From b6549edd58a69e3338a54d2ff3bab6088599ac6f Mon Sep 17 00:00:00 2001 From: Adam Spofford Date: Mon, 27 Apr 2026 15:22:06 -0700 Subject: [PATCH 1/3] Add `--of-principal` to `icp token/cycles balance` --- CHANGELOG.md | 1 + crates/icp-cli/src/commands/cycles/balance.rs | 9 ++++++++- crates/icp-cli/src/commands/token/balance.rs | 7 ++++++- crates/icp-cli/src/operations/token/balance.rs | 13 +++++++++---- crates/icp-cli/tests/cycles_tests.rs | 10 ++++++++-- docs/reference/cli.md | 2 ++ 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3681babb..cb1170168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +* feat: `icp token/cycles balance` now accept `--of-principal` * fix: `icp canister call` now serializes arguments built via the interactive Candid assist prompt against the method's declared signature, matching the behavior of arguments passed on the command line. Previously, narrower values (e.g. a variant case from a multi-case variant) were encoded with a type table inferred only from the value, which the target canister rejected with errors like "Variant index N larger than length 1". # v0.2.5 diff --git a/crates/icp-cli/src/commands/cycles/balance.rs b/crates/icp-cli/src/commands/cycles/balance.rs index ab7a1abde..7f7ba81ab 100644 --- a/crates/icp-cli/src/commands/cycles/balance.rs +++ b/crates/icp-cli/src/commands/cycles/balance.rs @@ -1,6 +1,7 @@ use std::io::stdout; use bigdecimal::BigDecimal; +use candid::Principal; use clap::Args; use icp::context::Context; use icp_canister_interfaces::cycles_ledger::CYCLES_LEDGER_PRINCIPAL; @@ -21,6 +22,10 @@ pub(crate) struct BalanceArgs { #[arg(long, value_parser = parse_subaccount)] pub(crate) subaccount: Option<[u8; 32]>, + /// Check the balance of this principal instead of the current identity + #[arg(long)] + pub(crate) of_principal: Option, + /// Output command results as JSON #[arg(long, conflicts_with = "quiet")] pub(crate) json: bool, @@ -43,7 +48,9 @@ pub(crate) async fn exec(ctx: &Context, args: &BalanceArgs) -> Result<(), anyhow .await?; // Get the balance from the ledger - let cycles = get_raw_balance(&agent, CYCLES_LEDGER_PRINCIPAL, args.subaccount).await?; + let cycles = + get_raw_balance(&agent, CYCLES_LEDGER_PRINCIPAL, args.subaccount, args.of_principal) + .await?; let cycles_amount = TokenAmount { amount: BigDecimal::from_biguint(cycles.0, 0), symbol: "cycles".to_string(), diff --git a/crates/icp-cli/src/commands/token/balance.rs b/crates/icp-cli/src/commands/token/balance.rs index 5651a3a78..310e25af4 100644 --- a/crates/icp-cli/src/commands/token/balance.rs +++ b/crates/icp-cli/src/commands/token/balance.rs @@ -1,5 +1,6 @@ use std::io::stdout; +use candid::Principal; use clap::Args; use icp::context::Context; use serde::Serialize; @@ -19,6 +20,10 @@ pub(crate) struct BalanceArgs { #[arg(long, value_parser = parse_subaccount)] pub(crate) subaccount: Option<[u8; 32]>, + /// Check the balance of this principal instead of the current identity + #[arg(long)] + pub(crate) of_principal: Option, + /// Output command results as JSON #[arg(long, conflicts_with = "quiet")] pub(crate) json: bool, @@ -50,7 +55,7 @@ pub(crate) async fn exec( .await?; // Get the balance from the ledger - let balance = get_balance(&agent, args.subaccount, token).await?; + let balance = get_balance(&agent, args.subaccount, token, args.of_principal).await?; if args.json { serde_json::to_writer( diff --git a/crates/icp-cli/src/operations/token/balance.rs b/crates/icp-cli/src/operations/token/balance.rs index efc21566b..40183a731 100644 --- a/crates/icp-cli/src/operations/token/balance.rs +++ b/crates/icp-cli/src/operations/token/balance.rs @@ -58,6 +58,7 @@ pub async fn get_balance( agent: &Agent, subaccount: Option<[u8; 32]>, token: &str, + of_principal: Option, ) -> Result { // Obtain token info let canister_id = match TOKEN_LEDGER_CIDS.get(token) { @@ -78,7 +79,7 @@ pub async fn get_balance( let (balance, decimals, symbol) = tokio::join!( // // Obtain token balance - get_raw_balance(agent, cid, subaccount), + get_raw_balance(agent, cid, subaccount, of_principal), // // Obtain the number of decimals the token uses async { @@ -120,10 +121,14 @@ pub async fn get_raw_balance( agent: &Agent, ledger: Principal, subaccount: Option<[u8; 32]>, + of_principal: Option, ) -> Result { - let owner = agent - .get_principal() - .map_err(|err| GetBalanceError::GetPrincipal { err })?; + let owner = match of_principal { + Some(p) => p, + None => agent + .get_principal() + .map_err(|err| GetBalanceError::GetPrincipal { err })?, + }; // Perform query let resp = agent .query(&ledger, "icrc1_balance_of") diff --git a/crates/icp-cli/tests/cycles_tests.rs b/crates/icp-cli/tests/cycles_tests.rs index fb4e21a68..a29ee9b58 100644 --- a/crates/icp-cli/tests/cycles_tests.rs +++ b/crates/icp-cli/tests/cycles_tests.rs @@ -240,10 +240,16 @@ async fn cycles_transfer() { .success(); // Check bob's balance - icp_client.use_identity("bob"); ctx.icp() .current_dir(&project_dir) - .args(["cycles", "balance", "--environment", "random-environment"]) + .args([ + "cycles", + "balance", + "--of-principal", + &bob_principal.to_string(), + "--environment", + "random-environment", + ]) .assert() .stdout(contains("Balance: 2_000_000_000_000 cycles")) .success(); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index fd0b19b4a..45bd7b8ef 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -781,6 +781,7 @@ Display the cycles balance * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--subaccount ` — The subaccount to check the balance for +* `--of-principal ` — Check the balance of this principal instead of the current identity * `--json` — Output command results as JSON * `-q`, `--quiet` — Suppress human-readable output; print only the balance @@ -1582,6 +1583,7 @@ Display the token balance on the ledger (default token: icp) * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as * `--subaccount ` — The subaccount to check the balance for +* `--of-principal ` — Check the balance of this principal instead of the current identity * `--json` — Output command results as JSON * `-q`, `--quiet` — Suppress human-readable output; print only the balance From 3c93b59f2faf4a5e161266b8c3c1783da370f02f Mon Sep 17 00:00:00 2001 From: Adam Spofford Date: Mon, 27 Apr 2026 15:42:49 -0700 Subject: [PATCH 2/3] fmt --- crates/icp-cli/src/commands/cycles/balance.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/icp-cli/src/commands/cycles/balance.rs b/crates/icp-cli/src/commands/cycles/balance.rs index 7f7ba81ab..98baed76a 100644 --- a/crates/icp-cli/src/commands/cycles/balance.rs +++ b/crates/icp-cli/src/commands/cycles/balance.rs @@ -48,9 +48,13 @@ pub(crate) async fn exec(ctx: &Context, args: &BalanceArgs) -> Result<(), anyhow .await?; // Get the balance from the ledger - let cycles = - get_raw_balance(&agent, CYCLES_LEDGER_PRINCIPAL, args.subaccount, args.of_principal) - .await?; + let cycles = get_raw_balance( + &agent, + CYCLES_LEDGER_PRINCIPAL, + args.subaccount, + args.of_principal, + ) + .await?; let cycles_amount = TokenAmount { amount: BigDecimal::from_biguint(cycles.0, 0), symbol: "cycles".to_string(), From f473322607709707db72d2058ace8b55dbde7c2e Mon Sep 17 00:00:00 2001 From: Adam Spofford Date: Thu, 30 Apr 2026 08:52:20 -0700 Subject: [PATCH 3/3] redo balance function args --- crates/icp-cli/src/commands/cycles/balance.rs | 11 ++++------- crates/icp-cli/src/commands/token/balance.rs | 5 ++++- crates/icp-cli/src/operations/token/balance.rs | 14 ++++---------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/crates/icp-cli/src/commands/cycles/balance.rs b/crates/icp-cli/src/commands/cycles/balance.rs index 98baed76a..192af682a 100644 --- a/crates/icp-cli/src/commands/cycles/balance.rs +++ b/crates/icp-cli/src/commands/cycles/balance.rs @@ -46,15 +46,12 @@ pub(crate) async fn exec(ctx: &Context, args: &BalanceArgs) -> Result<(), anyhow &selections.environment, ) .await?; + let owner = args + .of_principal + .unwrap_or_else(|| agent.get_principal().unwrap()); // Get the balance from the ledger - let cycles = get_raw_balance( - &agent, - CYCLES_LEDGER_PRINCIPAL, - args.subaccount, - args.of_principal, - ) - .await?; + let cycles = get_raw_balance(&agent, CYCLES_LEDGER_PRINCIPAL, owner, args.subaccount).await?; let cycles_amount = TokenAmount { amount: BigDecimal::from_biguint(cycles.0, 0), symbol: "cycles".to_string(), diff --git a/crates/icp-cli/src/commands/token/balance.rs b/crates/icp-cli/src/commands/token/balance.rs index 310e25af4..0875239e9 100644 --- a/crates/icp-cli/src/commands/token/balance.rs +++ b/crates/icp-cli/src/commands/token/balance.rs @@ -53,9 +53,12 @@ pub(crate) async fn exec( &selections.environment, ) .await?; + let owner = args + .of_principal + .unwrap_or_else(|| agent.get_principal().unwrap()); // Get the balance from the ledger - let balance = get_balance(&agent, args.subaccount, token, args.of_principal).await?; + let balance = get_balance(&agent, token, owner, args.subaccount).await?; if args.json { serde_json::to_writer( diff --git a/crates/icp-cli/src/operations/token/balance.rs b/crates/icp-cli/src/operations/token/balance.rs index 40183a731..271a7672f 100644 --- a/crates/icp-cli/src/operations/token/balance.rs +++ b/crates/icp-cli/src/operations/token/balance.rs @@ -56,9 +56,9 @@ pub enum GetBalanceError { /// pub async fn get_balance( agent: &Agent, - subaccount: Option<[u8; 32]>, token: &str, - of_principal: Option, + owner: Principal, + subaccount: Option<[u8; 32]>, ) -> Result { // Obtain token info let canister_id = match TOKEN_LEDGER_CIDS.get(token) { @@ -79,7 +79,7 @@ pub async fn get_balance( let (balance, decimals, symbol) = tokio::join!( // // Obtain token balance - get_raw_balance(agent, cid, subaccount, of_principal), + get_raw_balance(agent, cid, owner, subaccount), // // Obtain the number of decimals the token uses async { @@ -120,15 +120,9 @@ pub async fn get_balance( pub async fn get_raw_balance( agent: &Agent, ledger: Principal, + owner: Principal, subaccount: Option<[u8; 32]>, - of_principal: Option, ) -> Result { - let owner = match of_principal { - Some(p) => p, - None => agent - .get_principal() - .map_err(|err| GetBalanceError::GetPrincipal { err })?, - }; // Perform query let resp = agent .query(&ledger, "icrc1_balance_of")