From da13a52f54f5e3fdb10c257205834af2d888671b Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 28 Jul 2026 21:35:57 +0200 Subject: [PATCH 1/3] refactor(motoko/icrc2-swap): generate ICRC bindings from a committed .did MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the hand-written `backend/ICRC.mo` type module with Motoko bindings generated by `didc` from a committed ICRC-1/ICRC-2 interface, demonstrating the pattern for a backend that calls *many* ledgers with the ledger chosen at runtime — the complement to the single-fixed-target `--actor-env-alias` examples. - candid/icrc.did — the ICRC-1/ICRC-2 interface the backend calls (types taken from the ic-icrc1-ledger reference wasm, as a focused plain service). Regen source. - bindings/ICRC.mo — `didc bind -t mo candid/icrc.did`; committed so building needs no extra tooling. - backend/app.mo — import the generated bindings; `actor() : ICRC.Self` keeps the runtime-chosen-principal pattern. - removed backend/ICRC.mo (hand-written types). Also, unrelated tidy-ups in the same file/example: - Map `swap` (return value always discarded) -> `add` (`ignore swap`), 6 sites. - bump mo:core 2.5.0 -> 2.6.0. Verified end-to-end: `bash deploy.sh && bash test.sh` — all 7 tests pass (deposit via icrc2_transfer_from, atomic swap, withdraw via icrc1_transfer), confirming the generated bindings are wire-compatible with the real ledger. Co-Authored-By: Claude Opus 4.8 (1M context) --- motoko/icrc2-swap/README.md | 12 +- motoko/icrc2-swap/backend/app.mo | 18 +- .../icrc2-swap/{backend => bindings}/ICRC.mo | 166 +++++++----------- motoko/icrc2-swap/candid/icrc.did | 125 +++++++++++++ motoko/icrc2-swap/mops.toml | 2 +- 5 files changed, 213 insertions(+), 110 deletions(-) rename motoko/icrc2-swap/{backend => bindings}/ICRC.mo (59%) create mode 100644 motoko/icrc2-swap/candid/icrc.did diff --git a/motoko/icrc2-swap/README.md b/motoko/icrc2-swap/README.md index ff65f2c2c3..b42686bb82 100644 --- a/motoko/icrc2-swap/README.md +++ b/motoko/icrc2-swap/README.md @@ -27,7 +27,7 @@ Three canisters: - **`token_a` / `token_b`**: Standard ICRC-1/ICRC-2 ledger canisters, pre-built from the DFINITY IC release. - **`backend`**: The swap canister (`backend/app.mo`). Accepts deposits, performs 1:1 swaps, and processes withdrawals. It discovers the token canister principals automatically at runtime via `PUBLIC_CANISTER_ID:token_a` / `PUBLIC_CANISTER_ID:token_b` environment variables injected by icp-cli. -`backend/ICRC.mo` defines the ICRC-1/2 types and actor interface used by the backend. These are defined inline (rather than from a mops package) so the full interface is visible in the example. +The backend talks to the token ledgers through a single generated Motoko binding, `bindings/ICRC.mo`, produced from the ICRC-1/ICRC-2 interface in `candid/icrc.did`. Every token reference is typed against that one shared type — `actor() : ICRC.Self` — so the same binding serves both `token_a` and `token_b` (and would serve any ICRC-1/2 ledger the backend is pointed at, e.g. ckBTC or an SNS token). This is the "one interface, many ledgers, principal chosen at runtime" pattern: because the target is dynamic, the types come from the standard Candid interface rather than being bound to a single canister id. The binding is committed, so building the example needs no extra tooling. ## Build and deploy from the command line @@ -62,6 +62,16 @@ icp network stop `bash test.sh` runs the full swap flow with `icrc2-alice` and `icrc2-bob` as the two parties. Test 2 verifies that swapping with no deposits returns `InsufficientBalance`. Tests 6 and 7 verify the actual token balance delta in the ledger after withdrawal, confirming the full round-trip. Tests are idempotent — they can be run multiple times without redeploying. +## Updating the token interface bindings + +`candid/icrc.did` is the ICRC-1/ICRC-2 interface the backend calls; `bindings/ICRC.mo` is the Motoko binding generated from it. The binding is committed, so no extra tooling is needed to build. If you change the interface, regenerate the binding with [`didc`](https://github.com/dfinity/candid/releases): + +```bash +didc bind -t mo candid/icrc.did > bindings/ICRC.mo +``` + +(The ICRC-1/2 type names are already PascalCase, so `didc` output is idiomatic as-is — no post-processing needed.) + ## Fee handling ICRC-1 tokens charge a `transfer_fee` (10,000 e8s in this example) on every transfer through the ledger. diff --git a/motoko/icrc2-swap/backend/app.mo b/motoko/icrc2-swap/backend/app.mo index 94eaaeef8b..2befcf53b8 100644 --- a/motoko/icrc2-swap/backend/app.mo +++ b/motoko/icrc2-swap/backend/app.mo @@ -6,7 +6,7 @@ import Principal "mo:core/Principal"; import Result "mo:core/Result"; import Runtime "mo:core/Runtime"; -import ICRC "ICRC"; +import ICRC "../bindings/ICRC"; // The swap canister accepts deposits of two ICRC-2 tokens, swaps balances // 1:1 between users, and allows withdrawals. @@ -58,7 +58,7 @@ actor Swap { // - user deposits their token: `swap_canister.deposit({ token=token_a; amount=amount; ... })` // - These deposit handlers show how to safely accept and register deposits of an ICRC-2 token. public shared func deposit(args : DepositArgs) : async Result.Result { - let token : ICRC.Actor = actor (args.token.toText()); + let token : ICRC.Self = actor (args.token.toText()); let balances = which_balances(args.token); // Load the fee from the token here. The user can pass a null fee, which @@ -99,7 +99,7 @@ actor Swap { // Credit the sender's account let sender = args.from.owner; let old_balance = balances.get(sender).get(0 : Nat); - let _ = balances.swap(sender, old_balance + args.amount); + balances.add(sender, old_balance + args.amount); // Return the "block height" of the transfer #ok(block_height); @@ -131,7 +131,7 @@ actor Swap { }; // Give user_a's token_a to user_b - let _ = balancesA.swap( + balancesA.add( args.user_b, balancesA.get(args.user_a).get(0 : Nat) + balancesA.get(args.user_b).get(0 : Nat), @@ -139,7 +139,7 @@ actor Swap { balancesA.remove(args.user_a); // Give user_b's token_b to user_a - let _ = balancesB.swap( + balancesB.add( args.user_a, balancesB.get(args.user_a).get(0 : Nat) + balancesB.get(args.user_b).get(0 : Nat), @@ -179,7 +179,7 @@ actor Swap { Runtime.trap("anonymous caller not allowed"); }; - let token : ICRC.Actor = actor (args.token.toText()); + let token : ICRC.Self = actor (args.token.toText()); let balances = which_balances(args.token); let fee = switch (args.fee) { @@ -202,7 +202,7 @@ actor Swap { if (new_balance == 0) { balances.remove(msg.caller); } else { - let _ = balances.swap(msg.caller, new_balance); + balances.add(msg.caller, new_balance); }; // Perform the transfer, to send the tokens. @@ -222,7 +222,7 @@ actor Swap { } catch (e) { // Token ledger trapped — refund and surface the error. let b = balances.get(msg.caller).get(0 : Nat); - let _ = balances.swap(msg.caller, b + args.amount + fee); + balances.add(msg.caller, b + args.amount + fee); return #err(#CallFailed(e.message())); }; @@ -231,7 +231,7 @@ actor Swap { case (#Err(err)) { // Transfer failed — refund the user's account. let b = balances.get(msg.caller).get(0 : Nat); - let _ = balances.swap(msg.caller, b + args.amount + fee); + balances.add(msg.caller, b + args.amount + fee); return #err(#TransferError(err)); }; }; diff --git a/motoko/icrc2-swap/backend/ICRC.mo b/motoko/icrc2-swap/bindings/ICRC.mo similarity index 59% rename from motoko/icrc2-swap/backend/ICRC.mo rename to motoko/icrc2-swap/bindings/ICRC.mo index b0dd706db5..9ff6431e35 100644 --- a/motoko/icrc2-swap/backend/ICRC.mo +++ b/motoko/icrc2-swap/bindings/ICRC.mo @@ -1,131 +1,99 @@ -module ICRC { - public type BlockIndex = Nat; - public type Subaccount = Blob; - // Number of nanoseconds since the UNIX epoch in UTC timezone. - public type Timestamp = Nat64; - // Number of nanoseconds between two [Timestamp]s. - public type Tokens = Nat; - public type TxIndex = Nat; +// This is a generated Motoko binding. +// Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. - public type Account = { - owner : Principal; - subaccount : ?Subaccount; - }; - - public type TransferArg = { - from_subaccount : ?Subaccount; - to : Account; - amount : Tokens; +module { + public type Account = { owner : Principal; subaccount : ?Subaccount }; + public type Allowance = { allowance : Tokens; expires_at : ?Timestamp }; + public type AllowanceArgs = { account : Account; spender : Account }; + public type ApproveArgs = { fee : ?Tokens; memo : ?Blob; + from_subaccount : ?Subaccount; created_at_time : ?Timestamp; + amount : Tokens; + expected_allowance : ?Tokens; + expires_at : ?Timestamp; + spender : Account; }; - - public type TransferError = { - #BadFee : { expected_fee : Tokens }; - #BadBurn : { min_burn_amount : Tokens }; - #InsufficientFunds : { balance : Tokens }; - #TooOld; - #CreatedInFuture : { ledger_time : Nat64 }; + public type ApproveError = { + #GenericError : { message : Text; error_code : Nat }; #TemporarilyUnavailable; #Duplicate : { duplicate_of : BlockIndex }; - #GenericError : { error_code : Nat; message : Text }; - }; - - public type TransferResult = { - #Ok : BlockIndex; - #Err : TransferError; + #BadFee : { expected_fee : Tokens }; + #AllowanceChanged : { current_allowance : Tokens }; + #CreatedInFuture : { ledger_time : Timestamp }; + #TooOld; + #Expired : { ledger_time : Timestamp }; + #InsufficientFunds : { balance : Tokens }; }; - - // The value returned from the [icrc1_metadata] endpoint. + public type ApproveResult = { #Ok : BlockIndex; #Err : ApproveError }; + public type BlockIndex = Nat; public type MetadataValue = { - #Nat : Nat; #Int : Int; - #Text : Text; + #Nat : Nat; #Blob : Blob; + #Text : Text; }; - - public type ApproveArgs = { - from_subaccount : ?Subaccount; - spender : Account; - amount : Tokens; - expected_allowance : ?Tokens; - expires_at : ?Timestamp; + public type StandardRecord = { url : Text; name : Text }; + public type Subaccount = Blob; + public type Timestamp = Nat64; + public type Tokens = Nat; + public type TransferArg = { + to : Account; fee : ?Tokens; memo : ?Blob; + from_subaccount : ?Subaccount; created_at_time : ?Timestamp; + amount : Tokens; }; - - public type ApproveError = { + public type TransferError = { + #GenericError : { message : Text; error_code : Nat }; + #TemporarilyUnavailable; + #BadBurn : { min_burn_amount : Tokens }; + #Duplicate : { duplicate_of : BlockIndex }; #BadFee : { expected_fee : Tokens }; - #InsufficientFunds : { balance : Tokens }; - #AllowanceChanged : { current_allowance : Tokens }; - #Expired : { ledger_time : Nat64 }; + #CreatedInFuture : { ledger_time : Timestamp }; #TooOld; - #CreatedInFuture : { ledger_time : Nat64 }; - #Duplicate : { duplicate_of : BlockIndex }; - #TemporarilyUnavailable; - #GenericError : { error_code : Nat; message : Text }; - }; - - public type ApproveResult = { - #Ok : BlockIndex; - #Err : ApproveError; - }; - - public type AllowanceArgs = { - account : Account; - spender : Account; - }; - - public type Allowance = { - allowance : Tokens; - expires_at : ?Timestamp; + #InsufficientFunds : { balance : Tokens }; }; - public type TransferFromArgs = { - spender_subaccount : ?Subaccount; - from : Account; to : Account; - amount : Tokens; fee : ?Tokens; + spender_subaccount : ?Subaccount; + from : Account; memo : ?Blob; created_at_time : ?Timestamp; + amount : Tokens; }; - - public type TransferFromResult = { - #Ok : BlockIndex; - #Err : TransferFromError; - }; - public type TransferFromError = { - #BadFee : { expected_fee : Tokens }; - #BadBurn : { min_burn_amount : Tokens }; - #InsufficientFunds : { balance : Tokens }; + #GenericError : { message : Text; error_code : Nat }; + #TemporarilyUnavailable; #InsufficientAllowance : { allowance : Tokens }; - #TooOld; - #CreatedInFuture : { ledger_time : Nat64 }; + #BadBurn : { min_burn_amount : Tokens }; #Duplicate : { duplicate_of : BlockIndex }; - #TemporarilyUnavailable; - #GenericError : { error_code : Nat; message : Text }; + #BadFee : { expected_fee : Tokens }; + #CreatedInFuture : { ledger_time : Timestamp }; + #TooOld; + #InsufficientFunds : { balance : Tokens }; }; - - public type Actor = actor { - icrc1_name : shared query () -> async Text; - icrc1_symbol : shared query () -> async Text; + public type TransferFromResult = { + #Ok : BlockIndex; + #Err : TransferFromError; + }; + public type TransferResult = { #Ok : BlockIndex; #Err : TransferError }; + public type Self = actor { + icrc1_balance_of : shared query Account -> async Tokens; icrc1_decimals : shared query () -> async Nat8; - icrc1_metadata : shared query () -> async [(Text, MetadataValue)]; - icrc1_total_supply : shared query () -> async Tokens; icrc1_fee : shared query () -> async Tokens; + icrc1_metadata : shared query () -> async [(Text, MetadataValue)]; icrc1_minting_account : shared query () -> async ?Account; - icrc1_balance_of : shared query (Account) -> async Tokens; - icrc1_transfer : shared (TransferArg) -> async TransferResult; - icrc1_supported_standards : shared query () -> async [{ - name : Text; - url : Text; - }]; - icrc2_approve : shared (ApproveArgs) -> async ApproveResult; - icrc2_allowance : shared query (AllowanceArgs) -> async Allowance; - icrc2_transfer_from : shared (TransferFromArgs) -> async TransferFromResult; - }; -}; + icrc1_name : shared query () -> async Text; + icrc1_supported_standards : shared query () -> async [StandardRecord]; + icrc1_symbol : shared query () -> async Text; + icrc1_total_supply : shared query () -> async Tokens; + icrc1_transfer : shared TransferArg -> async TransferResult; + icrc2_allowance : shared query AllowanceArgs -> async Allowance; + icrc2_approve : shared ApproveArgs -> async ApproveResult; + icrc2_transfer_from : shared TransferFromArgs -> async TransferFromResult; + } +} diff --git a/motoko/icrc2-swap/candid/icrc.did b/motoko/icrc2-swap/candid/icrc.did new file mode 100644 index 0000000000..7e80b7df58 --- /dev/null +++ b/motoko/icrc2-swap/candid/icrc.did @@ -0,0 +1,125 @@ +// ICRC-1 + ICRC-2 token-ledger interface. +// +// This is the *standard* interface every ICRC-1/ICRC-2 ledger exposes — not a +// specific ledger. The swap backend types its `actor()` +// references against it, so the same generated bindings work for any ICRC-1/2 +// ledger the backend is pointed at (token_a, token_b, ckBTC, an SNS token, ...). +// +// The type definitions match the ICRC-1 ledger reference implementation +// (dfinity/ic, ledger-suite-icrc). Generate the Motoko bindings with: +// didc bind -t mo candid/icrc.did > bindings/ICRC.mo + +type Subaccount = blob; +type Timestamp = nat64; +type Tokens = nat; +type BlockIndex = nat; + +type Account = record { + owner : principal; + subaccount : opt Subaccount; +}; + +type TransferArg = record { + from_subaccount : opt Subaccount; + to : Account; + amount : Tokens; + fee : opt Tokens; + memo : opt blob; + created_at_time : opt Timestamp; +}; + +type TransferError = variant { + BadFee : record { expected_fee : Tokens }; + BadBurn : record { min_burn_amount : Tokens }; + InsufficientFunds : record { balance : Tokens }; + TooOld; + CreatedInFuture : record { ledger_time : Timestamp }; + TemporarilyUnavailable; + Duplicate : record { duplicate_of : BlockIndex }; + GenericError : record { error_code : nat; message : text }; +}; + +type TransferResult = variant { + Ok : BlockIndex; + Err : TransferError; +}; + +type MetadataValue = variant { + Nat : nat; + Int : int; + Text : text; + Blob : blob; +}; + +type StandardRecord = record { url : text; name : text }; + +type Allowance = record { allowance : Tokens; expires_at : opt Timestamp }; +type AllowanceArgs = record { account : Account; spender : Account }; + +type ApproveArgs = record { + from_subaccount : opt Subaccount; + spender : Account; + amount : Tokens; + expected_allowance : opt Tokens; + expires_at : opt Timestamp; + fee : opt Tokens; + memo : opt blob; + created_at_time : opt Timestamp; +}; + +type ApproveError = variant { + BadFee : record { expected_fee : Tokens }; + InsufficientFunds : record { balance : Tokens }; + AllowanceChanged : record { current_allowance : Tokens }; + Expired : record { ledger_time : Timestamp }; + TooOld; + CreatedInFuture : record { ledger_time : Timestamp }; + Duplicate : record { duplicate_of : BlockIndex }; + TemporarilyUnavailable; + GenericError : record { error_code : nat; message : text }; +}; + +type ApproveResult = variant { Ok : BlockIndex; Err : ApproveError }; + +type TransferFromArgs = record { + spender_subaccount : opt Subaccount; + from : Account; + to : Account; + amount : Tokens; + fee : opt Tokens; + memo : opt blob; + created_at_time : opt Timestamp; +}; + +type TransferFromError = variant { + BadFee : record { expected_fee : Tokens }; + BadBurn : record { min_burn_amount : Tokens }; + InsufficientFunds : record { balance : Tokens }; + InsufficientAllowance : record { allowance : Tokens }; + TooOld; + CreatedInFuture : record { ledger_time : Timestamp }; + Duplicate : record { duplicate_of : BlockIndex }; + TemporarilyUnavailable; + GenericError : record { error_code : nat; message : text }; +}; + +type TransferFromResult = variant { + Ok : BlockIndex; + Err : TransferFromError; +}; + +service : { + icrc1_name : () -> (text) query; + icrc1_symbol : () -> (text) query; + icrc1_decimals : () -> (nat8) query; + icrc1_metadata : () -> (vec record { text; MetadataValue }) query; + icrc1_total_supply : () -> (Tokens) query; + icrc1_fee : () -> (Tokens) query; + icrc1_minting_account : () -> (opt Account) query; + icrc1_balance_of : (Account) -> (Tokens) query; + icrc1_transfer : (TransferArg) -> (TransferResult); + icrc1_supported_standards : () -> (vec StandardRecord) query; + icrc2_approve : (ApproveArgs) -> (ApproveResult); + icrc2_allowance : (AllowanceArgs) -> (Allowance) query; + icrc2_transfer_from : (TransferFromArgs) -> (TransferFromResult); +} diff --git a/motoko/icrc2-swap/mops.toml b/motoko/icrc2-swap/mops.toml index d3f244542c..62f1f004b6 100644 --- a/motoko/icrc2-swap/mops.toml +++ b/motoko/icrc2-swap/mops.toml @@ -2,7 +2,7 @@ moc = "1.9.0" [dependencies] -core = "2.5.0" +core = "2.6.0" [moc] # M0236: use context dot notation From afd59840f04572b1ba7b8f8c8dbef822623a030f Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 4 Aug 2026 14:43:36 +0200 Subject: [PATCH 2/3] refactor(motoko/icrc2-swap): import the ICRC .did directly (moc 1.13.0 idl:), drop generated bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit moc 1.13.0 can import a Candid interface file directly, so the didc-generated `bindings/ICRC.mo` is no longer needed. Replace it with a native `idl:` import of the committed interface: import ICRC "idl:../candid/icrc.did"; This yields the named types and the service type `ICRC.Self` (snake_case Candid names → PascalCase automatically); the backend still supplies its own `actor() : ICRC.Self` per token, so the runtime-chosen-ledger pattern is unchanged. - mops.toml: moc 1.9.0 -> 1.13.0. - backend/app.mo: import `idl:../candid/icrc.did` instead of the generated module. - removed bindings/ICRC.mo (no committed bindings, no didc dependency). - candid/icrc.did unchanged (still the committed source of truth). - README updated: the interface is imported directly; nothing to regenerate. Verified end-to-end: `bash deploy.sh && bash test.sh` — all 7 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- motoko/icrc2-swap/README.md | 18 +++--- motoko/icrc2-swap/backend/app.mo | 7 ++- motoko/icrc2-swap/bindings/ICRC.mo | 99 ------------------------------ motoko/icrc2-swap/mops.toml | 2 +- 4 files changed, 16 insertions(+), 110 deletions(-) delete mode 100644 motoko/icrc2-swap/bindings/ICRC.mo diff --git a/motoko/icrc2-swap/README.md b/motoko/icrc2-swap/README.md index b42686bb82..25aa07ab3f 100644 --- a/motoko/icrc2-swap/README.md +++ b/motoko/icrc2-swap/README.md @@ -27,7 +27,13 @@ Three canisters: - **`token_a` / `token_b`**: Standard ICRC-1/ICRC-2 ledger canisters, pre-built from the DFINITY IC release. - **`backend`**: The swap canister (`backend/app.mo`). Accepts deposits, performs 1:1 swaps, and processes withdrawals. It discovers the token canister principals automatically at runtime via `PUBLIC_CANISTER_ID:token_a` / `PUBLIC_CANISTER_ID:token_b` environment variables injected by icp-cli. -The backend talks to the token ledgers through a single generated Motoko binding, `bindings/ICRC.mo`, produced from the ICRC-1/ICRC-2 interface in `candid/icrc.did`. Every token reference is typed against that one shared type — `actor() : ICRC.Self` — so the same binding serves both `token_a` and `token_b` (and would serve any ICRC-1/2 ledger the backend is pointed at, e.g. ckBTC or an SNS token). This is the "one interface, many ledgers, principal chosen at runtime" pattern: because the target is dynamic, the types come from the standard Candid interface rather than being bound to a single canister id. The binding is committed, so building the example needs no extra tooling. +The backend imports the ICRC-1/ICRC-2 interface **directly from the committed Candid file** `candid/icrc.did`, using Motoko's `idl:` import (moc 1.13.0+): + +```motoko +import ICRC "idl:../candid/icrc.did"; +``` + +This yields the interface's named types and its service type `ICRC.Self` — no bindings are generated or committed, and no extra tooling is needed. The `idl:` import provides *types only*, so the backend supplies its own actor reference per token: `actor() : ICRC.Self`. Every token reference is typed against that one shared type, so the same interface serves both `token_a` and `token_b` (and would serve any ICRC-1/2 ledger the backend is pointed at, e.g. ckBTC or an SNS token). This is the "one interface, many ledgers, principal chosen at runtime" pattern: because the target is dynamic, the types come from the standard Candid interface rather than being bound to a single canister id. ## Build and deploy from the command line @@ -62,15 +68,9 @@ icp network stop `bash test.sh` runs the full swap flow with `icrc2-alice` and `icrc2-bob` as the two parties. Test 2 verifies that swapping with no deposits returns `InsufficientBalance`. Tests 6 and 7 verify the actual token balance delta in the ledger after withdrawal, confirming the full round-trip. Tests are idempotent — they can be run multiple times without redeploying. -## Updating the token interface bindings - -`candid/icrc.did` is the ICRC-1/ICRC-2 interface the backend calls; `bindings/ICRC.mo` is the Motoko binding generated from it. The binding is committed, so no extra tooling is needed to build. If you change the interface, regenerate the binding with [`didc`](https://github.com/dfinity/candid/releases): - -```bash -didc bind -t mo candid/icrc.did > bindings/ICRC.mo -``` +## Updating the token interface -(The ICRC-1/2 type names are already PascalCase, so `didc` output is idiomatic as-is — no post-processing needed.) +`candid/icrc.did` is the ICRC-1/ICRC-2 interface the backend calls. It is imported directly via the `idl:` import (see [Architecture](#architecture)), so there is nothing to regenerate and no extra tooling to install — just edit `candid/icrc.did` and rebuild. moc reads it at build time and derives the Motoko types (snake_case Candid names become PascalCase automatically). ## Fee handling diff --git a/motoko/icrc2-swap/backend/app.mo b/motoko/icrc2-swap/backend/app.mo index 2befcf53b8..f6b2e17bb5 100644 --- a/motoko/icrc2-swap/backend/app.mo +++ b/motoko/icrc2-swap/backend/app.mo @@ -6,7 +6,12 @@ import Principal "mo:core/Principal"; import Result "mo:core/Result"; import Runtime "mo:core/Runtime"; -import ICRC "../bindings/ICRC"; +// Import the ICRC-1/ICRC-2 interface types directly from the committed Candid +// file (moc 1.13.0+ `idl:` import). This yields the named types and the service +// type `ICRC.Self`; no bindings are generated or committed. The backend brings +// its own actor reference per token — `actor() : ICRC.Self` — so the +// same types serve any ICRC-1/2 ledger the backend is pointed at. +import ICRC "idl:../candid/icrc.did"; // The swap canister accepts deposits of two ICRC-2 tokens, swaps balances // 1:1 between users, and allows withdrawals. diff --git a/motoko/icrc2-swap/bindings/ICRC.mo b/motoko/icrc2-swap/bindings/ICRC.mo deleted file mode 100644 index 9ff6431e35..0000000000 --- a/motoko/icrc2-swap/bindings/ICRC.mo +++ /dev/null @@ -1,99 +0,0 @@ -// This is a generated Motoko binding. -// Please use `import service "ic:canister_id"` instead to call canisters on the IC if possible. - -module { - public type Account = { owner : Principal; subaccount : ?Subaccount }; - public type Allowance = { allowance : Tokens; expires_at : ?Timestamp }; - public type AllowanceArgs = { account : Account; spender : Account }; - public type ApproveArgs = { - fee : ?Tokens; - memo : ?Blob; - from_subaccount : ?Subaccount; - created_at_time : ?Timestamp; - amount : Tokens; - expected_allowance : ?Tokens; - expires_at : ?Timestamp; - spender : Account; - }; - public type ApproveError = { - #GenericError : { message : Text; error_code : Nat }; - #TemporarilyUnavailable; - #Duplicate : { duplicate_of : BlockIndex }; - #BadFee : { expected_fee : Tokens }; - #AllowanceChanged : { current_allowance : Tokens }; - #CreatedInFuture : { ledger_time : Timestamp }; - #TooOld; - #Expired : { ledger_time : Timestamp }; - #InsufficientFunds : { balance : Tokens }; - }; - public type ApproveResult = { #Ok : BlockIndex; #Err : ApproveError }; - public type BlockIndex = Nat; - public type MetadataValue = { - #Int : Int; - #Nat : Nat; - #Blob : Blob; - #Text : Text; - }; - public type StandardRecord = { url : Text; name : Text }; - public type Subaccount = Blob; - public type Timestamp = Nat64; - public type Tokens = Nat; - public type TransferArg = { - to : Account; - fee : ?Tokens; - memo : ?Blob; - from_subaccount : ?Subaccount; - created_at_time : ?Timestamp; - amount : Tokens; - }; - public type TransferError = { - #GenericError : { message : Text; error_code : Nat }; - #TemporarilyUnavailable; - #BadBurn : { min_burn_amount : Tokens }; - #Duplicate : { duplicate_of : BlockIndex }; - #BadFee : { expected_fee : Tokens }; - #CreatedInFuture : { ledger_time : Timestamp }; - #TooOld; - #InsufficientFunds : { balance : Tokens }; - }; - public type TransferFromArgs = { - to : Account; - fee : ?Tokens; - spender_subaccount : ?Subaccount; - from : Account; - memo : ?Blob; - created_at_time : ?Timestamp; - amount : Tokens; - }; - public type TransferFromError = { - #GenericError : { message : Text; error_code : Nat }; - #TemporarilyUnavailable; - #InsufficientAllowance : { allowance : Tokens }; - #BadBurn : { min_burn_amount : Tokens }; - #Duplicate : { duplicate_of : BlockIndex }; - #BadFee : { expected_fee : Tokens }; - #CreatedInFuture : { ledger_time : Timestamp }; - #TooOld; - #InsufficientFunds : { balance : Tokens }; - }; - public type TransferFromResult = { - #Ok : BlockIndex; - #Err : TransferFromError; - }; - public type TransferResult = { #Ok : BlockIndex; #Err : TransferError }; - public type Self = actor { - icrc1_balance_of : shared query Account -> async Tokens; - icrc1_decimals : shared query () -> async Nat8; - icrc1_fee : shared query () -> async Tokens; - icrc1_metadata : shared query () -> async [(Text, MetadataValue)]; - icrc1_minting_account : shared query () -> async ?Account; - icrc1_name : shared query () -> async Text; - icrc1_supported_standards : shared query () -> async [StandardRecord]; - icrc1_symbol : shared query () -> async Text; - icrc1_total_supply : shared query () -> async Tokens; - icrc1_transfer : shared TransferArg -> async TransferResult; - icrc2_allowance : shared query AllowanceArgs -> async Allowance; - icrc2_approve : shared ApproveArgs -> async ApproveResult; - icrc2_transfer_from : shared TransferFromArgs -> async TransferFromResult; - } -} diff --git a/motoko/icrc2-swap/mops.toml b/motoko/icrc2-swap/mops.toml index 62f1f004b6..00581ef401 100644 --- a/motoko/icrc2-swap/mops.toml +++ b/motoko/icrc2-swap/mops.toml @@ -1,5 +1,5 @@ [toolchain] -moc = "1.9.0" +moc = "1.13.0" [dependencies] core = "2.6.0" From c2345f038b7eb362832f59e1c73325b9aa647059 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 4 Aug 2026 14:52:04 +0200 Subject: [PATCH 3/3] docs(motoko/icrc2-swap): fix stale didc/bindings note in candid/icrc.did header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The interface file's header still instructed generating Motoko bindings with `didc bind` — a leftover from before the switch to moc's `idl:` import. Update it to reflect that the .did is imported directly (no generated bindings, no didc). Addresses Copilot review feedback on #1454. Co-Authored-By: Claude Opus 4.8 (1M context) --- motoko/icrc2-swap/candid/icrc.did | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/motoko/icrc2-swap/candid/icrc.did b/motoko/icrc2-swap/candid/icrc.did index 7e80b7df58..728d11eeff 100644 --- a/motoko/icrc2-swap/candid/icrc.did +++ b/motoko/icrc2-swap/candid/icrc.did @@ -1,13 +1,14 @@ // ICRC-1 + ICRC-2 token-ledger interface. // // This is the *standard* interface every ICRC-1/ICRC-2 ledger exposes — not a -// specific ledger. The swap backend types its `actor()` -// references against it, so the same generated bindings work for any ICRC-1/2 -// ledger the backend is pointed at (token_a, token_b, ckBTC, an SNS token, ...). +// specific ledger. The swap backend imports it directly (moc `idl:` import, +// see backend/app.mo) and types its `actor()` references +// against it, so the same interface works for any ICRC-1/2 ledger the backend +// is pointed at (token_a, token_b, ckBTC, an SNS token, ...). // // The type definitions match the ICRC-1 ledger reference implementation -// (dfinity/ic, ledger-suite-icrc). Generate the Motoko bindings with: -// didc bind -t mo candid/icrc.did > bindings/ICRC.mo +// (dfinity/ic, ledger-suite-icrc). To update, edit this file directly — moc +// reads it at build time; there are no generated bindings to regenerate. type Subaccount = blob; type Timestamp = nat64;