Skip to content

feat(ckbtc-minter): support ICRC-21 consent messages#10093

Merged
Dfinity-Bjoern merged 19 commits intomasterfrom
bjoern/ckbtc-minter-icrc-21
May 6, 2026
Merged

feat(ckbtc-minter): support ICRC-21 consent messages#10093
Dfinity-Bjoern merged 19 commits intomasterfrom
bjoern/ckbtc-minter-icrc-21

Conversation

@Dfinity-Bjoern
Copy link
Copy Markdown
Contributor

@Dfinity-Bjoern Dfinity-Bjoern commented May 5, 2026

Summary

Implements the ICRC-21 canister call consent message standard on the ckBTC minter so that ICRC-21-aware wallets can display a human-readable description of retrieve_btc_with_approval calls before the user signs.

Both display variants from the standard are supported:

  • GenericDisplay — a Markdown message intended for software wallets with a full screen.
  • FieldsDisplay — a list of (label, Value) fields tailored for hardware wallets (e.g. Ledger Nano S+).

Also adds an icrc10_supported_standards query that advertises ICRC-10 and ICRC-21.

Implementation

  • New module rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs holds the consent-message construction and icrc10_supported_standards.
  • main.rs exposes the two new endpoints (icrc21_canister_call_consent_message as update, icrc10_supported_standards as query).
  • ckbtc_minter.did declares the ICRC-10 / ICRC-21 types and methods.
  • The handler enforces the standard's 500-byte argument-size limit and returns Icrc21Error::UnsupportedCanisterCall for unknown methods or undecodable args.

Test plan

  • 13 new unit tests in updates::icrc21::tests covering: argument-size limit, unsupported method, decode failures, GenericDisplay output for retrieve_btc_with_approval (with and without subaccount) and retrieve_btc, FieldsDisplay output for short and long addresses, subaccount chunking, label suffixing, chunk-text edge cases (empty / exact boundary / multi-byte UTF-8), and round-trip reassembly of chunked values.
  • cargo clippy -p ic-ckbtc-minter --lib --bins clean with workspace lint set.
  • cargo test -p ic-ckbtc-minter --bin ic-ckbtc-minter check_candid_interface_compatibility passes (verifies the .did matches the implementation).
  • Try the FieldsDisplay output on an actual Ledger Nano S+ to verify chunked labels and values fit on screen as expected.

🤖 Generated with Claude Code

Implement the ICRC-21 canister call consent message standard on the
ckBTC minter so that ICRC-21-aware wallets can show the user a human-
readable description of `retrieve_btc_with_approval` (and `retrieve_btc`)
calls before signing.

Both display variants are supported:
- GenericDisplay: a Markdown message for software wallets.
- FieldsDisplay: a list of (label, Value) fields tailored for hardware
  wallets like the Ledger Nano S+. Long Text values (Bitcoin addresses,
  hex subaccounts) are split into chunks of at most 18 characters with a
  "(N/M)" pagination suffix, and labels are kept short ("BTC address",
  "From subaccount", "Amount") so they fit on a single hardware-wallet
  line.

Also adds an `icrc10_supported_standards` query that advertises
ICRC-10 and ICRC-21.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds ICRC-21 “canister call consent message” support to the ckBTC minter so ICRC-21-aware wallets can display a human-readable preview for retrieve_btc_with_approval and retrieve_btc before signing, and advertises support via an ICRC-10 supported-standards endpoint.

Changes:

  • Introduces consent-message construction for retrieve_btc_with_approval and retrieve_btc with both GenericDisplay (Markdown) and FieldsDisplay (chunked text fields).
  • Exposes new canister endpoints icrc21_canister_call_consent_message (update) and icrc10_supported_standards (query).
  • Extends the public Candid interface (ckbtc_minter.did) with the ICRC-10/ICRC-21 types and methods.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Implements ICRC-21 request handling, consent message formatting, chunking helper, and unit tests.
rs/bitcoin/ckbtc/minter/src/updates.rs Exposes the new icrc21 updates module.
rs/bitcoin/ckbtc/minter/src/main.rs Wires the new ICRC-21 (update) and ICRC-10 (query) endpoints into the canister.
rs/bitcoin/ckbtc/minter/ckbtc_minter.did Declares ICRC-10 / ICRC-21 types and the two new service methods.

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

Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Outdated
Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Outdated
Address Copilot review feedback on PR #10093:

1. Token symbol was hard-coded to "ckBTC" / "BTC" but the minter is also
   deployed against testnet/regtest where the same code drives the
   ckTESTBTC / TESTBTC tokens (see dashboard.rs and update_balance.rs).
   Introduce a TokenSymbols struct that picks the right symbols based on
   the configured Network and thread it through both the GenericDisplay
   markdown and FieldsDisplay fields so consent messages stay accurate
   on test deployments.

