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.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shrustup target add wasm32-unknown-unknowndfx identity new my_identity && dfx identity use my_identitydfx ledger account-id).add_canister_to_my_account (see Step 7)get_canister_version) work without any delegationdfx 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
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.
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.
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.
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);
}
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
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:
credits_limit: Maximum total credits this canister can spend (e.g. opt (1000 : nat64))daily_limit: Maximum credits per day (e.g. opt (100 : nat64))expires_at: Expiration timestamp in nanoseconds (e.g. opt (1735689600000000000 : nat64))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.
# 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
| Function | Cost | Returns |
|---|---|---|
get_canister_version | Free (public) | text |
validate_nano_address | Free* | Result<(), NanoError> |
convert_raw_to_nano | Free* | Result<text, NanoError> |
convert_nano_to_raw | Free* | Result<text, NanoError> |
get_account_balance | 10 credits | Result<BalanceResponse, NanoError> |
send_simple_nano_request | 10+ credits | NanoResponse (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
These Rust types must match Nanogate's Candid interface exactly. Copy them into your lib.rs.
#[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 —