Version 1.1.1 — October 17, 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.
sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" — releases and notes at github.com/dfinity/sdk. This guide is written for dfx. DFINITY's own getting-started pages now install icp-cli instead, which does not provide the dfx commands used below, and dfx itself prints a deprecation notice from 0.32 on — see the note in step 1.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 refuses mainnet commands when the identity is stored unencrypted — which the auto-created default identity always is:
# 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 — and the auto-created default identity is always plaintext — dfx refuses every mainnet command, it does not merely warn. Acknowledge the risk to proceed: export DFX_WARNING=-mainnet_plaintext_identity
Deprecation warning: Since dfx 0.32, every command also prints WARNING: dfx is deprecated, use icp-cli. It is written to stderr and changes nothing about what a command does. DFINITY has published icp-cli as the successor to dfx but has not announced an end-of-life date. To suppress both warnings at once, combine the values with a comma: export DFX_WARNING=-mainnet_plaintext_identity,-deprecation
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 },
NotGovernance,
NotKeyOperator,
OwnerCannotSelfDelete,
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 —