2. Fix a stale doc comment that claimed push_chunked_text iterates via
   `char_indices` — it iterates `chars()` (the comment intent stands:
   we use Unicode-scalar iteration to keep the helper safe for any
   non-ASCII caller).

Add unit tests for the testnet/regtest symbol path on both display
variants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.


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

Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Outdated
Dfinity-Bjoern and others added 2 commits May 5, 2026 14:56
Address Copilot review feedback on PR #10093: replace the local 500-byte
copy of the ICRC-21 argument-size limit with the shared constant
icrc_ledger_types::icrc21::lib::MAX_CONSENT_MESSAGE_ARG_SIZE_BYTES so
the two values cannot drift in the future. The shared constant is a
u16, cast to usize at the comparison and test sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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


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

@Dfinity-Bjoern Dfinity-Bjoern marked this pull request as ready for review May 5, 2026 14:14
@Dfinity-Bjoern Dfinity-Bjoern requested a review from a team as a code owner May 5, 2026 14:14
@github-actions github-actions Bot added the @defi label May 5, 2026
Copy link
Copy Markdown
Contributor

@mbjorkqvist mbjorkqvist left a comment

Choose a reason for hiding this comment

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

Thanks @Dfinity-Bjoern, just a few minor comments!

Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Outdated
Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Outdated
Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Outdated
Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Outdated
Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Outdated
Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Outdated
Dfinity-Bjoern and others added 5 commits May 5, 2026 18:01
Co-authored-by: Mathias Björkqvist <mathias.bjorkqvist@dfinity.org>
Co-authored-by: Mathias Björkqvist <mathias.bjorkqvist@dfinity.org>
Per the ICRC-21 spec, hardware wallets are responsible for paginating
long Value::Text fields across device screens. Verified by inspecting
the Ledger ICP app, which reads the raw Text content and calls
handle_ui_message(text, message, page) to slice it into device-sized
pages with the original label preserved across screens.

Pre-chunking on the canister side was actively harmful: it inflated
field counts (one logical address turned into 3 separate fields the
user had to step through with confusing "(1/3)" / "(2/3)" / "(3/3)"
labels) and prevented the device from paginating cleanly. Drop the
push_chunked_text / chunk_text helpers and the
FIELDS_DISPLAY_TEXT_CHUNK_LEN constant, and emit each long value as a
single Value::Text. Update the FieldsDisplay tests accordingly.

Short labels ("Amount", "BTC address", "From subaccount", intent
"ckBTC to BTC") are kept since titles in the Ledger app are truncated
rather than paginated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Mathias's review feedback on PR #10093 (Markdown injection nit):
the destination address from RetrieveBtc(WithApproval)Args was being
interpolated directly into the GenericDisplay Markdown. A crafted value
containing newlines, backticks, or '#' could fake additional fields in
the consent message (e.g. an "address" of
"bc1q...\n# You will receive 100 BTC") and confuse the user. The
malicious request would later fail when retrieve_btc[_with_approval] is
invoked because the actual call also parses the address, but a bad
consent message is itself the issue.

Add a validate_address step that parses the address with the same
BitcoinAddress::parse used by retrieve_btc[_with_approval]. If parsing
fails, return Icrc21Error::UnsupportedCanisterCall before constructing
any consent message. This both eliminates the injection vector and
guarantees the address shown to the user is actually parseable, so the
consent endpoint and the actual call agree on what's accepted.

Replace fake test addresses ("tb1qexampleaddress" etc.) with valid
network-matching ones, and add test_malformed_address_is_rejected with
a few crafted Markdown-injection payloads as well as an address from
the wrong network.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two leftover bits from the FieldsDisplay chunking add/remove cycle that
no longer earn their keep:

- format_subaccount was a one-line wrapper around hex::encode used in a
  single place. Inline it and drop the now-unused Subaccount import.
- The GenericDisplay markdown was being built up with five separate
  push_str(&format!(...)) calls per builder, each allocating a throwaway
  String. Collapse into a single format! per builder; the optional
  subaccount block on the with-approval variant stays as a conditional
  push_str. Same output, fewer allocations, easier to read the whole
  template at a glance.

