From fc30cefc31315cd730c34c2d52490141594233cd Mon Sep 17 00:00:00 2001 From: Severin Siffert Date: Fri, 22 Apr 2022 14:31:48 +0200 Subject: [PATCH 1/4] feat: add possibility to enter ICP and e8s as amount of cycles to fabricate --- .../src/commands/ledger/fabricate_cycles.rs | 95 +++++++++++++++---- src/dfx/src/lib/root_key.rs | 12 ++- src/dfx/src/util/clap/validators.rs | 2 +- src/dfx/src/util/mod.rs | 1 + 4 files changed, 88 insertions(+), 22 deletions(-) diff --git a/src/dfx/src/commands/ledger/fabricate_cycles.rs b/src/dfx/src/commands/ledger/fabricate_cycles.rs index 761a492a53..31a7ea04b2 100644 --- a/src/dfx/src/commands/ledger/fabricate_cycles.rs +++ b/src/dfx/src/commands/ledger/fabricate_cycles.rs @@ -4,24 +4,72 @@ 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::clap::validators::{ + cycle_amount_validator, e8s_validator, icpts_amount_validator, trillion_cycle_amount_validator, +}; +use crate::util::currency_conversion::as_cycles_with_current_exchange_rate; use crate::util::expiry_duration; +use anyhow::Context; use clap::Parser; use ic_types::Principal; use slog::info; use std::time::Duration; +use super::get_icpts_from_args; + 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). +/// Can specify a number of ICP/e8s (which will be converted to cycles using the current exchange rate) or a number of cycles. +/// If no amount is specified, 10T cycles are added. #[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"))] + /// Specifies the amount of cycles to fabricate. + #[clap( + long, + validator(cycle_amount_validator), + conflicts_with("t"), + conflicts_with("amount"), + conflicts_with("icp"), + conflicts_with("e8s") + )] + cycles: Option, + + /// ICP to mint into cycles and deposit into destination canister + /// Can be specified as a Decimal with the fractional portion up to 8 decimal places + /// i.e. 100.012 + #[clap( + long, + validator(icpts_amount_validator), + conflicts_with("cycles"), + conflicts_with("icp"), + conflicts_with("e8s"), + conflicts_with("t") + )] amount: Option, - /// Specifies the amount of trillion cycles to fabricate. Defaults to 10T cycles. + /// Specify ICP as a whole number, helpful for use in conjunction with `--e8s` + #[clap( + long, + validator(e8s_validator), + conflicts_with("amount"), + conflicts_with("cycles"), + conflicts_with("t") + )] + icp: Option, + + /// Specify e8s as a whole number, helpful for use in conjunction with `--icp` + #[clap( + long, + validator(e8s_validator), + conflicts_with("amount"), + conflicts_with("cycles"), + conflicts_with("t") + )] + e8s: Option, + + /// Specifies the amount of trillion cycles to fabricate. #[clap( long, validator(trillion_cycle_amount_validator), @@ -31,7 +79,7 @@ pub struct FabricateCyclesOpts { /// 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)] + #[clap(long, required_unless_present("all"))] canister: Option, /// Deposit cycles to all of the canisters configured in the dfx.json file. @@ -51,23 +99,20 @@ async fn deposit_minted_cycles( 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 + "Fabricated {} cycles for {}, updated balance: {} cycles", cycles, canister, 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); + let cycles = cycles_to_fabricate(env, &opts).await?; fetch_root_key_or_anyhow(env).await?; @@ -89,16 +134,30 @@ pub async fn exec(env: &dyn Environment, opts: FabricateCyclesOpts) -> DfxResult } } -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() +async fn cycles_to_fabricate(env: &dyn Environment, opts: &FabricateCyclesOpts) -> DfxResult { + if let Some(cycles_str) = &opts.cycles { + //cycles_str is validated by cycle_amount_validator. Therefore unwrap is safe + Ok(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) + //t_cycles_str is validated by trillion_cycle_amount_validator. Therefore unwrap is safe + Ok(format!("{}000000000000", t_cycles_str) .parse::() - .unwrap() + .unwrap()) + } else if opts.amount.is_some() || opts.icp.is_some() || opts.e8s.is_some() { + let icpts = get_icpts_from_args(&opts.amount, &opts.icp, &opts.e8s) + .context("Encountered an error while parsing --amount, --icp, or --e8s")?; + let cycles = as_cycles_with_current_exchange_rate(&icpts) + .await + .context("Encountered an error while converting at the current exchange rate. If this issue persist, please specify an amount of cycles manually using the --cycles or --t flag.")?; + let log = env.get_logger(); + info!( + log, + "At the current exchange rate, {} e8s produces approximately {} cycles.", + icpts.get_e8s().to_string(), + cycles.to_string() + ); + Ok(cycles) } else { - DEFAULT_CYCLES_TO_FABRICATE + Ok(DEFAULT_CYCLES_TO_FABRICATE) } } diff --git a/src/dfx/src/lib/root_key.rs b/src/dfx/src/lib/root_key.rs index c2ef274067..3c966e8ff7 100644 --- a/src/dfx/src/lib/root_key.rs +++ b/src/dfx/src/lib/root_key.rs @@ -1,7 +1,7 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; pub async fn fetch_root_key_if_needed(env: &dyn Environment) -> DfxResult { let agent = env @@ -13,7 +13,10 @@ pub async fn fetch_root_key_if_needed(env: &dyn Environment) -> DfxResult { .expect("no network descriptor") .is_ic { - agent.fetch_root_key().await?; + agent + .fetch_root_key() + .await + .context("Encountered an error while trying to fetch the root key.")?; } Ok(()) } @@ -30,7 +33,10 @@ pub async fn fetch_root_key_or_anyhow(env: &dyn Environment) -> DfxResult { .expect("no network descriptor") .is_ic { - agent.fetch_root_key().await?; + agent + .fetch_root_key() + .await + .context("Encountered an error while trying to fetch the local replica's root key.")?; Ok(()) } else { Err(anyhow!( diff --git a/src/dfx/src/util/clap/validators.rs b/src/dfx/src/util/clap/validators.rs index 85c78cc014..645c3ffaf0 100644 --- a/src/dfx/src/util/clap/validators.rs +++ b/src/dfx/src/util/clap/validators.rs @@ -48,7 +48,7 @@ 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()) + Err("Must be a non negative amount. Currently only accepts whole numbers. Use --cycles otherwise.".to_string()) } pub fn compute_allocation_validator(compute_allocation: &str) -> Result<(), String> { diff --git a/src/dfx/src/util/mod.rs b/src/dfx/src/util/mod.rs index 325afc9218..4aa9d3ebf4 100644 --- a/src/dfx/src/util/mod.rs +++ b/src/dfx/src/util/mod.rs @@ -11,6 +11,7 @@ use std::time::Duration; pub mod assets; pub mod clap; +pub mod currency_conversion; // The user can pass in port "0" to dfx start or dfx bootstrap i.e. "127.0.0.1:0" or "[::1]:0", // thus, we need to recreate SocketAddr with the kernel provided dynmically allocated port here. From 1979a616c0e225173be7e279ae5aa2842ce9bde6 Mon Sep 17 00:00:00 2001 From: Severin Siffert Date: Fri, 22 Apr 2022 14:42:55 +0200 Subject: [PATCH 2/4] add the missing file --- src/dfx/src/util/currency_conversion.rs | 64 +++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/dfx/src/util/currency_conversion.rs diff --git a/src/dfx/src/util/currency_conversion.rs b/src/dfx/src/util/currency_conversion.rs new file mode 100644 index 0000000000..4c885951c5 --- /dev/null +++ b/src/dfx/src/util/currency_conversion.rs @@ -0,0 +1,64 @@ +use crate::{ + config::dfinity::DEFAULT_IC_GATEWAY, + lib::{ + error::DfxResult, + nns_types::icpts::{ICPTs, ICP_SUBDIVIDABLE_BY}, + }, +}; +use anyhow::Context; +use candid::{CandidType, Decode, Deserialize, Encode, Principal}; +use ic_agent::{agent::http_transport::ReqwestHttpReplicaV2Transport, Agent}; +use serde::Serialize; +use std::{convert::TryFrom, str::FromStr}; + +/// How many cycles you get per XDR when converting ICP to cycles +const CYCLES_PER_XDR: u128 = 1_000_000_000_000; +const CMC_MAINNET_PRINCIPAL: &str = "rkp4c-7iaaa-aaaaa-aaaca-cai"; + +/// This returns how many cycles the amount of ICP/e8s is currently worth. +/// Fetches the exchange rate from the (hardcoded) IC network. +pub async fn as_cycles_with_current_exchange_rate(icpts: &ICPTs) -> DfxResult { + let cycles_per_icp: u128 = { + let agent = Agent::builder() + .with_transport(ReqwestHttpReplicaV2Transport::create(DEFAULT_IC_GATEWAY)?) + .build() + .context("Cannot create mainnet agent.")?; + let response = agent + .query(&Principal::from_str(CMC_MAINNET_PRINCIPAL)?, "get_icp_xdr_conversion_rate") + .with_arg(Encode!(&()).unwrap()) + .call() + .await + .context("Failed to fetch ICP -> cycles conversion rate from mainnet CMC.")?; + + /// These two data structures are stolen straight from https://github.com/dfinity/ic/blob/master/rs/nns/cmc/src/lib.rs + /// At the time of writing, the latest version is https://github.com/dfinity/ic/blob/d69f7f5b6682958bfdc4836ca4adfa83ce3d4252/rs/nns/cmc/src/lib.rs + #[derive(Serialize, Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] + pub struct IcpXdrConversionRateCertifiedResponse { + pub data: IcpXdrConversionRate, + pub hash_tree: Vec, + pub certificate: Vec, + } + #[derive(Serialize, Deserialize, CandidType, Clone, PartialEq, Eq, Debug, Default)] + pub struct IcpXdrConversionRate { + /// The time for which the market data was queried, expressed in UNIX epoch + /// time in seconds. + pub timestamp_seconds: u64, + /// The number of 10,000ths of IMF SDR (currency code XDR) that corresponds + /// to 1 ICP. This value reflects the current market price of one ICP + /// token. In other words, this value specifies the ICP/XDR conversion + /// rate to four decimal places. + pub xdr_permyriad_per_icp: u64, + + } + let decoded_response: IcpXdrConversionRateCertifiedResponse = Decode!(response.as_slice(), IcpXdrConversionRateCertifiedResponse).context("Failed to decode CMC response.")?; + + let cycles_per_icp: u128 = u128::try_from(decoded_response.data.xdr_permyriad_per_icp).context("Encountered an error while translating response into cycles")? * (CYCLES_PER_XDR / 10_000); + DfxResult::::Ok(cycles_per_icp) + }.context("Encountered a problem while fetching the exchange rate bewteen ICP and cycles. If this issue continues to happen, please sepcify an amount in cycles directly.")?; + + // This will make rounding errors, but that's fine. We just don't want to be wildly inaccurate. + let cycles_per_e8s = cycles_per_icp / u128::from(ICP_SUBDIVIDABLE_BY); + let total_cycles = cycles_per_icp * u128::from(icpts.get_icpts()) + + cycles_per_e8s * u128::from(icpts.get_remainder_e8s()); + Ok(total_cycles) +} From bf44e226164f3a5482b86d12158b2aa2955f1e95 Mon Sep 17 00:00:00 2001 From: Severin Siffert Date: Tue, 26 Apr 2022 09:13:38 +0200 Subject: [PATCH 3/4] fix broken tests --- e2e/tests-dfx/fabricate_cycles.bash | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/e2e/tests-dfx/fabricate_cycles.bash b/e2e/tests-dfx/fabricate_cycles.bash index 0887bba691..a702c6fa35 100644 --- a/e2e/tests-dfx/fabricate_cycles.bash +++ b/e2e/tests-dfx/fabricate_cycles.bash @@ -31,7 +31,7 @@ teardown() { 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_command dfx ledger fabricate-cycles --canister "$(dfx canister id hello)" --cycles 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' @@ -42,3 +42,12 @@ teardown() { assert_command_fail dfx ledger --network ic fabricate-cycles --all assert_match "Cannot run this on the real IC." } + +@test "ledger fabricate-cycles fails with wrong option combinations" { + install_asset greet + assert_command_fail dfx ledger --network ic fabricate-cycles --all --cycles 1 --icp 1 + assert_command_fail dfx ledger --network ic fabricate-cycles --all --icp 1 --t 1 + assert_command_fail dfx ledger --network ic fabricate-cycles --all --t 1 --cycles 1 + assert_command_fail dfx ledger --network ic fabricate-cycles --all --e8s 1 --amount 1 + assert_command_fail dfx ledger --network ic fabricate-cycles --all --amount 1 --cycles 1 +} From 21ae133d1c14c40efcb080d8a89662d6501c8974 Mon Sep 17 00:00:00 2001 From: Severin Siffert Date: Tue, 26 Apr 2022 18:00:28 +0200 Subject: [PATCH 4/4] Update src/dfx/src/util/currency_conversion.rs Co-authored-by: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> --- src/dfx/src/util/currency_conversion.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dfx/src/util/currency_conversion.rs b/src/dfx/src/util/currency_conversion.rs index 4c885951c5..2c40ad2cd1 100644 --- a/src/dfx/src/util/currency_conversion.rs +++ b/src/dfx/src/util/currency_conversion.rs @@ -54,7 +54,7 @@ pub async fn as_cycles_with_current_exchange_rate(icpts: &ICPTs) -> DfxResult::Ok(cycles_per_icp) - }.context("Encountered a problem while fetching the exchange rate bewteen ICP and cycles. If this issue continues to happen, please sepcify an amount in cycles directly.")?; + }.context("Encountered a problem while fetching the exchange rate between ICP and cycles. If this issue continues to happen, please specify an amount in cycles directly.")?; // This will make rounding errors, but that's fine. We just don't want to be wildly inaccurate. let cycles_per_e8s = cycles_per_icp / u128::from(ICP_SUBDIVIDABLE_BY);