ICP Transfer demonstrates how a canister can hold ICP and send it to other accounts using the ICP ledger.
The ICP ledger identifies accounts with a 32-byte AccountIdentifier — a hash of a principal and an optional subaccount. Centralized exchanges (CEXs) use this format for deposit addresses; wallets and newer integrations typically prefer the ICRC-1 account format (principal + subaccount directly). AccountIdentifier::new(&principal, &subaccount) in Rust performs this conversion.
The example exposes three functions to make this concrete:
to_account_id_hex(principal, subaccount)— query returning the AccountIdentifier as a 64-char lowercase hex string, the format shown in block explorers and CEX deposit screens.transfer_to_principal(amount, principal, subaccount)— callsAccountIdentifier::newinternally. Use this when you have a principal.transfer_to_account_id(amount, account_id_hex)— accepts the AccountIdentifier as a 64-char hex string (the format CEXs and block explorers provide). Validates the embedded CRC32 checksum before transferring.
The ICP ledger also supports the ICRC-1 standard via
icrc1_transfer. For new token integrations that don't require AccountIdentifier compatibility, ICRC-1 is the recommended interface. A comprehensive ICRC ledger example is planned.
- Node.js
- icp-cli:
npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm
icp network start -d
icp deploy
bash test.sh
icp network stopbash test.sh funds the backend with 2 ICP, then:
- Compares
icp identity account-id --format ledgerwithto_account_id_hexto verify CLI and backend compute the same AccountIdentifier. - Calls
transfer_to_principal— transfers 99_990_000 e8s (amount) + 10_000 e8s (fee) = exactly 1 ICP deducted from the backend; confirms both sides viaicp token balance. - Calls
transfer_to_account_idwith the hex string from step 1 — same 1 ICP deduction, confirming both transfer paths reach the same account and the backend balance reaches zero.
Refer to the security best practices for information on security and best practices for your ICP app. For this example the following aspects are particularly relevant:
- Securely handle traps in callbacks: issues around inter-canister calls (here the ledger) can lead to time-of-check time-of-use or double-spending bugs.
- Certified variables: essential when displaying financial data that users act on.