No behavior change; all 14 unit tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.


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

Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs
Dfinity-Bjoern and others added 3 commits May 5, 2026 21:27
The mainnet_events.mem.gz genrule that ckbtc_minter_canbench depends on
declared its macOS-only constraint as @platforms//cpu:arm, which is the
32-bit ARM (aarch32) constraint — Apple Silicon machines are aarch64
and therefore failed the constraint check, making the _update target
incompatible on every Mac. Switch to @platforms//cpu:aarch64 so the
target is buildable on Apple Silicon while keeping macOS x86_64
excluded as the surrounding comment intended.

Also regenerate canbench/results.yml with the updated instruction
counts produced by the new minter Wasm (the ICRC-21 endpoints
introduced in this branch shift the layout enough to trip the
canbench_test noise threshold).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ICRC-21 spec is being moved from the wg-identity-authentication
repo into the canonical dfinity/ICRC repo (one ICRC per directory under
ICRCs/). Update the URL the minter advertises via
icrc10_supported_standards, plus the surrounding doc comments in the
.did file and the icrc21 Rust module, to point at
https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-21/ICRC-21.md.

This change goes in only after the corresponding ICRC-repo PR is
merged and the new path resolves, so wallets that follow the URL
always reach a live spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@gregorydemay gregorydemay left a comment

Choose a reason for hiding this comment

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

Thanks for that PR @Dfinity-Bjoern ! The main comment is regarding not supporting ICRC-21 for retrieve_btc. Otherwise, the rest is minor

Comment thread rs/bitcoin/ckbtc/minter/src/main.rs
Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Outdated
Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs
Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs Outdated
Bjoern Tackmann and others added 3 commits May 6, 2026 09:20
The non-approval `retrieve_btc` flow requires the user to first deposit
ckBTC into a minter-derived withdrawal subaccount, which wallets cannot
discover or sign for via standard ICRC-21 — the wallet would render a
consent message for a call that, on its own, never moves the user's
tokens. Drop ICRC-21 support for that method so the minter only emits
a consent message for `retrieve_btc_with_approval`, which is the flow
ICRC-21-aware wallets actually drive.

Also tighten test_unsupported_method to assert that retrieve_btc (and
a few other unrelated methods) explicitly return UnsupportedCanisterCall
so future re-additions to the supported set are deliberate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Group all updates-module tests in the same file (updates/tests.rs) the
way update_balance is already organized: keep production code in
updates/icrc21.rs and put the tests in a sibling `mod icrc21 {...}`
block in updates/tests.rs.

Bumps the visibility of the items the tests reach into — DECIMALS,
TokenSymbols, TokenSymbols::for_network, format_amount, and
build_consent_info — to pub(super), which is the minimum needed for
the sibling `crate::updates::tests::icrc21` module to use them while
keeping them out of the crate-public API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address review feedback on PR #10093: add a state-machine-driven
integration test covering the new endpoints end-to-end.

The test installs a mainnet minter via the existing CkBtcSetup helper
and asserts:

* `icrc10_supported_standards` advertises both ICRC-10 and ICRC-21,
  with the ICRC-21 entry pointing at the canonical
  `dfinity/ICRC/blob/main/ICRCs/ICRC-21/ICRC-21.md` URL.
* `icrc21_canister_call_consent_message` for `retrieve_btc_with_approval`
  returns a GenericDisplay markdown message with the expected
  `# Convert ckBTC to BTC` header, amount and address; and a
  FieldsDisplay structured message with the expected intent and
  (Amount, BTC address) fields.
* Unsupported methods — including `retrieve_btc`, which the minter
  intentionally does *not* render consent for — return
  `Icrc21Error::UnsupportedCanisterCall`.

The test exercises the full Candid path (encode the request, send via
ingress, decode the response), which the unit tests in
updates::tests::icrc21 do not.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs
Comment thread rs/bitcoin/ckbtc/minter/src/updates/icrc21.rs
Comment thread rs/bitcoin/ckbtc/minter/tests/tests.rs Outdated
Copy link
Copy Markdown
Contributor

@gregorydemay gregorydemay left a comment

Choose a reason for hiding this comment

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

A couple of nits but ottherwise LGTM!

Bjoern Tackmann and others added 2 commits May 6, 2026 11:34
Replace the loose `starts_with` + `contains` checks in
`test_icrc21_endpoints_smoke` with a full `assert_eq!` on the entire
GenericDisplay markdown produced for `retrieve_btc_with_approval`.
Any future wording tweak now surfaces as a test diff that has to be
updated consciously instead of slipping past a substring match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Dfinity-Bjoern Dfinity-Bjoern added this pull request to the merge queue May 6, 2026
Merged via the queue into master with commit ce5f73f May 6, 2026
37 checks passed
@Dfinity-Bjoern Dfinity-Bjoern deleted the bjoern/ckbtc-minter-icrc-21 branch May 6, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants