← Back to Home

Canister Integration Guide

Version 1.1.0 — February 28, 2026

A step-by-step guide to calling Nanogate from your own ICP canister. Create a Rust canister, define the required types, and make inter-canister calls to query Nano balances, validate addresses, and more.

Prerequisites

How It Works

Your Canister --inter-canister call--> Nanogate (7jsss-6qaaa-aaaad-aegpq-cai) | v Is canister registered? | v Charges credits to delegation owner

1 Create Identity and Project

If you already have a dfx identity (the one registered on Nanogate), just make sure it's active:

# Check current identity and principal
dfx identity whoami
dfx identity get-principal

If you don't have an identity yet, create one. dfx 0.30.2+ requires a named identity (not default) for mainnet operations:

# Create identity (will prompt for password, or use --storage-mode plaintext)
dfx identity new my_identity
dfx identity use my_identity

Plaintext identity: If you use --storage-mode plaintext, dfx will warn on every mainnet command. Suppress with: export DFX_WARNING=-mainnet_plaintext_identity

Then create the project:

dfx new my_canister --type rust --no-frontend
cd my_canister

This creates the project structure:

my_canister/
├── dfx.json
├── Cargo.toml
└── src/my_canister_backend/
    ├── Cargo.toml
    ├── my_canister_backend.did
    └── src/lib.rs

2 Verify Dependencies

Open src/my_canister_backend/Cargo.toml and verify these dependencies are present (dfx 0.30.2+ generates them automatically):

[dependencies]
candid = "0.10"
ic-cdk = "0.19"
serde = { version = "1", features = ["derive"] }

If serde is missing, add it manually. The other two should already be there.

3 Define Nanogate Types

Your canister needs Rust types that match Nanogate's Candid interface. At minimum, define the response types you'll use and the full NanoError enum for error handling.

In src/my_canister_backend/src/lib.rs:

use candid::{CandidType, Deserialize, Principal};

const NANOGATE_CANISTER: &str = "7jsss-6qaaa-aaaad-aegpq-cai";

fn nanogate_id() -> Principal {
    Principal::from_text(NANOGATE_CANISTER).unwrap()
}

// -- Response types (add more as needed) --

#[derive(CandidType, Deserialize, Clone, Debug)]
pub struct BalanceResponse {
    pub response_time_ms: Option<u64>,
    pub node_used: Option<String>,
    pub balance: String,
    pub pending: Option<String>,
}

// -- NanoError + all sub-enums --
// See the complete type definitions in the Appendix below.
// Every variant must be defined, even if you don't use it.
// Candid deserialization fails if any variant is missing.

Important: Every variant of NanoError and its sub-enums must be defined exactly as shown in the Appendix. Candid deserialization fails if any variant is missing.

4 Write Inter-Canister Calls

ic-cdk 0.19 uses the Call builder pattern for inter-canister calls. Nanogate functions return Result<T, NanoError>, and the IC call itself can also fail, giving you two levels of error handling.

use ic_cdk::call::Call;

type NanogateResult<T> = Result<T, NanoError>;

// Paid function - 10 credits per call (base price)
#[ic_cdk::update]
async fn check_balance(account: String, node: Option<String>) -> String {
    // Make inter-canister call
    let response = match Call::unbounded_wait(nanogate_id(), "get_account_balance")
        .with_args(&(account, node))
        .await
    {
        Ok(r) => r,
        Err(e) => return format!("IC call failed: {:?}", e),
    };

    // Decode Candid response
    let result: NanogateResult<BalanceResponse> = match response.candid() {
        Ok(r) => r,
        Err(e) => return format!("Decode error: {:?}", e),
    };

    match result {
        Ok(resp) => format!(
            "balance: {}, pending: {:?}",
            resp.balance, resp.pending
        ),
        Err(e) => format!("Nanogate error: {:?}", e),
    }
}

