diff --git a/e2e/tests-dfx/ledger.bash b/e2e/tests-dfx/ledger.bash index 9b5bc0a4f9..73509680a1 100644 --- a/e2e/tests-dfx/ledger.bash +++ b/e2e/tests-dfx/ledger.bash @@ -29,6 +29,36 @@ current_time_nanoseconds() { echo "$(date +%s)"000000000 } +@test "deploy icp retry options" { + dfx identity use alice + dfx ledger balance + dfx ledger account-id + dfx_new + assert_command_fail dfx deploy --using-icp-amount 1 --retry-create-canister-name abc + assert_contains "error: the following required arguments were not provided" + assert_contains "<--retry-create-canister-transfer-created-at-time |--retry-create-canister-notify-block-height >" + + assert_command_fail dfx deploy --using-icp-amount 1 --retry-create-canister-transfer-created-at-time 777 + assert_contains "error: the following required arguments were not provided" + assert_contains "<--retry-create-canister-transfer-created-at-time |--retry-create-canister-notify-block-height >" + + assert_command_fail dfx deploy --using-icp-amount 1 --retry-create-canister-notify-block-height 87 + assert_contains "ABC" + assert_contains "error: the following required arguments were not provided" + assert_contains "--retry-create-canister-name " +} + +@test "deploy --using-icp-amount" { + dfx identity use alice + dfx ledger balance + dfx ledger account-id + dfx_new + assert_command dfx deploy --using-icp-amount 1 + assert_contains "Using transfer at block height" # todo + assert_command dfx ledger balance + assert_eq "999999997.99980000 ICP" +} + @test "ledger account-id" { dfx identity use alice assert_command dfx ledger account-id diff --git a/scripts/workflows/e2e-matrix.py b/scripts/workflows/e2e-matrix.py index c8d683bc8c..f079c925dd 100755 --- a/scripts/workflows/e2e-matrix.py +++ b/scripts/workflows/e2e-matrix.py @@ -14,7 +14,7 @@ def test_scripts(prefix): test = sorted(test_scripts("dfx") + test_scripts("replica") + test_scripts("icx-asset")) matrix = { - "test": test, + "test": ["dfx/ledger"], "backend": ["replica"], "os": ["macos-12", "ubuntu-20.04"], "exclude": [ diff --git a/src/dfx/src/commands/canister/create.rs b/src/dfx/src/commands/canister/create.rs index 84df896632..28ff893e15 100644 --- a/src/dfx/src/commands/canister/create.rs +++ b/src/dfx/src/commands/canister/create.rs @@ -5,7 +5,7 @@ use crate::lib::ic_attributes::{ get_compute_allocation, get_freezing_threshold, get_memory_allocation, CanisterSettings, }; use crate::lib::identity::wallet::get_or_create_wallet_canister; -use crate::lib::operations::canister::create_canister; +use crate::lib::operations::canister::{create_canister, Funding}; use crate::lib::root_key::fetch_root_key_if_needed; use crate::util::clap::parsers::cycle_amount_parser; use crate::util::clap::parsers::{ @@ -77,7 +77,7 @@ pub async fn exec( fetch_root_key_if_needed(env).await?; - let with_cycles = opts.with_cycles; + let funding = Funding::MaybeCycles(opts.with_cycles); let config_interface = config.get_config(); let network = env.get_network_descriptor(); @@ -163,7 +163,7 @@ pub async fn exec( create_canister( env, canister_name, - with_cycles, + &funding, opts.specified_id, call_sender, CanisterSettings { @@ -222,7 +222,7 @@ pub async fn exec( create_canister( env, canister_name, - with_cycles, + &funding, None, call_sender, CanisterSettings { diff --git a/src/dfx/src/commands/deploy.rs b/src/dfx/src/commands/deploy.rs index 7a429dcd85..b92dff6c44 100644 --- a/src/dfx/src/commands/deploy.rs +++ b/src/dfx/src/commands/deploy.rs @@ -1,17 +1,22 @@ use crate::lib::agent::create_agent_environment; use crate::lib::canister_info::CanisterInfo; use crate::lib::error::DfxResult; +use crate::lib::ledger_types::BlockHeight; use crate::lib::network::network_opt::NetworkOpt; -use crate::lib::operations::canister::deploy_canisters::deploy_canisters; -use crate::lib::operations::canister::deploy_canisters::DeployMode::{ +use crate::lib::nns_types::account_identifier::Subaccount; +use crate::lib::nns_types::icpts::ICPTs; +use crate::lib::operations::canister::DeployMode::{ ComputeEvidence, ForceReinstallSingleCanister, NormalDeploy, PrepareForProposal, }; +use crate::lib::operations::canister::{ + deploy_canisters, Funding, ICPFunding, ICPFundingRetry, ICPFundingRetryPhase, +}; use crate::lib::root_key::fetch_root_key_if_needed; use crate::lib::{environment::Environment, named_canister}; use crate::util::clap::parsers::cycle_amount_parser; use anyhow::{anyhow, bail, Context}; use candid::Principal; -use clap::Parser; +use clap::{ArgGroup, Parser}; use console::Style; use dfx_core::config::model::network_descriptor::NetworkDescriptor; use dfx_core::identity::CallSender; @@ -29,6 +34,10 @@ const MAINNET_CANDID_INTERFACE_PRINCIPAL: &str = "a4gq6-oaaaa-aaaab-qaa4q-cai"; /// Deploys all or a specific canister from the code in your project. By default, all canisters are deployed. #[derive(Parser)] +#[clap( +group(ArgGroup::new("using_icp_group").multiple(false).required(false)), +group(ArgGroup::new("icp_retry").multiple(false).required(false)), +)] pub struct DeployOpts { /// Specifies the name of the canister you want to deploy. /// If you don’t specify a canister name, all canisters defined in the dfx.json file are deployed. @@ -59,7 +68,7 @@ pub struct DeployOpts { /// Specifies the initial cycle balance to deposit into the newly created canister. /// The specified amount needs to take the canister create fee into account. /// This amount is deducted from the wallet's cycle balance. - #[arg(long, value_parser = cycle_amount_parser)] + #[arg(long, value_parser = cycle_amount_parser, conflicts_with("using_icp_group"))] with_cycles: Option, /// Attempts to create the canister with this Canister ID. @@ -99,6 +108,41 @@ pub struct DeployOpts { /// Compute evidence and compare it against expected evidence #[arg(long, conflicts_with("by_proposal"))] compute_evidence: bool, + + // /// Group to enforce at most one of --using-icp-amount or --using-icp + // #[clap(group = ArgGroup::with_name("using_icp_group").required(false).multiple(false))] + // using_icp_group: bool, + /// 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 + /// This option implies the --no-wallet flag. + #[arg(long, group = "using_icp_group", conflicts_with("with_cycles"))] + using_icp_amount: Option, + + /// Mint ICP into cycles and deposit into destination canister + /// This option implies the --no-wallet flag. + #[arg(long, group = "using_icp_group", conflicts_with("with_cycles"))] + using_icp: bool, + + /// Subaccount to withdraw from + #[arg(long, requires("using_icp_group"))] + icp_from_subaccount: Option, + + /// Transaction fee, default is 10000 e8s. + #[arg(long, requires("using_icp_group"))] + icp_fee: Option, + + /// Resume creation of this canister + #[arg(long, requires("using_icp_group"), requires("icp_retry"))] + retry_create_canister_name: Option, + + /// Transaction timestamp, in nanoseconds, for use in controlling transaction-deduplication, default is system-time. // https://internetcomputer.org/docs/current/developer-docs/integrations/icrc-1/#transaction-deduplication- + #[arg(long, group = "icp_retry", requires("retry_create_canister_name"))] + retry_create_canister_transfer_created_at_time: Option, + + /// Block height at which transaction took place + #[arg(long, group = "icp_retry", requires("retry_create_canister_name"))] + retry_create_canister_notify_block_height: Option, } pub fn exec(env: &dyn Environment, opts: DeployOpts) -> DfxResult { @@ -119,7 +163,10 @@ pub fn exec(env: &dyn Environment, opts: DeployOpts) -> DfxResult { .output_env_file .or_else(|| config.get_config().output_env_file.clone()); - let with_cycles = opts.with_cycles; + let no_wallet = opts.no_wallet + || opts.specified_id.is_some() + || opts.using_icp_amount.is_some() + || opts.using_icp; let deploy_mode = match (mode, canister_name) { (Some(InstallMode::Reinstall), Some(canister_name)) => { @@ -156,6 +203,24 @@ pub fn exec(env: &dyn Environment, opts: DeployOpts) -> DfxResult { let runtime = Runtime::new().expect("Unable to create a runtime"); + let funding = if opts.using_icp || opts.using_icp_amount.is_some() { + // as_cycles_with_current_exchange_rate ? + let amount = opts + .using_icp_amount + .unwrap_or_else(|| ICPTs::from_icpts(1).unwrap()); + + Funding::Icp(ICPFunding { + amount, + from_subaccount: opts.icp_from_subaccount, + fee: opts.icp_fee, + retry_canister_name: opts.retry_create_canister_name, + retry_transfer_created_at_time: opts.retry_create_canister_transfer_created_at_time, + retry_notify_block_height: opts.retry_create_canister_notify_block_height, + }) + } else { + Funding::MaybeCycles(opts.with_cycles) + }; + let call_sender = CallSender::from(&opts.wallet) .map_err(|e| anyhow!("Failed to determine call sender: {}", e))?; @@ -168,10 +233,10 @@ pub fn exec(env: &dyn Environment, opts: DeployOpts) -> DfxResult { argument_type, &deploy_mode, opts.upgrade_unchanged, - with_cycles, + &funding, opts.specified_id, &call_sender, - opts.no_wallet, + no_wallet, opts.yes, env_file, opts.no_asset_upgrade, diff --git a/src/dfx/src/commands/ledger/create_canister.rs b/src/dfx/src/commands/ledger/create_canister.rs index 8d0fc6606d..f2a57620c4 100644 --- a/src/dfx/src/commands/ledger/create_canister.rs +++ b/src/dfx/src/commands/ledger/create_canister.rs @@ -6,15 +6,13 @@ use crate::lib::ledger_types::Memo; use crate::lib::ledger_types::NotifyError::Refunded; use crate::lib::nns_types::account_identifier::Subaccount; use crate::lib::nns_types::icpts::{ICPTs, TRANSACTION_FEE}; -use crate::lib::operations::cmc::{notify_create, transfer_cmc}; +use crate::lib::operations::cmc::{notify_create, transfer_cmc, MEMO_CREATE_CANISTER}; use crate::lib::root_key::fetch_root_key_if_needed; use crate::util::clap::parsers::e8s_parser; use anyhow::{anyhow, bail, Context}; use candid::Principal; use clap::Parser; -pub const MEMO_CREATE_CANISTER: u64 = 1095062083_u64; - /// Create a canister from ICP #[derive(Parser)] pub struct CreateCanisterOpts { diff --git a/src/dfx/src/commands/quickstart.rs b/src/dfx/src/commands/quickstart.rs index 2059cb0713..e24f160ad2 100644 --- a/src/dfx/src/commands/quickstart.rs +++ b/src/dfx/src/commands/quickstart.rs @@ -1,7 +1,7 @@ use crate::lib::error::NotifyCreateCanisterError::Notify; use crate::lib::ledger_types::NotifyError::Refunded; +use crate::lib::operations::cmc::MEMO_CREATE_CANISTER; use crate::{ - commands::ledger::create_canister::MEMO_CREATE_CANISTER, lib::{ agent::create_agent_environment, environment::Environment, diff --git a/src/dfx/src/lib/operations/canister/create_canister.rs b/src/dfx/src/lib/operations/canister/create_canister.rs index 1d6ee09171..33d9da18ed 100644 --- a/src/dfx/src/lib/operations/canister/create_canister.rs +++ b/src/dfx/src/lib/operations/canister/create_canister.rs @@ -1,7 +1,16 @@ +use crate::lib::diagnosis::DiagnosedError; use crate::lib::environment::Environment; use crate::lib::error::DfxResult; +use crate::lib::error::NotifyCreateCanisterError::Notify; use crate::lib::ic_attributes::CanisterSettings; +use crate::lib::ledger_types::{Memo, NotifyError}; +use crate::lib::nns_types::icpts::{ICPTs, TRANSACTION_FEE}; +use crate::lib::operations::canister::deploy_canisters::{ + ICPFunding, ICPFundingRetry, ICPFundingRetryPhase, +}; use crate::lib::operations::canister::motoko_playground::reserve_canister_with_playground; +use crate::lib::operations::canister::Funding; +use crate::lib::operations::cmc::{notify_create, transfer_cmc, MEMO_CREATE_CANISTER}; use anyhow::{anyhow, bail, Context}; use candid::Principal; use dfx_core::canister::build_wallet_canister; @@ -25,7 +34,7 @@ const CANISTER_INITIAL_CYCLE_BALANCE: u128 = 3_000_000_000_000_u128; pub async fn create_canister( env: &dyn Environment, canister_name: &str, - with_cycles: Option, + funding: &Funding, specified_id: Option, call_sender: &CallSender, settings: CanisterSettings, @@ -76,12 +85,18 @@ pub async fn create_canister( let agent = env .get_agent() .ok_or_else(|| anyhow!("Cannot get HTTP client from environment."))?; - let cid = match call_sender { - CallSender::SelectedId => { - create_with_management_canister(env, agent, with_cycles, specified_id, settings).await + let cid = match (call_sender, funding) { + (CallSender::SelectedId, Funding::Icp(icp_funding)) => { + create_with_ledger(agent, canister_name, icp_funding, settings).await + } + (CallSender::SelectedId, Funding::MaybeCycles(cycles)) => { + create_with_management_canister(env, agent, *cycles, specified_id, settings).await + } + (CallSender::Wallet(wallet_id), Funding::MaybeCycles(cycles)) => { + create_with_wallet(agent, wallet_id, *cycles, settings).await } - CallSender::Wallet(wallet_id) => { - create_with_wallet(agent, wallet_id, with_cycles, settings).await + (CallSender::Wallet(_), Funding::Icp(_)) => { + unreachable!("Cannot create canister with wallet and ICP at the same time.") } }?; let canister_id = cid.to_text(); @@ -97,6 +112,61 @@ pub async fn create_canister( Ok(()) } +async fn create_with_ledger( + agent: &Agent, + canister_name: &str, + funding: &ICPFunding, + _settings: CanisterSettings, +) -> DfxResult { + let to_principal = agent.get_principal().unwrap(); + + let canister_name_matches = matches!(&funding.retry_canister_name, Some(canister_name)); + + let retry_block_height = funding + .retry_notify_block_height + .as_ref() + .filter(|_| canister_name_matches); + + let height = if let Some(height) = retry_block_height { + *height + } else { + let retry_transfer_created_at_time = funding + .retry_transfer_created_at_time + .as_ref() + .filter(|_| canister_name_matches) + .copied(); + let fee = TRANSACTION_FEE; + let memo = Memo(MEMO_CREATE_CANISTER); + let amount = funding.amount; + let from_subaccount = funding.from_subaccount; + let height = transfer_cmc( + agent, + memo, + amount, + fee, + from_subaccount, + to_principal, + retry_transfer_created_at_time, + ) + .await?; + println!("Using transfer at block height {height}"); + height + }; + + let controller = to_principal; + let subnet_type = None; + + let principal = notify_create(agent, controller, height, subnet_type) + .await + .map_err(|e| { + DiagnosedError::new( + format!("Failed to notify cmc: {}", e), + format!("Re-run the command with --icp-block-height {}", height), + ) + })?; + Ok(principal) +} + async fn create_with_management_canister( env: &dyn Environment, agent: &Agent, diff --git a/src/dfx/src/lib/operations/canister/deploy_canisters.rs b/src/dfx/src/lib/operations/canister/deploy_canisters.rs index 7984f6cb76..91eaa95e39 100644 --- a/src/dfx/src/lib/operations/canister/deploy_canisters.rs +++ b/src/dfx/src/lib/operations/canister/deploy_canisters.rs @@ -6,7 +6,10 @@ use crate::lib::error::DfxResult; use crate::lib::ic_attributes::CanisterSettings; use crate::lib::identity::wallet::get_or_create_wallet_canister; use crate::lib::installers::assets::prepare_assets_for_proposal; +use crate::lib::ledger_types::BlockHeight; use crate::lib::models::canister::CanisterPool; +use crate::lib::nns_types::account_identifier::Subaccount; +use crate::lib::nns_types::icpts::ICPTs; use crate::lib::operations::canister::deploy_canisters::DeployMode::{ ComputeEvidence, ForceReinstallSingleCanister, NormalDeploy, PrepareForProposal, }; @@ -35,6 +38,34 @@ pub enum DeployMode { ComputeEvidence(String), } +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum ICPFundingRetryPhase { + Transfer(u64), + Notify(BlockHeight), +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct ICPFundingRetry { + pub canister_name: String, + pub phase: ICPFundingRetryPhase, +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct ICPFunding { + pub amount: ICPTs, + pub from_subaccount: Option, + pub fee: Option, + pub retry_canister_name: Option, + pub retry_transfer_created_at_time: Option, + pub retry_notify_block_height: Option, +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum Funding { + Icp(ICPFunding), + MaybeCycles(Option), +} + #[context("Failed while trying to deploy canisters.")] pub async fn deploy_canisters( env: &dyn Environment, @@ -43,7 +74,7 @@ pub async fn deploy_canisters( argument_type: Option<&str>, deploy_mode: &DeployMode, upgrade_unchanged: bool, - with_cycles: Option, + funding: &Funding, specified_id: Option, call_sender: &CallSender, no_wallet: bool, @@ -126,7 +157,7 @@ pub async fn deploy_canisters( env, &canisters_to_load, &initial_canister_id_store, - with_cycles, + funding, specified_id, create_call_sender, &config, @@ -196,7 +227,7 @@ async fn register_canisters( env: &dyn Environment, canister_names: &[String], canister_id_store: &CanisterIdStore, - with_cycles: Option, + funding: &Funding, specified_id: Option, call_sender: &CallSender, config: &Config, @@ -246,7 +277,7 @@ async fn register_canisters( create_canister( env, canister_name, - with_cycles, + funding, specified_id, call_sender, CanisterSettings { diff --git a/src/dfx/src/lib/operations/canister/mod.rs b/src/dfx/src/lib/operations/canister/mod.rs index 1107c293b3..c89b7b72a2 100644 --- a/src/dfx/src/lib/operations/canister/mod.rs +++ b/src/dfx/src/lib/operations/canister/mod.rs @@ -13,7 +13,9 @@ use candid::CandidType; use candid::Principal as CanisterId; use candid::Principal; pub use deploy_canisters::deploy_canisters; -pub use deploy_canisters::DeployMode; +pub use deploy_canisters::{ + DeployMode, Funding, ICPFunding, ICPFundingRetry, ICPFundingRetryPhase, +}; use dfx_core::canister::build_wallet_canister; pub use dfx_core::canister::install_canister_wasm; use dfx_core::identity::CallSender; diff --git a/src/dfx/src/lib/operations/cmc.rs b/src/dfx/src/lib/operations/cmc.rs index 5db9b608ae..9f65fb9557 100644 --- a/src/dfx/src/lib/operations/cmc.rs +++ b/src/dfx/src/lib/operations/cmc.rs @@ -12,6 +12,8 @@ use ic_agent::Agent; const NOTIFY_CREATE_CANISTER_METHOD: &str = "notify_create_canister"; const NOTIFY_TOP_UP_METHOD: &str = "notify_top_up"; +pub const MEMO_CREATE_CANISTER: u64 = 1095062083_u64; + pub async fn transfer_cmc( agent: &Agent, memo: Memo,