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.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh (if already installed, run rustup update stable — ic-cdk 0.19 requires a recent Rust version)rustup target add wasm32-unknown-unknowndfx identity get-principal).add_canister_to_my_account (see Step 7)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
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.
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. 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),
}
}
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);
}
Canisters on the Internet Computer run on cycles. You need to convert ICP tokens into cycles before deploying.
# 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 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.
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.
# 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.
| Function | Cost | Returns |
|---|---|---|
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 (base price) | Result<BalanceResponse, NanoError> |
* No credits charged, but requires delegation.
Full list of available functions: 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 },
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 —