Skip to content

refactor(motoko/icrc2-swap): generate ICRC bindings from a committed .did - #1454

Open
marc0olo wants to merge 1 commit into
masterfrom
motoko-icrc2-swap-generated-bindings
Open

refactor(motoko/icrc2-swap): generate ICRC bindings from a committed .did#1454
marc0olo wants to merge 1 commit into
masterfrom
motoko-icrc2-swap-generated-bindings

Conversation

@marc0olo

Copy link
Copy Markdown
Member

What

Replaces the hand-written backend/ICRC.mo type module in icrc2-swap with Motoko bindings generated by didc from a committed ICRC-1/ICRC-2 .did. This example is the deliberate counterpart to the single-fixed-target --actor-env-alias examples (ic-pos, evm_block_explorer, llm_chatbot): a backend that talks to many ledgers, with the ledger chosen at runtime.

Why this pattern for ICRC

icrc2-swap holds two ledgers at once (token_a, token_b) and picks the target per call (actor(args.token.toText()) : ICRC.Self). That's the "one interface, many ledgers, runtime-chosen principal" case — which --actor-env-alias structurally cannot express (it binds one import to one principal). The right tool is a shared generated service type + actor(id). This generalizes to any DeFi-shaped backend (swaps, routers, wallets) that must interact with many ICRC ledgers (ICP, ckBTC/ckETH, SNS tokens, …).

Contrast with ic-pos, which talks to a single configured ledger and also calls a ledger-specific method (get_transactions) — there --actor-env-alias with the full ledger .did is the right fit. The two examples now demonstrate both axes on purpose.

Changes

  • candid/icrc.did — the ICRC-1/ICRC-2 interface the backend calls, as a focused plain service. Type definitions are taken from the ic-icrc1-ledger reference wasm (the exact artifact this example pins for token_a/token_b); the service is the standard ICRC-1 + ICRC-2 method subset. This is the regeneration source.
  • bindings/ICRC.modidc bind -t mo candid/icrc.did. Committed, so building needs no extra tooling.
  • backend/app.mo — imports the generated bindings (ICRC.Self), keeps the runtime-principal pattern; hand-written ICRC.mo removed.
  • Unrelated tidy-ups in the same example: Map swap (return always discarded) → add (which is literally ignore swap), 6 sites; and mo:core 2.5.0 → 2.6.0.

Testing

bash deploy.sh && bash test.shall 7 tests pass (deposit via icrc2_transfer_from, atomic swap, withdraw via icrc1_transfer with correct fee-adjusted balance deltas), confirming the generated bindings are wire-compatible with the real ledger.

Discussion: binding generation & tooling (cc @Kamirus)

The bindings here are idiomatic straight from didc today because ICRC-1/2 Candid type names are already PascalCase. For interfaces whose Candid type names are snake_case (e.g. an LLM canister's chat_message_v1), that isn't the case — which is exactly what dfinity/candid#729 fixes (PascalCase Motoko type aliases). Flagging it because it's the generator that makes this pattern broadly clean.

Bigger picture: we commit bindings/ICRC.mo here because the only generator today is didc, a separate tool outside the icp-cli/mops/ic-wasm toolchain — gitignoring it would force a didc install on everyone who builds. The ideal end state would mirror the JS/TS story (@icp-sdk/bindgen generates src/bindings/*.ts at build time, gitignored): mops generating .did→Motoko bindings as a pre-build step, with the .did committed and the generated .mo gitignored (ideally with committing as an opt-in for teams that prefer it). mops already does the forward direction (mops generate candid), so this is the symmetric missing half — and #729 is the generator that would power it. Until then, committing the bindings is the right call.

(didc releases: https://github.com/dfinity/candid/releases)

🤖 Generated with Claude Code

….did

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(<token>) : 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) <noreply@anthropic.com>
@marc0olo
marc0olo requested a review from a team as a code owner July 28, 2026 19:36
@marc0olo
marc0olo requested a review from Copilot July 28, 2026 20:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the motoko/icrc2-swap example to use a committed, didc-generated Motoko binding for the standard ICRC-1/ICRC-2 ledger interface, allowing the backend to type ledgers selected by principal at runtime (actor(<principal>) : ICRC.Self) without maintaining a hand-written interface module.

Changes:

  • Adds a focused candid/icrc.did for the ICRC-1/ICRC-2 method subset used by the example and commits the generated Motoko binding in bindings/ICRC.mo.
  • Updates the backend to import and use ICRC.Self from the generated bindings, while preserving the runtime-selected-ledger pattern.
  • Bumps mo:core from 2.5.0 to 2.6.0 and applies minor map-update tidy-ups (swapadd).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
motoko/icrc2-swap/README.md Updates architecture docs and adds binding-regeneration instructions using didc.
motoko/icrc2-swap/mops.toml Bumps mo:core dependency version.
motoko/icrc2-swap/candid/icrc.did Introduces the committed Candid interface used as the binding source-of-truth.
motoko/icrc2-swap/bindings/ICRC.mo Adds committed, generated Motoko service/types binding (ICRC.Self, records, variants).
motoko/icrc2-swap/backend/app.mo Switches backend to the generated binding import and updates types/usages accordingly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Kamirus

Kamirus commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Yes, I think once dfinity/candid#729 lands it would make sense for mops to adopt this tool.
Mops would handle fetching it via mops toolchain and we could wrap it with mops generate ... command

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants