diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 28e7ca432d..6be008692a 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -66,6 +66,10 @@ Example how to specify the subnet type: } ---- +=== feat: Introduce command for local cycles top-up + +`dfx ledger fabricate-cycles ` can be used during local development to create cycles out of thin air and add them to a canister. Instead of supplying a canister name or id it is also possible to use `--all` to add the cycles to every canister in the current project. When no amount is supplied, the command uses 10T cycles as default. Using this command with `--network ic` will result in an error. + === feat: Private keys can be stored in encrypted format `dfx identity new` and `dfx identity import` now ask you for a password to encrypt the private key (PEM file) when it is stored on disk. diff --git a/e2e/tests-dfx/fabricate_cycles.bash b/e2e/tests-dfx/fabricate_cycles.bash new file mode 100644 index 0000000000..0887bba691 --- /dev/null +++ b/e2e/tests-dfx/fabricate_cycles.bash @@ -0,0 +1,44 @@ +#!/usr/bin/env bats + +load ../utils/_ + +setup() { + standard_setup + + dfx_new hello +} + +teardown() { + dfx_stop + + standard_teardown +} + +@test "ledger fabricate-cycles works with default amount" { + install_asset greet + dfx_start + dfx deploy + # default amount is 10 trillion cycles, which results in an amount like 13_899_071_239_420 + assert_command dfx ledger fabricate-cycles --canister "$(dfx canister id hello)" + # bash does not accept \d, use [0-9] instead + assert_match 'updated balance: [0-9]{2}(_[0-9]{3}){4} cycles' + assert_command dfx ledger fabricate-cycles --all + assert_match 'updated balance: [0-9]{2}(_[0-9]{3}){4} cycles' +} + +@test "ledger fabricate-cycles works with specific amount" { + install_asset greet + dfx_start + dfx deploy + # adding 100 trillion cycles, which results in an amount like 103_899_071_239_420 + assert_command dfx ledger fabricate-cycles --canister "$(dfx canister id hello)" --amount 100000000000000 + assert_match 'updated balance: [0-9]{3}(_[0-9]{3}){4} cycles' + assert_command dfx ledger fabricate-cycles --canister hello --t 100 + assert_match 'updated balance: [0-9]{3}(_[0-9]{3}){4} cycles' +} + +@test "ledger fabricate-cycles fails on real IC" { + install_asset greet + assert_command_fail dfx ledger --network ic fabricate-cycles --all + assert_match "Cannot run this on the real IC." +} diff --git a/src/dfx/src/commands/ledger/fabricate_cycles.rs b/src/dfx/src/commands/ledger/fabricate_cycles.rs new file mode 100644 index 0000000000..761a492a53 --- /dev/null +++ b/src/dfx/src/commands/ledger/fabricate_cycles.rs @@ -0,0 +1,104 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::identity::identity_utils::CallSender; +use crate::lib::models::canister_id_store::CanisterIdStore; +use crate::lib::operations::canister; +use crate::lib::root_key::fetch_root_key_or_anyhow; +use crate::util::clap::validators::{cycle_amount_validator, trillion_cycle_amount_validator}; +use crate::util::expiry_duration; + +use clap::Parser; +use ic_types::Principal; +use slog::info; +use std::time::Duration; + +const DEFAULT_CYCLES_TO_FABRICATE: u128 = 10_000_000_000_000_u128; + +/// Local development only: Fabricate cycles out of thin air and deposit them into the specified canister(s). +#[derive(Parser)] +pub struct FabricateCyclesOpts { + /// Specifies the amount of cycles to fabricate. Defaults to 10T cycles. + #[clap(long, validator(cycle_amount_validator), conflicts_with("t"))] + amount: Option, + + /// Specifies the amount of trillion cycles to fabricate. Defaults to 10T cycles. + #[clap( + long, + validator(trillion_cycle_amount_validator), + conflicts_with("amount") + )] + t: Option, + + /// Specifies the name or id of the canister to receive the cycles deposit. + /// You must specify either a canister name/id or the --all option. + #[clap(long)] + canister: Option, + + /// Deposit cycles to all of the canisters configured in the dfx.json file. + #[clap(long, required_unless_present("canister"))] + all: bool, +} + +async fn deposit_minted_cycles( + env: &dyn Environment, + canister: &str, + timeout: Duration, + call_sender: &CallSender, + cycles: u128, +) -> DfxResult { + let log = env.get_logger(); + let canister_id_store = CanisterIdStore::for_env(env)?; + let canister_id = + Principal::from_text(canister).or_else(|_| canister_id_store.get(canister))?; + + info!(log, "Fabricating {} cycles onto {}", cycles, canister,); + + canister::provisional_deposit_cycles(env, canister_id, timeout, call_sender, cycles).await?; + + let status = canister::get_canister_status(env, canister_id, timeout, call_sender).await?; + + info!( + log, + "Fabricated {} cycles, updated balance: {} cycles", cycles, status.cycles + ); + + Ok(()) +} + +pub async fn exec(env: &dyn Environment, opts: FabricateCyclesOpts) -> DfxResult { + // amount has been validated by cycle_amount_validator + let cycles = cycles_to_fabricate(&opts); + + fetch_root_key_or_anyhow(env).await?; + + let timeout = expiry_duration(); + + if let Some(canister) = opts.canister.as_deref() { + deposit_minted_cycles(env, canister, timeout, &CallSender::SelectedId, cycles).await + } else if opts.all { + let config = env.get_config_or_anyhow()?; + if let Some(canisters) = &config.get_config().canisters { + for canister in canisters.keys() { + deposit_minted_cycles(env, canister, timeout, &CallSender::SelectedId, cycles) + .await?; + } + } + Ok(()) + } else { + unreachable!() + } +} + +fn cycles_to_fabricate(opts: &FabricateCyclesOpts) -> u128 { + if let Some(cycles_str) = &opts.amount { + //cycles_str is validated by cycle_amount_validator + cycles_str.parse::().unwrap() + } else if let Some(t_cycles_str) = &opts.t { + //cycles_str is validated by trillion_cycle_amount_validator + format!("{}000000000000", t_cycles_str) + .parse::() + .unwrap() + } else { + DEFAULT_CYCLES_TO_FABRICATE + } +} diff --git a/src/dfx/src/commands/ledger/mod.rs b/src/dfx/src/commands/ledger/mod.rs index 0e2eff37c5..1f09ec6210 100644 --- a/src/dfx/src/commands/ledger/mod.rs +++ b/src/dfx/src/commands/ledger/mod.rs @@ -28,6 +28,7 @@ const NOTIFY_METHOD: &str = "notify_dfx"; mod account_id; mod balance; mod create_canister; +mod fabricate_cycles; mod notify; mod top_up; mod transfer; @@ -49,6 +50,7 @@ enum SubCommand { AccountId(account_id::AccountIdOpts), Balance(balance::BalanceOpts), CreateCanister(create_canister::CreateCanisterOpts), + FabricateCycles(fabricate_cycles::FabricateCyclesOpts), Notify(notify::NotifyOpts), TopUp(top_up::TopUpOpts), Transfer(transfer::TransferOpts), @@ -62,6 +64,7 @@ pub fn exec(env: &dyn Environment, opts: LedgerOpts) -> DfxResult { SubCommand::AccountId(v) => account_id::exec(&agent_env, v).await, SubCommand::Balance(v) => balance::exec(&agent_env, v).await, SubCommand::CreateCanister(v) => create_canister::exec(&agent_env, v).await, + SubCommand::FabricateCycles(v) => fabricate_cycles::exec(&agent_env, v).await, SubCommand::Notify(v) => notify::exec(&agent_env, v).await, SubCommand::TopUp(v) => top_up::exec(&agent_env, v).await, SubCommand::Transfer(v) => transfer::exec(&agent_env, v).await, diff --git a/src/dfx/src/lib/operations/canister/mod.rs b/src/dfx/src/lib/operations/canister/mod.rs index d318b9402f..ec2572c74d 100644 --- a/src/dfx/src/lib/operations/canister/mod.rs +++ b/src/dfx/src/lib/operations/canister/mod.rs @@ -258,6 +258,37 @@ pub async fn deposit_cycles( Ok(()) } +/// Can only run this locally, not on the real IC. +/// Conjures cycles from nothing and deposits them in the selected canister. +pub async fn provisional_deposit_cycles( + env: &dyn Environment, + canister_id: Principal, + timeout: Duration, + call_sender: &CallSender, + cycles: u128, +) -> DfxResult { + #[derive(CandidType)] + struct In { + canister_id: Principal, + amount: u128, + } + let _: () = do_management_call( + env, + canister_id, + MgmtMethod::ProvisionalTopUpCanister.as_ref(), + In { + canister_id, + amount: cycles, + }, + timeout, + call_sender, + 0, + ) + .await?; + + Ok(()) +} + pub fn get_local_cid_and_candid_path( env: &dyn Environment, canister_name: &str, diff --git a/src/dfx/src/lib/root_key.rs b/src/dfx/src/lib/root_key.rs index b5d909fb2b..c2ef274067 100644 --- a/src/dfx/src/lib/root_key.rs +++ b/src/dfx/src/lib/root_key.rs @@ -17,3 +17,24 @@ pub async fn fetch_root_key_if_needed(env: &dyn Environment) -> DfxResult { } Ok(()) } + +/// Fetches the root key of the local network. +/// Returns an error if attempted to run on the real IC. +pub async fn fetch_root_key_or_anyhow(env: &dyn Environment) -> DfxResult { + let agent = env + .get_agent() + .ok_or_else(|| anyhow!("Cannot get HTTP client from environment."))?; + + if !env + .get_network_descriptor() + .expect("no network descriptor") + .is_ic + { + agent.fetch_root_key().await?; + Ok(()) + } else { + Err(anyhow!( + "This command only runs on local instances. Cannot run this on the real IC." + )) + } +} diff --git a/src/dfx/src/util/clap/validators.rs b/src/dfx/src/util/clap/validators.rs index a30c6ec40e..85c78cc014 100644 --- a/src/dfx/src/util/clap/validators.rs +++ b/src/dfx/src/util/clap/validators.rs @@ -44,6 +44,13 @@ pub fn cycle_amount_validator(cycles: &str) -> Result<(), String> { Err("Must be a non negative amount.".to_string()) } +pub fn trillion_cycle_amount_validator(cycles: &str) -> Result<(), String> { + if format!("{}000000000000", cycles).parse::().is_ok() { + return Ok(()); + } + Err("Must be a non negative amount.".to_string()) +} + pub fn compute_allocation_validator(compute_allocation: &str) -> Result<(), String> { if let Ok(num) = compute_allocation.parse::() { if num <= 100 {