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
11 changes: 10 additions & 1 deletion e2e/tests-dfx/fabricate_cycles.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
}
95 changes: 77 additions & 18 deletions src/dfx/src/commands/ledger/fabricate_cycles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

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

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

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

/// Specifies the amount of trillion cycles to fabricate.
#[clap(
long,
validator(trillion_cycle_amount_validator),
Expand All @@ -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<String>,

/// Deposit cycles to all of the canisters configured in the dfx.json file.
Expand All @@ -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?;

Expand All @@ -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::<u128>().unwrap()
async fn cycles_to_fabricate(env: &dyn Environment, opts: &FabricateCyclesOpts) -> DfxResult<u128> {
if let Some(cycles_str) = &opts.cycles {
//cycles_str is validated by cycle_amount_validator. Therefore unwrap is safe
Ok(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)
//t_cycles_str is validated by trillion_cycle_amount_validator. Therefore unwrap is safe
Ok(format!("{}000000000000", t_cycles_str)
.parse::<u128>()
.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)
}
}
12 changes: 9 additions & 3 deletions src/dfx/src/lib/root_key.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(())
}
Expand All @@ -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!(
Expand Down
2 changes: 1 addition & 1 deletion src/dfx/src/util/clap/validators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ 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())
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> {
Expand Down
64 changes: 64 additions & 0 deletions src/dfx/src/util/currency_conversion.rs
Original file line number Diff line number Diff line change
@@ -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<u128> {
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<u8>,
pub certificate: Vec<u8>,
}
#[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::<u128>::Ok(cycles_per_icp)
}.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);
let total_cycles = cycles_per_icp * u128::from(icpts.get_icpts())
+ cycles_per_e8s * u128::from(icpts.get_remainder_e8s());
Ok(total_cycles)
}
1 change: 1 addition & 0 deletions src/dfx/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down