Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ Example how to specify the subnet type:
}
----

=== feat: Introduce command for local cycles top-up

`dfx ledger fabricate-cycles <canister (id)> <optional amount>` 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.
Expand Down
44 changes: 44 additions & 0 deletions e2e/tests-dfx/fabricate_cycles.bash
Original file line number Diff line number Diff line change
@@ -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."
}
104 changes: 104 additions & 0 deletions src/dfx/src/commands/ledger/fabricate_cycles.rs
Original file line number Diff line number Diff line change
@@ -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<String>,

/// Specifies the amount of trillion cycles to fabricate. Defaults to 10T cycles.
#[clap(
long,
validator(trillion_cycle_amount_validator),
conflicts_with("amount")
)]
t: Option<String>,
Comment thread
This conversation was marked as resolved.

/// 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<String>,

/// 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::<u128>().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::<u128>()
.unwrap()
} else {
DEFAULT_CYCLES_TO_FABRICATE
}
}
3 changes: 3 additions & 0 deletions src/dfx/src/commands/ledger/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
Expand All @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions src/dfx/src/lib/operations/canister/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions src/dfx/src/lib/root_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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."
))
}
}
7 changes: 7 additions & 0 deletions src/dfx/src/util/clap/validators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u128>().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::<u64>() {
if num <= 100 {
Expand Down