← Back to Home

Canister Integration Guide

Version 1.0.0 — February 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 App (frontend) | v Your Canister --inter-canister call--> Nanogate (7jsss-6qaaa-aaaad-aegpq-cai) | v Checks delegation: - Is canister registered? - Is it active? - Limits not exceeded? - Not expired? | v Charges credits to delegation owner

1 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 Configure Dependencies

Edit src/my_canister_backend/Cargo.toml:

[package]
name = "my_canister_backend"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

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

Note: serde is only needed for the Deserialize derive on Nanogate types. If you use candid::Deserialize re-export instead, you can omit it.

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:

use ic_cdk::call::Call;

// 1. Build and execute the call
let response = Call::unbounded_wait(nanogate_id(), "method_name")
    .with_args(&(arg1, arg2))  // or .with_arg(&single_arg)
    .await;                       // → Result<Response, CallFailed>

// 2. Decode the Candid response
let result: T = response.candid()?;  // → Result<T, Error>

Nanogate functions return Result<T, NanoError> (Candid: variant { Ok : T; Err : NanoError }). The ICP inter-canister 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
#[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),
    }
}

About query functions: Functions like validate_nano_address, convert_raw_to_nano, and convert_nano_to_raw are query functions on Nanogate. When called via inter-canister call, they run as replicated (update) calls and cost cycles. For frontend-only use cases, call these directly from JavaScript — they're free as queries.

5 Define the Candid Interface

Edit src/my_canister_backend/my_canister_backend.did:

Every public function in your Rust code needs a matching entry here. The types must match your Rust function signatures.

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

6 Build and Deploy

First, convert some ICP to cycles (you need ~0.5 TC for canister creation plus installation):

# Convert ICP to cycles (run once)
dfx cycles convert --amount 1 --network ic

Then deploy in one step:

dfx deploy my_canister_backend --network ic

This creates the canister, builds your code, and installs it. Your canister ID will be shown in the output — note it down, you'll need it next.

Tip: If dfx deploy fails with "insufficient cycles", you can create the canister with an explicit cycle amount and then deploy:

dfx canister create my_canister_backend --network ic --with-cycles 500000000000
dfx deploy my_canister_backend --network ic

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

Optional limits you can set:

You can update limits anytime with update_canister_limits or deactivate with set_canister_active.

Register your canister after deployment. Only one user can hold a canister's delegation at a time.

8 Test

# Free call - no credits needed
dfx canister call <YOUR_CANISTER_ID> validate_address \
  '("nano_3fny73d...")' \
  --network ic

# Paid call - credits charged to your delegation
dfx canister call <YOUR_CANISTER_ID> check_balance \
  '("nano_3t6k35gi95xu...")' \
  --network ic

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

Quick Reference: Nanogate Functions

FunctionCostReturns
get_canister_versionFree (public)text
validate_nano_addressFree*Result<(), NanoError>
convert_raw_to_nanoFree*Result<text, NanoError>
convert_nano_to_rawFree*Result<text, NanoError>
get_account_balance10 creditsResult<BalanceResponse, NanoError>
send_simple_nano_request10+ creditsNanoResponse (not a Result!)

* No credits charged, but requires delegation (registered canister or user account).

Note: send_simple_nano_request returns NanoResponse directly instead of Result<T, NanoError>. Check the success field and error field of the response.

Full API reference: 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 },
    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 },
}

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

#[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 —