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
raymondk
pushed a commit
to dfinity/examples
that referenced
this pull request
Jul 9, 2026
* chore: migrate rust/stake_neuron_from_cli to icp-cli Converts the dfx-based CLI binary into an icp-cli canister example. Replaces ic-agent/tokio/clap with ic-cdk inter-canister calls, adds icp.yaml (rust@v3.3.0), Makefile with local compute_subaccount tests, CI workflow, and updated README preserving the educational NNS staking domain knowledge. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(rust/stake_neuron_from_cli): test.sh over Makefile, bump CI image to 1.0.1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(rust/stake_neuron_from_cli): enable local NNS testing, fix controller/subaccount bug - Add nns: true to icp.yaml so NNS canisters deploy at mainnet IDs locally - Fix bug in stake_neuron: subaccount was computed from msg_caller() but Governance was told controller = canister_self(); Governance recomputes the expected subaccount from the controller field, so these must match — switched both to canister_self() since the canister holds the ICP and owns the neuron - Replace hand-rolled ledger types (AccountIdentifier, TransferArgs, Tokens, Memo, crc32, build_account_identifier) with ic-ledger-types = "0.16"; the transfer() helper handles correct Candid serialization of AccountIdentifier as a blob - Update test.sh: fund backend with 2 ICP via icp token transfer, add Test 3 that calls stake_neuron and asserts a neuron ID is returned from local NNS Governance - Update edition to 2024 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(rust/stake_neuron_from_cli): update README and docstring for local NNS testing - Remove stale "mainnet required" callout — nns: true enables full local testing - Update code snippet to match actual ic-ledger-types-based implementation - Add Node.js v18+ and Rust to prerequisites - Fix deploy/test description to reflect that test.sh exercises stake_neuron locally - Fix mainnet canister call flag: --network ic → -e ic - Update stake_neuron docstring: remove "NNS not available locally" note, add nns: true requirement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(rust/stake_neuron_from_cli): add icp token transfer command for mainnet funding Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(rust/stake_neuron_from_cli): add minimum amount guard and authorization note - Reject amounts below 100_000_000 e8s early so no ICP is transferred before Governance rejects the claim and strands funds in the staking subaccount - Add code comment noting stake_neuron is unguarded; production deployments should restrict to an authorized principal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(rust/stake_neuron_from_cli): strengthen test.sh with known-value, guard, and cross-validation tests - Test 1: known-value assertion for compute_subaccount against a fixed reference principal (aaaaa-aa, nonce 0) — catches any regression in the SHA-256 domain separation without depending on a deployment-specific canister ID - Test 2: verify different nonces produce different subaccounts (replaces trivial determinism check) - Test 3: verify stake_neuron rejects amounts below 100_000_000 e8s with a clear error - Test 4: cross-validate that subaccount_hex in the stake_neuron result matches compute_subaccount(backend, nonce) — the key safety invariant Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(rust/stake_neuron_from_cli): explain claim_or_refresh re-run behavior in test.sh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(rust/stake_neuron_from_cli): clarify staked_amount_e8s is per-call, not total neuron balance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style(rust/stake_neuron_from_cli): pass &claim_request by reference to with_arg Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(rust/stake_neuron_from_cli): revert to CLI binary, update deps, apply 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> * docs(rust/stake_neuron_from_cli): clarify Rust vs icp-cli roles in README - State upfront that ic-agent/Rust is the primary artifact; icp-cli is only used for local network management and not required on mainnet - Explain why Node.js is needed (icp-cli installs via npm) and link to alternative installation methods - Add --compute-only usage example to the subaccount section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(rust/stake_neuron_from_cli): address Daniel's follow-up review - Sort Cargo.toml dependencies alphabetically - Guard fetch_root_key behind is_local URL check: never call it against mainnet (a rogue node could return a fake key; ic-agent has the real mainnet key compiled in) - Add comment on created_at_time pointing to deduplication docs - Add governance.did reference in the Candid types section - Move identity funding before Test 3 so the test sequence is funded throughout; add comment explaining why 2 ICP (fee on top of 1 ICP stake) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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.