// No credits, but requires delegation
#[ic_cdk::update]
async fn validate_address(address: String) -> String {
    let response = match Call::unbounded_wait(nanogate_id(), "validate_nano_address")
        .with_arg(&address)
        .await
    {
        Ok(r) => r,
        Err(e) => return format!("IC call failed: {:?}", e),
    };

    let result: NanogateResult<()> = match response.candid() {
        Ok(r) => r,
        Err(e) => return format!("Decode error: {:?}", e),
    };

    match result {
        Ok(()) => "valid".to_string(),
        Err(e) => format!("invalid: {:?}", e),
    }
}

// No credits, but requires delegation
#[ic_cdk::update]
async fn raw_to_nano(raw: String) -> String {
    let response = match Call::unbounded_wait(nanogate_id(), "convert_raw_to_nano")
        .with_arg(&raw)
        .await
    {
        Ok(r) => r,
        Err(e) => return format!("IC call failed: {:?}", e),
    };

    let result: NanogateResult<String> = match response.candid() {
        Ok(r) => r,
        Err(e) => return format!("Decode error: {:?}", e),
    };

    match result {
        Ok(nano) => nano,
        Err(e) => format!("error: {:?}", e),
    }
}

5 Define the Candid Interface

The .did file describes your canister's public API using the Candid interface language. Edit src/my_canister_backend/my_canister_backend.did:

Every public function in your Rust code (#[ic_cdk::update] or #[ic_cdk::query]) needs a matching entry here:

service : {
    "check_balance": (text, opt text) -> (text);
    "validate_address": (text) -> (text);
    "raw_to_nano": (text) -> (text);
}

6 Build and Deploy

Canisters on the Internet Computer run on cycles. You need to convert ICP tokens into cycles before deploying.

Get your ledger address and fund it

# Show your dfx ledger address
dfx ledger account-id --network ic

Send ICP to this address from an exchange or wallet. Then convert to cycles:

# Convert ICP to cycles (1 TC costs ~$1.35)
# Note: A 0.0001 ICP transfer fee applies, so use slightly less than your full balance
dfx cycles convert --amount 0.9 --network ic

Create and deploy

# Create canister with enough cycles for creation + installation
dfx canister create my_canister_backend --network ic --with-cycles 1000000000000

# Build and install code
dfx deploy my_canister_backend --network ic

Your canister ID will be shown after dfx canister create — note it down, you'll need it in the next step.

Troubleshooting: If dfx deploy fails with "out of cycles", top up your canister with dfx canister deposit-cycles 200000000000 my_canister_backend --network ic and retry.

7 Register Canister Delegation

Before your canister can make paid Nanogate calls, you need to register it under your Nanogate account. This links the canister to your credits.

# Register your canister
dfx canister call 7jsss-6qaaa-aaaad-aegpq-cai add_canister_to_my_account \
  '(record {
    canister_id = principal "<YOUR_CANISTER_ID>";
    credits_limit = null;
    daily_limit = null;
    expires_at = null
  })' --network ic

The null values mean no limits. You can set spending limits later with update_canister_limits.

Note: Only canister principals (ending in -cai) can be delegated. User principals are not allowed — they should register directly via the donation flow.

8 Test

# Free call - no credits needed, but delegation required
dfx canister call <YOUR_CANISTER_ID> validate_address \
  '("nano_3rrfgsjs653mefzpf81sqcty13cg5aseoccem5sajwjb1crqkxmdksdkhe14")' \
  --network ic
# Expected: ("valid")

# Paid call - credits charged to your Nanogate account
dfx canister call <YOUR_CANISTER_ID> check_balance \
  '("nano_3rrfgsjs653mefzpf81sqcty13cg5aseoccem5sajwjb1crqkxmdksdkhe14", null)' \
  --network ic
# Expected: ("balance: ..., pending: ...")

# Check remaining credits on Nanogate
dfx canister call 7jsss-6qaaa-aaaad-aegpq-cai get_my_user_credits '()' --network ic

Without delegation: If you forgot Step 7, calls will return Auth(NotRegistered). Register the delegation first, then retry.

Quick Reference: Nanogate Functions

