Skip to content

feat: add ICRC-2 token approve and token allowance commands#637

Open
marc0olo wants to merge 5 commits into
mainfrom
feat/token-approve-allowance
Open

feat: add ICRC-2 token approve and token allowance commands#637
marc0olo wants to merge 5 commits into
mainfrom
feat/token-approve-allowance

Conversation

@marc0olo

@marc0olo marc0olo commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

Adds first-class ICRC-2 allowance support to the token command group, so developers can authorize a spender (typically a canister) to transfer tokens on their behalf and inspect existing allowances — without hand-writing raw icp canister call payloads and escaped subaccount blobs.

Previously the only way to approve a spender was:

icp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc2_approve '(record {
  from_subaccount = opt blob "\00\00...\01";
  spender = record { owner = principal "<SPENDER>" };
  amount = 500_000_000 : nat;
})'

Now:

icp token approve 5 <SPENDER> --from-subaccount 1 --expires-in 24h

Two new subcommands, both purely additive (no changes to existing command behavior):

  • icp token [TOKEN|LEDGER_ID] approve <AMOUNT> <SPENDER>icrc2_approve
  • icp token [TOKEN|LEDGER_ID] allowance <SPENDER>icrc2_allowance

Both 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's icrc1_decimals) and shown with the ledger's icrc1_symbol.


icp token approve

Usage: 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-principal here.

Argument / flag Meaning
<AMOUNT> Allowance in whole tokens (e.g. 1.5). Supports k/m/b/t suffixes.
<SPENDER> Principal of the spender being authorized.
--from-subaccount <HEX> The caller's subaccount to grant from / debit for the fee. 32-byte hex, left-padded (e.g. 100…01). Defaults to the default subaccount.
--spender-subaccount <HEX> The spender's subaccount. Same hex format.
--expires-in <DURATION> Optional auto-expiry, relative to now (e.g. 24h, 30d; suffixes s/m/h/d/w, bare number is seconds). Resolved to an absolute timestamp at call time. Never expires if omitted.
--json Emit { "block_index": "...", "expires_at": <ns|null> }.
-q, --quiet Print only the block index.

Plus the standard --network / --environment / --identity selection flags.

Examples

# Approve a spender for 5 ICP from your default account
icp token approve 5 <SPENDER>

# Approve from your subaccount 1
icp token approve 5 <SPENDER> --from-subaccount 1

# Approve a specific subaccount of the spender
icp token approve 5 <SPENDER> --spender-subaccount 2

# Auto-expire the allowance after 24 hours (security best practice)
icp token approve 5 <SPENDER> --expires-in 24h

# Revoke by approving 0
icp token approve 0 <SPENDER>

# Any ICRC-2 ledger, by canister id
icp token mxzaz-hqaaa-aaaar-qaada-cai approve 0.01 <SPENDER>

# Scripting: capture just the block index
BLOCK=$(icp token approve 5 <SPENDER> --quiet)

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-length and identity 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 allowance

Usage: 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-principal to set the owner. Prints the amount in whole tokens, plus an RFC 3339 expiry line if one was set on the allowance.

Argument / flag Meaning
<SPENDER> Principal of the spender whose allowance to look up.
--of-principal <PRINCIPAL> The allowance owner to inspect, instead of the current identity.
--subaccount <HEX> The owner's subaccount that granted the allowance. 32-byte hex, left-padded.
--spender-subaccount <HEX> The spender's subaccount. Same hex format.
--json Emit { "allowance": "...", "expires_at": <ns|null> }.
-q, --quiet Print only the allowance amount.

Examples — every combination

# 1. Allowance YOU granted to a spender (default subaccounts)
icp token allowance <SPENDER>

# 2. Allowance you granted from one of YOUR subaccounts
icp token allowance <SPENDER> --subaccount 1

# 3. Allowance you granted to a spender's subaccount
icp token allowance <SPENDER> --spender-subaccount 2

# 4. Allowance SOMEONE ELSE granted (an account you don't own)
icp token allowance <SPENDER> --of-principal <OWNER>

# 5. Fully explicit: owner P (subaccount 1) -> spender S (subaccount 2)
icp token allowance <SPENDER> --of-principal <OWNER> --subaccount 1 --spender-subaccount 2

# 6. Non-ICP ledger, as JSON
icp token <LEDGER_ID> allowance <SPENDER> --of-principal <OWNER> --json

Design notes

  • Expiry format is a relative --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 format follows the rest of the CLI: 32-byte hex, left-padded (--subaccount 100…01), so no hand-escaped Candid blobs.
  • Flag naming matches each command's closest existing analog: approve --from-subaccount mirrors transfer (an action from your account), allowance --subaccount mirrors balance (a query about an account).
  • Spender is a bare Principal + --spender-subaccount rather than a FlexibleAccountId, since ICP-ledger hex account-ids are meaningless for an ICRC-2 spender.
  • Implementation mirrors balance/transfer: thin command layer over operations/token/{approve,allowance}.rs, shared token-name/canister-id resolution, concurrent icrc1_decimals/icrc1_symbol queries, and one Snafu error variant per erroring action.

Documentation

  • Added a "Approving Token Spending (Allowances)" section to the Tokens and Cycles guide, placed after the ICRC-1 section: concept, granting, expiry, checking, and a subaccount-flag reference table.
  • Regenerated docs/reference/cli.md via ./scripts/generate-cli-docs.sh.
  • CHANGELOG updated.

Testing

cargo test -p icp-cli --test token_tests (all 7 pass). Integration tests cover:

  • approve + allowance happy path and the zero/no-allowance case
  • --from-subaccount + --spender-subaccount together, with a negative check that default accounts stay at 0
  • --of-principal (a third party reading an allowance they don't own)
  • --json and -q/--quiet output modes
  • overwrite-not-increment semantics (re-approving replaces the amount)
  • --expires-in: expiry recorded ~24h ahead (bounded by the wall-clock window around the call), surfaced in human output and --json, and absent when omitted

cargo fmt and cargo clippy are clean.

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.
@marc0olo marc0olo requested a review from a team as a code owner July 3, 2026 09:10
marc0olo added 2 commits July 3, 2026 11:38
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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-2 icrc2_approve) and icp token allowance (ICRC-2 icrc2_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.

Comment thread crates/icp-cli/src/operations/token/approve.rs Outdated
Comment thread crates/icp-cli/src/operations/token/approve.rs Outdated
Comment thread crates/icp-cli/src/operations/token/allowance.rs Outdated
…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>
@marc0olo marc0olo requested a review from Copilot July 3, 2026 10:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 12 out of 12 changed files in this pull request and generated 1 comment.

from_subaccount,
spender,
amount: ledger_amount.clone(),
expected_allowance: None,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@raymondk assuming we want to add this feature, shall we also add this flag? I guess it makes sense to provide the option

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.

2 participants