feat: add ICRC-2 token approve and token allowance commands#637
Open
marc0olo wants to merge 5 commits into
Open
feat: add ICRC-2 token approve and token allowance commands#637marc0olo wants to merge 5 commits into
token approve and token allowance commands#637marc0olo wants to merge 5 commits into
Conversation
Add first-class support for ICRC-2 allowances so developers can authorize a spender (e.g. a canister) to transfer tokens on their behalf, and inspect existing allowances, without hand-writing raw `canister call` payloads. - `icp token [TOKEN|LEDGER_ID] approve <AMOUNT> <SPENDER>` sets a spender's allowance (overwrite, not increment) via `icrc2_approve`. Supports `--from-subaccount` (the debited account) and `--spender-subaccount`. - `icp token [TOKEN|LEDGER_ID] allowance <SPENDER>` reads an allowance via `icrc2_allowance`. Supports `--subaccount`, `--spender-subaccount`, and `--of-principal` to inspect any owner/spender pair, including accounts you do not control. Both work with any ICRC-2 ledger by known name or canister id, mirror the existing `balance`/`transfer` conventions (subaccounts as hex, `--json`, `-q/--quiet`, operations-layer split, per-action Snafu errors), and are covered by integration tests exercising subaccounts, third-party reads, output modes, and overwrite semantics.
Add an optional `--expires-in <DURATION>` flag to `icp token approve` that sets an ICRC-2 allowance expiry, using the CLI's existing relative-duration format (`24h`, `30d`; suffixes s/m/h/d/w, bare number is seconds). The relative duration is resolved to an absolute nanosecond timestamp at call time. Approval and allowance expiries are now rendered as RFC 3339 timestamps in human output and as raw nanoseconds in `--json`. Also document the new `approve`/`allowance` commands (including expiry and subaccounts) in the "Tokens and Cycles" guide, and cover expiry with an integration test.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds first-class ICRC-2 allowance support to the icp token command group by introducing new approve and allowance subcommands, plus documentation and integration tests to validate expected behavior across subaccounts, expiry, quiet/json modes, and overwrite semantics.
Changes:
- Add
icp token approve(ICRC-2icrc2_approve) andicp token allowance(ICRC-2icrc2_allowance) command implementations and dispatch wiring. - Add/extend integration test utilities and new token tests covering approvals, allowance queries, subaccounts, overwrite semantics, and expiry.
- Update docs (guide + generated CLI reference) and changelog for the new commands.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/reference/cli.md | Regenerated CLI reference content to include token approve / token allowance help. |
| docs/guides/tokens-and-cycles.md | Adds an allowances section explaining ICRC-2 approvals, expiry, and subaccount flags. |
| crates/icp-cli/tests/token_tests.rs | Adds integration tests for approve/allowance flows including expiry, JSON/quiet, and subaccounts. |
| crates/icp-cli/tests/common/clients/ledger.rs | Adds a ledger test client helper to query icrc2_allowance. |
| crates/icp-cli/src/operations/token/mod.rs | Exposes new approve and allowance operation modules. |
| crates/icp-cli/src/operations/token/approve.rs | Implements the ICRC-2 icrc2_approve operation with decimals/symbol queries and amount scaling. |
| crates/icp-cli/src/operations/token/allowance.rs | Implements the ICRC-2 icrc2_allowance query operation and formatting. |
| crates/icp-cli/src/main.rs | Wires new token subcommands into the main dispatcher. |
| crates/icp-cli/src/commands/token/mod.rs | Registers new subcommands and adds shared expiry formatting helper. |
| crates/icp-cli/src/commands/token/approve.rs | Adds CLI surface for approvals, including --expires-in, subaccounts, json/quiet output. |
| crates/icp-cli/src/commands/token/allowance.rs | Adds CLI surface for allowance queries, including --of-principal, subaccounts, json/quiet output. |
| CHANGELOG.md | Documents the two new additive token subcommands and their flags. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…se errors
- approve: convert the decimal amount via `icp::parsers::to_token_unit_amount`
instead of `10^decimals` multiplication. This rejects amounts that aren't
exactly representable at the token's precision (rather than silently
truncating) and uses arbitrary-precision math to avoid overflow for large
`decimals`. Covered by a new test asserting an over-precise amount is refused.
- approve/allowance: include `{source}` in the `ParseCanisterId` error display,
matching `operations/token/balance.rs` for better diagnostics.
marc0olo
added a commit
to dfinity/examples
that referenced
this pull request
Jul 3, 2026
…pply review feedback The original example was a CLI binary using ic-agent — converting it to an icp-cli canister changed its fundamental purpose. This reverts to the original CLI approach while updating everything to current standards: - Revert from canister to CLI binary (src/main.rs + src/lib.rs, no backend/) - icp.yaml: network-only config with nns: true; no canister recipe - Upgrade ic-agent 0.35 → 0.47 (add pem + ring features for secp256k1 support — icp-cli generates secp256k1 keys by default) - Upgrade ic-ledger-types 0.15 → 0.16; drop hex and crc32fast (covered by ic-ledger-types) - edition = "2024" - Add --compute-only flag: prints the staking subaccount without any IC call or transfer — lets users verify the destination before committing funds - Rename amount → amount_e8s throughout; .unwrap() for constant principal - Clarify memo: the ledger transfer memo is cosmetic and does not need to match the governance nonce; what matters is that the nonce used to derive the staking subaccount matches the memo in claim_or_refresh_neuron_from_account - Document that anyone can complete step 2 (neuron controller is determined by the subaccount derived in step 1) - README: remove "domain-separated" jargon, list the 4 SHA-256 inputs plainly; add nns.internetcomputer.org link; note ICRC-2 as a safer alternative with link to dfinity/icp-cli#637 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| from_subaccount, | ||
| spender, | ||
| amount: ledger_amount.clone(), | ||
| expected_allowance: None, |
Member
Author
There was a problem hiding this comment.
@raymondk assuming we want to add this feature, shall we also add this flag? I guess it makes sense to provide the option
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds first-class ICRC-2 allowance support to the
tokencommand group, so developers can authorize a spender (typically a canister) to transfer tokens on their behalf and inspect existing allowances — without hand-writing rawicp canister callpayloads and escaped subaccount blobs.Previously the only way to approve a spender was:
Now:
Two new subcommands, both purely additive (no changes to existing command behavior):
icp token [TOKEN|LEDGER_ID] approve <AMOUNT> <SPENDER>→icrc2_approveicp token [TOKEN|LEDGER_ID] allowance <SPENDER>→icrc2_allowanceBoth work against any ICRC-2 ledger, referenced either by a known token name (currently
icp) or by a ledger canister id. Amounts are entered/displayed in whole tokens (auto-scaled by each ledger'sicrc1_decimals) and shown with the ledger'sicrc1_symbol.icp token approveUsage:
icp token [TOKEN|LEDGER_ID] approve [OPTIONS] <AMOUNT> <SPENDER>Sets the spender's allowance to
<AMOUNT>, overwriting any existing allowance (this is a set, not an increment). The allowance is granted from the calling identity's account, which is charged the ledger's approval fee. You can only approve from funds you control, so there is intentionally no--of-principalhere.<AMOUNT>1.5). Supportsk/m/b/tsuffixes.<SPENDER>--from-subaccount <HEX>1→00…01). Defaults to the default subaccount.--spender-subaccount <HEX>--expires-in <DURATION>24h,30d; suffixess/m/h/d/w, bare number is seconds). Resolved to an absolute timestamp at call time. Never expires if omitted.--json{ "block_index": "...", "expires_at": <ns|null> }.-q, --quietPlus the standard
--network/--environment/--identityselection flags.Examples
On expiry (
--expires-in)ICRC-2 stores expiry as an absolute nanosecond timestamp. Rather than make users compute a future UTC instant, the CLI takes a relative duration and resolves it to that timestamp at call time — reusing the same duration format already used by
settings session-lengthandidentity delegation(least surprise, no timezone math). The resolved expiry is shown as an RFC 3339 timestamp in human output and as raw nanoseconds in--json. A short expiry limits exposure if a spender is ever compromised.icp token allowanceUsage:
icp token [TOKEN|LEDGER_ID] allowance [OPTIONS] <SPENDER>Read-only query of the allowance an owner account has granted to a spender. Because it is a public query, it works for any owner/spender pair, including accounts you do not control — use
--of-principalto set the owner. Prints the amount in whole tokens, plus an RFC 3339 expiry line if one was set on the allowance.<SPENDER>--of-principal <PRINCIPAL>--subaccount <HEX>--spender-subaccount <HEX>--json{ "allowance": "...", "expires_at": <ns|null> }.-q, --quietExamples — every combination
Design notes
--expires-in <DURATION>(not an absolute--expires-at), chosen for ergonomics and consistency with the CLI's existing duration convention; the flag name matches the "from now" semantics. Absolute-timestamp input can be added later without breaking this.--subaccount 1→00…01), so no hand-escaped Candid blobs.approve --from-subaccountmirrorstransfer(an action from your account),allowance --subaccountmirrorsbalance(a query about an account).Principal+--spender-subaccountrather than aFlexibleAccountId, since ICP-ledger hex account-ids are meaningless for an ICRC-2 spender.balance/transfer: thin command layer overoperations/token/{approve,allowance}.rs, shared token-name/canister-id resolution, concurrenticrc1_decimals/icrc1_symbolqueries, and one Snafu error variant per erroring action.Documentation
docs/reference/cli.mdvia./scripts/generate-cli-docs.sh.Testing
cargo test -p icp-cli --test token_tests(all 7 pass). Integration tests cover:--from-subaccount+--spender-subaccounttogether, with a negative check that default accounts stay at 0--of-principal(a third party reading an allowance they don't own)--jsonand-q/--quietoutput modes--expires-in: expiry recorded ~24h ahead (bounded by the wall-clock window around the call), surfaced in human output and--json, and absent when omittedcargo fmtandcargo clippyare clean.