FunctionCostReturns
validate_nano_addressFree*Result<(), NanoError>
convert_raw_to_nanoFree*Result<text, NanoError>
convert_nano_to_rawFree*Result<text, NanoError>
get_account_balance10 credits (base price)Result<BalanceResponse, NanoError>

* No credits charged, but requires delegation.

Full list of available functions: nanogate.run/docs/api.html

Appendix: Complete NanoError Type Definitions

These Rust types must match Nanogate's Candid interface exactly. Copy them into your lib.rs.

Show/Hide NanoError types (click to expand)
#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum NanoError {
    Pow(PowErrorCode),
    Rpc(RpcErrorCode),
    Auth(AuthErrorCode),
    Transaction(TxErrorCode),
    Node(NodeErrorCode),
    Donation(DonationErrorCode),
    Price(PriceErrorCode),
    Dispenser(DispenserErrorCode),
    RateLimit(RateLimitErrorCode),
    General(GeneralErrorCode),
    Credit(CreditErrorCode),
    Purchase(PurchaseErrorCode),
    Validation(ValidationErrorCode),
    Config(ConfigErrorCode),
    Worker(WorkerErrorCode),
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum AuthErrorCode {
    AlreadyOwner,
    SelfTransfer,
    RpcBlocked { action: String },
    NotRegistered,
    LastOwner,
    NotFound,
    NotOwner,
    Blacklisted { abuse_count: u64 },
    Other(String),
    NoOwner,
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum CreditErrorCode {
    CanisterNotFound,
    SelfTransfer,
    NotRegistered,
    Anonymous,
    PurchaseMaximumExceeded { provided: u64, maximum: u64 },
    CanisterTotalLimitReached { used: u64, limit: u64 },
    DelegationNotFound,
    CanisterExpired,
    ZeroAmount,
    AlreadyRegistered,
    BrowserNotAllowed,
    Insufficient { have: u64, need: u64 },
    Unauthorized,
    CanisterDailyLimitReached { used: u64, limit: u64 },
    BalanceOverflow,
    TransferMinimumNotMet { provided: u64, required: u64 },
    NotOwner,
    CanisterLimitReached { maximum: u64 },
    BrowserFlowDisabled,
    Other(String),
    RecipientNotRegistered,
    CanisterOwned,
    CanisterAlreadyRegistered,
    CanisterDisabled,
    UserNotFound { principal: String },
    TransferMaximumExceeded { provided: u64, maximum: u64 },
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum ValidationErrorCode {
    CountZero,
    InvalidAddress { address: String },
    NotBlacklisted,
    InvalidAmountFormat,
    SeedLength,
    SizeTooLarge { max: u64, field: String },
    SizeTooSmall { min: u64 },
    BlockedAddress { address: String },
    UserNotAllowed,
    SelfDelegation,
    HashLength,
    MissingParameter { name: String },
    UrlScheme,
    PercentageExceeded { max: u64, field: String },
    CountExceeded { max: u64 },
    Other(String),
    MustBePositive { field: String },
    KeyLength,
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum RpcErrorCode {
    OutcallFailed { details: String },
    ActionNotAllowed { action: String, hint: String },
    BlockParseFailed { details: String },
    JsonSerializeFailed { details: String },
    ConnectionFailed,
    ParseError { details: String },
    Timeout,
    NodeError { details: String },
    ResponseFieldMissing { field: String },
    Other(String),
    JsonDeserializeFailed { details: String },
    HttpError { status: u16 },
    BalanceNotFound,
    Utf8DecodeError { details: String },
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum NodeErrorCode {
    PrivateNotRegistered,
    NoApiKeys,
    AuthHeaderInvalid,
    NoNodes,
    OwnerNodeNotConfigured,
    NotFound,
    ApiKeyMismatch,
    PrivateAlreadyExists,
    AlreadyExists,
    PublicPrivateUrl,
    AliasInvalid,
    AliasTaken { alias: String },
    PrivatePublicUrl,
    ApiKeyNotFound,
    AuthPrefixInvalid,
    Other(String),
    AliasChars,
    UrlInvalid { reason: String },
    ApiKeyShort,
    NoOwnerAssigned { alias: String },
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum PowErrorCode {
    ValidationFailed,
    ServerExists,
    JsonParseError { details: String },
    DifficultyInvalid,
    HashInvalid,
    ChallengeDifficultyChanged,
    AllFailed { last_error: String },
    ChallengeExpired,
    ApiKeyMismatch,
    UrlScheme,
    ChallengeNotFound,
    ServerNotFound,
    AliasInvalid,
    InvalidWork,
    AliasTaken { alias: String },
    ApiKeyNotFound,
    WorkNotInResponse,
    NoOwnerAssigned { alias: String },
    Other(String),
    FetchFailed { reason: String },
    AliasChars,
    NoServers,
    HttpError { status: u16 },
    ChallengeUsed,
    FrontierInvalid,
    TooManyChallenges { max: u64 },
    Utf8Error,
    ServerError { details: String },
    ApiKeyShort,
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum TxErrorCode {
    WorkMissing,
    FrontierNotFound,
    Overflow,
    InvalidBlock,
    PowFailed { reason: String },
    AccountInfoFailed { reason: String },
    LinkConvertFailed { reason: String },
    InvalidKey,
    BroadcastFailed { reason: String },
    InvalidHash,
    ResponseParseFailed { reason: String },
    HashFailed { reason: String },
    SignatureInvalid { reason: String },
    BlockSerializeFailed { reason: String },
    RepresentativeNotFound,
    Other(String),
    PubkeyExtractFailed { reason: String },
    HashNotFound,
    BalanceNotFound,
    InsufficientFunds,
    SigningFailed { reason: String },
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum DonationErrorCode {
    MaxPendingDonations,
    OrderCancelled,
    HighDemand,
    OrderNotFound,
    OrderExpired,
    NoAddressAvailable,
    OrderAlreadyConfirmed,
    Other(String),
    UserBlocked { abuse_count: u64 },
    CreditsDisabled,
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum PriceErrorCode {
    ApiKeyNotSet,
    CurrencyInvalid { currency: String },
    ApiUnavailable,
    FetchFailed { details: String },
    Other(String),
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum DispenserErrorCode {
    ApiKeyNotSet,
    Unauthorized,
    ParseError { details: String },
    FetchFailed { details: String },
    Forbidden,
    ApiKeyShort,
    Other(String),
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum RateLimitErrorCode {
    TooManyRequests,
    NoSlotsAvailable,
    TransferLimitExceeded,
    PerMinuteLimitReached,
    ConfigInvalid { reason: String },
    FairnessLimitReached,
    GlobalLimitReached,
    EconomyCutoff,
    Other(String),
    UserConcurrentLimitReached,
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum GeneralErrorCode {
    NotRegistered,
    AlreadyRegistrar,
    AlreadyRegistered,
    NotRegistrar,
    NotAuthorized,
    Other(String),
    AlreadyAuthorized,
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum ConfigErrorCode {
    LogMaxSize,
    LogMinSize,
    Other(String),
    AlertNotFound,
    PriceNotFound { action: String },
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum PurchaseErrorCode {
    OrderCancelled,
    HighDemand,
    MaxPendingOrders,
    InsufficientPayment { expected: String, received: String },
    DisabledBeta,
    PriceUnavailable,
    OrderNotFound,
    FallbackNotFound { package_id: u8 },
    OrderExpired,
    PackageInactive,
    NoAddressAvailable,
    PackageNotFound,
    OrderAlreadyConfirmed,
    Other(String),
    UserBlocked { abuse_count: u64 },
}

#[derive(CandidType, Deserialize, Clone, Debug)]
pub enum WorkerErrorCode {
    CallFailed { details: String },
    AlreadyRegistered,
    NotFound,
    NoWorkers,
    AliasInvalid,
    CandidError { details: String },
    Other(String),
    InternalError { details: String },
}

— End of Guide —