Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions motoko/icp_transfer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ The example exposes three functions to make this concrete:

> The ICP ledger also supports the [ICRC-1](https://github.com/dfinity/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.

## Calling the ICP ledger

The backend reaches the ledger through the typed import `import IcpLedger "canister:icp_ledger"` — no ledger types are declared in the code. Because the ICP ledger lives at the **same well-known principal** (`ryjl3-tyaaa-aaaaa-aaaba-cai`) on both mainnet and the local development network, the `--actor-id-alias` flag in `mops.toml` binds the import to that fixed id and types it against the ledger's official Candid interface (`candid/icp_ledger.did`). The request/response types (`IcpLedger.Tokens`, `IcpLedger.TransferArgs`, …) come straight from that interface.

> `--actor-id-alias` fits here because the target's id is *fixed and universal*. When a target's id varies per environment, `--actor-env-alias` resolves it from an injected env var instead; when the target is chosen at runtime, generate bindings and construct `actor(principal)` per call.

`candid/icp_ledger.did` is the ledger's own interface, taken from the [ICP ledger suite release](https://github.com/dfinity/ic/releases/tag/ledger-suite-icp-2025-08-29) (`ledger.did`). To refresh it after a new ledger release, download the `ledger.did` asset from that release.

## Build and deploy from the command line

### Prerequisites
Expand Down
56 changes: 16 additions & 40 deletions motoko/icp_transfer/backend/app.mo
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,24 @@ import Result "mo:core/Result";
import Error "mo:core/Error";
import Principal "mo:core/Principal";

// The ICP ledger is imported by name via `canister:icp_ledger`. It lives at the
// same well-known principal (ryjl3-tyaaa-aaaaa-aaaba-cai) on both mainnet and the
// local development network, so the mops.toml `--actor-id-alias` flag binds this
// import to that fixed id and types it against the ledger's official Candid
// interface (candid/icp_ledger.did) — no ledger types are declared here.
import IcpLedger "canister:icp_ledger";

actor IcpTransfer {
// ICP Ledger types (matches the ICP ledger Candid interface)
type Tokens = { e8s : Nat64 };
type SubAccount = Blob;
type BlockIndex = Nat64;

// Shared transfer logic.
//
// An AccountIdentifier is a 32-byte blob that encodes a principal and an
// optional subaccount. It is the native account format used by the ICP ledger.
// optional subaccount the native account format used by the ICP ledger.
// Centralized exchanges (CEXs) identify accounts by this blob; wallets and
// newer integrations prefer the ICRC-1 format (principal + subaccount directly).
//
// Use Principal.toLedgerAccount(subaccount) to compute one from a principal.
type AccountIdentifier = Blob;

type TimeStamp = { timestamp_nanos : Nat64 };

type LedgerTransferArgs = {
memo : Nat64;
amount : Tokens;
fee : Tokens;
from_subaccount : ?SubAccount;
to : AccountIdentifier;
created_at_time : ?TimeStamp;
};

type TransferError = {
#BadFee : { expected_fee : Tokens };
#InsufficientFunds : { balance : Tokens };
#TxTooOld : { allowed_window_nanos : Nat64 };
#TxCreatedInFuture;
#TxDuplicate : { duplicate_of : BlockIndex };
};

type TransferResult = { #Ok : BlockIndex; #Err : TransferError };

// The ICP ledger is a system canister available on both mainnet and
// the local development network at this well-known principal.
let icpLedger : actor { transfer : (LedgerTransferArgs) -> async TransferResult } = actor ("ryjl3-tyaaa-aaaaa-aaaba-cai");

// Shared transfer logic.
func doTransfer(amount : Tokens, to : AccountIdentifier) : async Result.Result<BlockIndex, Text> {
let transferArgs : LedgerTransferArgs = {
func doTransfer(amount : IcpLedger.Tokens, to : IcpLedger.AccountIdentifier) : async Result.Result<IcpLedger.BlockIndex, Text> {
let transferArgs : IcpLedger.TransferArgs = {
memo = 0;
amount;
fee = { e8s = 10_000 };
Expand All @@ -59,7 +35,7 @@ actor IcpTransfer {
created_at_time = null;
};
try {
switch (await icpLedger.transfer(transferArgs)) {
switch (await IcpLedger.transfer(transferArgs)) {
case (#Err(e)) #err("Transfer failed: " # debug_show e);
case (#Ok(blockIndex)) #ok blockIndex;
};
Expand All @@ -70,15 +46,15 @@ actor IcpTransfer {

// Convert a principal and optional subaccount to its AccountIdentifier as a
// lowercase hex string — the format shown in block explorers and CEX deposit screens.
public query func toAccountIdHex(p : Principal, subaccount : ?SubAccount) : async Text {
public query func toAccountIdHex(p : Principal, subaccount : ?IcpLedger.SubAccount) : async Text {
let bytes = Array.fromIter(p.toLedgerAccount(subaccount).vals());
Hex.toText(bytes);
};

// Transfer ICP to a recipient identified by principal + optional subaccount.
// Internally calls Principal.toLedgerAccount to derive the AccountIdentifier.
// This is the most convenient form when you have a principal.
public shared func transferToPrincipal(amount : Tokens, toPrincipal : Principal, toSubaccount : ?SubAccount) : async Result.Result<BlockIndex, Text> {
public shared func transferToPrincipal(amount : IcpLedger.Tokens, toPrincipal : Principal, toSubaccount : ?IcpLedger.SubAccount) : async Result.Result<IcpLedger.BlockIndex, Text> {
Debug.print("Transferring " # debug_show amount # " to principal " # debug_show toPrincipal);
await doTransfer(amount, toPrincipal.toLedgerAccount(toSubaccount));
};
Expand All @@ -91,7 +67,7 @@ actor IcpTransfer {
// validates it and returns a clear error on mismatch. A fromHex helper with
// CRC32 validation (equivalent to AccountIdentifier::from_hex in ic-ledger-types)
// would be a valuable addition to a Motoko ICP library.
public shared func transferToAccountId(amount : Tokens, toAccountIdHex : Text) : async Result.Result<BlockIndex, Text> {
public shared func transferToAccountId(amount : IcpLedger.Tokens, toAccountIdHex : Text) : async Result.Result<IcpLedger.BlockIndex, Text> {
switch (Hex.toArray(toAccountIdHex)) {
case (#err(e)) #err("invalid hex: " # e);
case (#ok(bytes)) {
Expand Down
Loading
Loading