diff --git a/CHANGELOG.md b/CHANGELOG.md index 065c77b180..d350ddd452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,10 @@ The parameter was erroneously passed twice. Now it is passed only once. Removed this warning: "Project-specific networks are deprecated and will be removed after February 2023." While we may remove project-specific networks in the future, it is not imminent. One key requirement is the ability to run more than one subnet type at one time. +### feat: added `cycles balance` command + +This won't work on mainnet yet, but it can work locally after installing the cycles ledger. + ## Dependencies ### icx-proxy diff --git a/Cargo.lock b/Cargo.lock index 36caf124d2..10af87e4af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1364,6 +1364,7 @@ dependencies = [ "ic-identity-hsm", "ic-utils 0.29.0", "ic-wasm", + "icrc-ledger-types", "indicatif", "itertools 0.10.5", "json-patch", @@ -2988,6 +2989,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "icrc-ledger-types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ee120bab077a71cf85fcc6b0e88674edfeca7fe18240bee9afa947d8e08865" +dependencies = [ + "candid 0.9.6", + "hex", + "num-traits", + "serde", + "serde_bytes", + "sha2 0.10.7", +] + [[package]] name = "icx-asset" version = "0.20.0" diff --git a/Cargo.toml b/Cargo.toml index 4cae352f04..18a85414e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ aes-gcm = "0.9.4" anyhow = "1.0.56" anstyle = "1.0.0" argon2 = "0.4.0" -backoff = "0.4.0" +backoff = { version = "0.4.0", features = [ "futures", "tokio" ] } base64 = "0.13.0" byte-unit = "4.0.14" bytes = "1.2.1" diff --git a/docs/cli-reference/dfx-cycles.md b/docs/cli-reference/dfx-cycles.md new file mode 100644 index 0000000000..c027ecb7dc --- /dev/null +++ b/docs/cli-reference/dfx-cycles.md @@ -0,0 +1,67 @@ +# dfx cycles + +> **NOTE**: The cycles ledger is in development and the dfx cycles command is not expected to work on mainnet at this time. + +Use the `dfx cycles` command to manage cycles associated with an identity's principal. + +The basic syntax for running `dfx cycles` commands is: + +``` bash +dfx cycles [subcommand] [options] +``` + +The following subcommands are available: + +| Command | Description | +|---------------------------------------|--------------------------------------------------------------------------------------| +| [`balance`](#dfx-ledger-balance) | Prints the account balance of the user. | +| `help` | Displays usage information message for a specified subcommand. | + +To view usage information for a specific subcommand, specify the subcommand and the `--help` flag. For example, to see usage information for `dfx cycles balance`, you can run the following command: + +`dfx cycles balance --help` + +## dfx cycles balance + +Use the `dfx cycles balance` command to print your account balance or that of another user. + +### Basic usage + +``` bash +dfx cycles balance [flag] --network ic +``` + +### Options + +You can specify the following arguments for the `dfx cycles balance` command. + +| Option | Description | +|---------------------------------------------|---------------------------------------------------------------------| +| `--owner ` | Display the balance of this principal | +| `--subaccount ` | Display the balance of this subaccount | +| `--precise` | Displays the exact balance, without scaling to trillions of cycles. | +| `--cycles-ledger-canister-id ` | Specify the ID of the cycles ledger canister. | + +### Examples + +> **NOTE**: None of the examples below specify the `--cycles-ledger-canister-id` option, but it is required until the cycles ledger canister ID is known. + +Check the cycles balance of the selected identity. + +``` +$ dfx cycles balance --network ic +89.000 TC (trillion cycles). +``` + +To see the exact amount of cycles, you can use the `--precise` option: +``` +$ dfx cycles balance --network ic --precise +89000000000000 cycles. +``` + +You can use the `dfx cycles balance` command to check the balance of another principal: + +``` bash +dfx cycles balance --owner raxcz-bidhr-evrzj-qyivt-nht5a-eltcc-24qfc-o6cvi-hfw7j-dcecz-kae --network ic +``` + diff --git a/docs/cli-reference/index.md b/docs/cli-reference/index.md index c4290ce2cd..d05b68d23a 100644 --- a/docs/cli-reference/index.md +++ b/docs/cli-reference/index.md @@ -24,6 +24,8 @@ When you have the SDK installed, you can use the following commands to specify t - [dfx canister](./dfx-canister.md) +- [dfx cycles](./dfx-cycles.md) + - [dfx deploy](./dfx-deploy.md) - [dfx deps](./dfx-deps.md) diff --git a/e2e/tests-dfx/cycles-ledger.bash b/e2e/tests-dfx/cycles-ledger.bash index d942ee5032..a0ee937b4f 100644 --- a/e2e/tests-dfx/cycles-ledger.bash +++ b/e2e/tests-dfx/cycles-ledger.bash @@ -25,6 +25,80 @@ teardown() { standard_teardown } +@test "cycles ledger balance" { + ALICE=$(dfx identity get-principal --identity alice) + ALICE_SUBACCT1="000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + ALICE_SUBACCT1_CANDID="\00\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b\1c\1d\1e\1f" + ALICE_SUBACCT2="9C9B9A030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + ALICE_SUBACCT2_CANDID="\9C\9B\9A\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b\1c\1d\1e\1f" + BOB=$(dfx identity get-principal --identity bob) + + assert_command dfx deploy cycles-ledger + assert_command dfx deploy cycles-depositor --argument "(record {ledger_id = principal \"$(dfx canister id cycles-ledger)\"})" --with-cycles 10000000000000 + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --precise + assert_eq "0 cycles." + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice + assert_eq "0.000 TC (trillion cycles)." + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity bob --precise + assert_eq "0 cycles." + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity bob + assert_eq "0.000 TC (trillion cycles)." + + + assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\";};cycles = 1_700_400_200_150;})" --identity cycle-giver + assert_eq "(record { balance = 1_700_400_200_150 : nat; txid = 0 : nat })" + + assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\"; subaccount = opt blob \"$ALICE_SUBACCT1_CANDID\"};cycles = 3_750_000_000_000;})" --identity cycle-giver + assert_eq "(record { balance = 3_750_000_000_000 : nat; txid = 1 : nat })" + + assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\"; subaccount = opt blob \"$ALICE_SUBACCT2_CANDID\"};cycles = 760_500_000_000;})" --identity cycle-giver + assert_eq "(record { balance = 760_500_000_000 : nat; txid = 2 : nat })" + + assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$BOB\";};cycles = 2_900_000_000_000;})" --identity cycle-giver + assert_eq "(record { balance = 2_900_000_000_000 : nat; txid = 3 : nat })" + + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity alice + assert_eq "1700400200150 cycles." + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity alice --subaccount "$ALICE_SUBACCT1" + assert_eq "3750000000000 cycles." + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity alice --subaccount "$ALICE_SUBACCT2" + assert_eq "760500000000 cycles." + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --precise --identity bob + assert_eq "2900000000000 cycles." + + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice + assert_eq "1.700 TC (trillion cycles)." + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --subaccount "$ALICE_SUBACCT1" + assert_eq "3.750 TC (trillion cycles)." + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --subaccount "$ALICE_SUBACCT2" + assert_eq "0.760 TC (trillion cycles)." + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity bob + assert_eq "2.900 TC (trillion cycles)." + + + # can see cycles balance of other accounts + assert_command dfx cycles balance --owner "$ALICE" --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity bob + assert_eq "1.700 TC (trillion cycles)." + + assert_command dfx cycles balance --owner "$ALICE" --subaccount "$ALICE_SUBACCT1" --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity bob + assert_eq "3.750 TC (trillion cycles)." + + assert_command dfx cycles balance --owner "$BOB" --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity anonymous + assert_eq "2.900 TC (trillion cycles)." +} + @test "cycles ledger howto" { # This is the equivalent of https://www.notion.so/dfinityorg/How-to-install-and-test-the-cycles-ledger-521c9f3c410f4a438514a03e35464299 ALICE=$(dfx identity get-principal --identity alice) @@ -41,11 +115,12 @@ teardown() { dfx canister status cycles-depositor - assert_command dfx canister call cycles-ledger icrc1_balance_of "(record {owner = principal \"$ALICE\"})" - assert_eq "(0 : nat)" + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --precise + assert_eq "0 cycles." + + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity bob --precise + assert_eq "0 cycles." - assert_command dfx canister call cycles-ledger icrc1_balance_of "(record {owner = principal \"$BOB\"})" - assert_eq "(0 : nat)" assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\";};cycles = 500_000_000;})" --identity cycle-giver assert_eq "(record { balance = 500_000_000 : nat; txid = 0 : nat })" @@ -53,23 +128,23 @@ teardown() { assert_command dfx canister status cycles-depositor assert_contains "Balance: 9_999_500_000_000 Cycles" - assert_command dfx canister call cycles-ledger icrc1_balance_of "(record {owner = principal \"$ALICE\"})" - assert_eq "(500_000_000 : nat)" + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --precise + assert_eq "500000000 cycles." assert_command dfx canister call cycles-ledger icrc1_transfer "(record {to = record{owner = principal \"$BOB\"}; amount = 100_000;})" --identity alice assert_eq "(variant { Ok = 1 : nat })" - assert_command dfx canister call cycles-ledger icrc1_balance_of "(record {owner = principal \"$ALICE\"})" - assert_eq "(399_900_000 : nat)" + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --precise + assert_eq "399900000 cycles." - assert_command dfx canister call cycles-ledger icrc1_balance_of "(record {owner = principal \"$BOB\"})" - assert_eq "(100_000 : nat)" + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity bob --precise + assert_eq "100000 cycles." assert_command dfx canister call cycles-ledger send "(record {amount = 100_000;to = principal \"$(dfx canister id cycles-depositor)\"})" --identity alice assert_eq "(variant { Ok = 2 : nat })" - assert_command dfx canister call cycles-ledger icrc1_balance_of "(record {owner = principal \"$ALICE\"})" - assert_eq "(299_800_000 : nat)" + assert_command dfx cycles balance --cycles-ledger-canister-id "$(dfx canister id cycles-ledger)" --identity alice --precise + assert_eq "299800000 cycles." assert_command dfx canister status cycles-depositor assert_contains "Balance: 9_999_500_100_000 Cycles" diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index 4f6f1ae8a2..011e596b88 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -66,6 +66,7 @@ ic-asset.workspace = true ic-identity-hsm = { workspace = true } ic-utils = { workspace = true } ic-wasm = "0.4.0" +icrc-ledger-types = "0.1.1" indicatif = "0.16.0" itertools.workspace = true json-patch = "1.0.0" diff --git a/src/dfx/src/commands/cycles/balance.rs b/src/dfx/src/commands/cycles/balance.rs new file mode 100644 index 0000000000..1cdbe78e1b --- /dev/null +++ b/src/dfx/src/commands/cycles/balance.rs @@ -0,0 +1,54 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::nns_types::account_identifier::Subaccount; +use crate::lib::operations::cycles_ledger; +use crate::util::{format_as_trillions, pretty_thousand_separators}; +use candid::Principal; +use clap::Parser; + +/// Get the cycle balance of the selected Identity's cycles wallet. +#[derive(Parser)] +pub struct CyclesBalanceOpts { + /// Specifies a Principal to get the balance of + #[arg(long)] + owner: Option, + + /// Subaccount of the selected identity to get the balance of + #[arg(long)] + subaccount: Option, + + /// Get balance raw value (without upscaling to trillions of cycles). + #[arg(long)] + precise: bool, + + /// Canister ID of the cycles ledger canister. + /// If not specified, the default cycles ledger canister ID will be used. + // todo: remove this. See https://dfinity.atlassian.net/browse/SDK-1262 + #[arg(long)] + cycles_ledger_canister_id: Principal, +} + +pub async fn exec(env: &dyn Environment, opts: CyclesBalanceOpts) -> DfxResult { + let agent = env.get_agent(); + + let owner = opts.owner.unwrap_or_else(|| { + env.get_selected_identity_principal() + .expect("Selected identity not instantiated.") + }); + + let subaccount = opts.subaccount.map(|x| x.0); + + let balance = + cycles_ledger::balance(agent, owner, subaccount, opts.cycles_ledger_canister_id).await?; + + if opts.precise { + println!("{} cycles.", balance); + } else { + println!( + "{} TC (trillion cycles).", + pretty_thousand_separators(format_as_trillions(balance)) + ); + } + + Ok(()) +} diff --git a/src/dfx/src/commands/cycles/mod.rs b/src/dfx/src/commands/cycles/mod.rs new file mode 100644 index 0000000000..36a269a713 --- /dev/null +++ b/src/dfx/src/commands/cycles/mod.rs @@ -0,0 +1,34 @@ +use crate::lib::agent::create_agent_environment; +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::network::network_opt::NetworkOpt; +use clap::Parser; +use tokio::runtime::Runtime; + +mod balance; + +/// Helper commands to manage the user's cycles. +#[derive(Parser)] +#[command(name = "wallet")] +pub struct CyclesOpts { + #[command(flatten)] + network: NetworkOpt, + + #[command(subcommand)] + subcmd: SubCommand, +} + +#[derive(Parser)] +enum SubCommand { + Balance(balance::CyclesBalanceOpts), +} + +pub fn exec(env: &dyn Environment, opts: CyclesOpts) -> DfxResult { + let agent_env = create_agent_environment(env, opts.network.to_network_name())?; + let runtime = Runtime::new().expect("Unable to create a runtime"); + runtime.block_on(async { + match opts.subcmd { + SubCommand::Balance(v) => balance::exec(&agent_env, v).await, + } + }) +} diff --git a/src/dfx/src/commands/mod.rs b/src/dfx/src/commands/mod.rs index 2a402902d5..139ac8ae6c 100644 --- a/src/dfx/src/commands/mod.rs +++ b/src/dfx/src/commands/mod.rs @@ -7,6 +7,7 @@ mod beta; mod build; mod cache; mod canister; +mod cycles; mod deploy; mod deps; mod diagnose; @@ -35,6 +36,8 @@ pub enum DfxCommand { Build(build::CanisterBuildOpts), Cache(cache::CacheOpts), Canister(canister::CanisterOpts), + #[command(hide = true)] + Cycles(cycles::CyclesOpts), Deploy(deploy::DeployOpts), Deps(deps::DepsOpts), Diagnose(diagnose::DiagnoseOpts), @@ -64,6 +67,7 @@ pub fn exec(env: &dyn Environment, cmd: DfxCommand) -> DfxResult { DfxCommand::Build(v) => build::exec(env, v), DfxCommand::Cache(v) => cache::exec(env, v), DfxCommand::Canister(v) => canister::exec(env, v), + DfxCommand::Cycles(v) => cycles::exec(env, v), DfxCommand::Deploy(v) => deploy::exec(env, v), DfxCommand::Deps(v) => deps::exec(env, v), DfxCommand::Diagnose(v) => diagnose::exec(env, v), diff --git a/src/dfx/src/lib/mod.rs b/src/dfx/src/lib/mod.rs index 221cd6999d..e53ae0828d 100644 --- a/src/dfx/src/lib/mod.rs +++ b/src/dfx/src/lib/mod.rs @@ -27,6 +27,7 @@ pub mod progress_bar; pub mod project; pub mod replica; pub mod replica_config; +pub mod retryable; pub mod root_key; pub mod sign; pub mod state_tree; diff --git a/src/dfx/src/lib/operations/cycles_ledger.rs b/src/dfx/src/lib/operations/cycles_ledger.rs new file mode 100644 index 0000000000..ba75220d4e --- /dev/null +++ b/src/dfx/src/lib/operations/cycles_ledger.rs @@ -0,0 +1,44 @@ +use crate::lib::error::DfxResult; +use crate::lib::retryable::retryable; +use anyhow::anyhow; +use backoff::future::retry; +use backoff::ExponentialBackoff; +use candid::Principal; +use ic_agent::Agent; +use ic_utils::call::SyncCall; +use ic_utils::Canister; +use icrc_ledger_types::icrc1; + +const ICRC1_BALANCE_OF_METHOD: &str = "icrc1_balance_of"; + +pub async fn balance( + agent: &Agent, + owner: Principal, + subaccount: Option, + cycles_ledger_canister_id: Principal, +) -> DfxResult { + let canister = Canister::builder() + .with_agent(agent) + .with_canister_id(cycles_ledger_canister_id) + .build()?; + let arg = icrc1::account::Account { owner, subaccount }; + + let retry_policy = ExponentialBackoff::default(); + + retry(retry_policy, || async { + let result = canister + .query(ICRC1_BALANCE_OF_METHOD) + .with_arg(arg) + .build() + .call() + .await; + match result { + Ok((balance,)) => Ok(balance), + Err(agent_err) if retryable(&agent_err) => { + Err(backoff::Error::transient(anyhow!(agent_err))) + } + Err(agent_err) => Err(backoff::Error::permanent(anyhow!(agent_err))), + } + }) + .await +} diff --git a/src/dfx/src/lib/operations/mod.rs b/src/dfx/src/lib/operations/mod.rs index bbe6390dd4..d7d7b006f5 100644 --- a/src/dfx/src/lib/operations/mod.rs +++ b/src/dfx/src/lib/operations/mod.rs @@ -1,3 +1,4 @@ pub mod canister; pub mod cmc; +pub mod cycles_ledger; pub mod ledger; diff --git a/src/dfx/src/lib/retryable.rs b/src/dfx/src/lib/retryable.rs new file mode 100644 index 0000000000..edc76f241f --- /dev/null +++ b/src/dfx/src/lib/retryable.rs @@ -0,0 +1,8 @@ +use ic_agent::AgentError; + +pub fn retryable(agent_error: &AgentError) -> bool { + matches!( + agent_error, + AgentError::TimeoutWaitingForResponse() | AgentError::TransportError(_) + ) +}