diff --git a/e2e/tests-dfx/create.bash b/e2e/tests-dfx/create.bash index bd16462e84..63fdb4b34f 100644 --- a/e2e/tests-dfx/create.bash +++ b/e2e/tests-dfx/create.bash @@ -269,8 +269,3 @@ teardown() { assert_command dfx wallet upgrade --identity alice assert_command dfx canister create --all --controller alice --controller bob --identity alice } - -@test "canister-create on mainnet without wallet does not propagate the 404" { - assert_command_fail dfx deploy --network ic --no-wallet - assert_match 'dfx ledger create-canister' -} diff --git a/e2e/tests-dfx/cycles-ledger.bash b/e2e/tests-dfx/cycles-ledger.bash index 7345d85da7..f015872900 100644 --- a/e2e/tests-dfx/cycles-ledger.bash +++ b/e2e/tests-dfx/cycles-ledger.bash @@ -30,7 +30,7 @@ add_cycles_ledger_canisters_to_project() { } deploy_cycles_ledger() { - assert_command dfx deploy cycles-ledger --specified-id "um5iw-rqaaa-aaaaq-qaaba-cai" + assert_command dfx deploy cycles-ledger --specified-id "um5iw-rqaaa-aaaaq-qaaba-cai" --argument '(variant { Init = record { max_transactions_per_request = 100; index_id = null; } })' assert_command dfx deploy cycles-depositor --argument "(record {ledger_id = principal \"$(dfx canister id cycles-ledger)\"})" --with-cycles 10000000000000 --specified-id "ul4oc-4iaaa-aaaaq-qaabq-cai" } @@ -62,16 +62,16 @@ current_time_nanoseconds() { assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\";};cycles = 1_700_400_200_150;})" --identity cycle-giver - assert_eq "(record { balance = 1_700_400_200_150 : nat; txid = 0 : nat })" + assert_eq "(record { balance = 1_700_400_200_150 : nat; block_index = 0 : nat })" assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\"; subaccount = opt blob \"$ALICE_SUBACCT1_CANDID\"};cycles = 3_750_000_000_000;})" --identity cycle-giver - assert_eq "(record { balance = 3_750_000_000_000 : nat; txid = 1 : nat })" + assert_eq "(record { balance = 3_750_000_000_000 : nat; block_index = 1 : nat })" assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\"; subaccount = opt blob \"$ALICE_SUBACCT2_CANDID\"};cycles = 760_500_000_000;})" --identity cycle-giver - assert_eq "(record { balance = 760_500_000_000 : nat; txid = 2 : nat })" + assert_eq "(record { balance = 760_500_000_000 : nat; block_index = 2 : nat })" assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$BOB\";};cycles = 2_900_000_000_000;})" --identity cycle-giver - assert_eq "(record { balance = 2_900_000_000_000 : nat; txid = 3 : nat })" + assert_eq "(record { balance = 2_900_000_000_000 : nat; block_index = 3 : nat })" assert_command dfx cycles balance --precise --identity alice @@ -389,7 +389,7 @@ current_time_nanoseconds() { assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\";};cycles = 500_000_000;})" --identity cycle-giver - assert_eq "(record { balance = 500_000_000 : nat; txid = 0 : nat })" + assert_eq "(record { balance = 500_000_000 : nat; block_index = 0 : nat })" assert_command dfx canister status cycles-depositor assert_contains "Balance: 9_999_500_000_000 Cycles" @@ -415,3 +415,89 @@ current_time_nanoseconds() { assert_command dfx canister status cycles-depositor assert_contains "Balance: 9_999_500_100_000 Cycles" } + +@test "canister creation" { + skip "can't be properly tested with feature flag turned off (CYCLES_LEDGER_ENABLED). TODO(SDK-1331): re-enable this test" + dfx_new temporary + add_cycles_ledger_canisters_to_project + install_cycles_ledger_canisters + + ALICE=$(dfx identity get-principal --identity alice) + ALICE_SUBACCT1="7C7B7A030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + ALICE_SUBACCT1_CANDID="\7C\7B\7A\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b\1c\1d\1e\1f" + + assert_command deploy_cycles_ledger + CYCLES_LEDGER_ID=$(dfx canister id cycles-ledger) + echo "Cycles ledger deployed at id $CYCLES_LEDGER_ID" + assert_command dfx deploy cycles-depositor --argument "(record {ledger_id = principal \"$(dfx canister id cycles-ledger)\"})" + echo "Cycles depositor deployed at id $(dfx canister id cycles-depositor)" + assert_command dfx ledger fabricate-cycles --canister cycles-depositor --t 9999 + + assert_command dfx deploy + + assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\";};cycles = 13_400_000_000_000;})" --identity cycle-giver + assert_command dfx canister call cycles-depositor deposit "(record {to = record{owner = principal \"$ALICE\"; subaccount = opt blob \"$ALICE_SUBACCT1_CANDID\"};cycles = 2_600_000_000_000;})" --identity cycle-giver + + cd .. + dfx_new + # setup done + + # using dfx canister create + dfx identity use alice + export DFX_DISABLE_AUTO_WALLET=1 + t=$(current_time_nanoseconds) + assert_command dfx canister create e2e_project_backend --with-cycles 1T --created-at-time "$t" + assert_command dfx canister id e2e_project_backend + E2E_PROJECT_BACKEND_CANISTER_ID=$(dfx canister id e2e_project_backend) + assert_command dfx cycles balance --precise + assert_eq "12399900000000 cycles." + # forget about canister. If --created-at-time is a valid idempotency key we should end up with the same canister id + rm .dfx/local/canister_ids.json + assert_command dfx canister create e2e_project_backend --with-cycles 1T --created-at-time "$t" + assert_command dfx canister id e2e_project_backend + assert_contains "$E2E_PROJECT_BACKEND_CANISTER_ID" + assert_command dfx cycles balance --precise + assert_eq "12399900000000 cycles." + dfx canister stop e2e_project_backend + dfx canister delete e2e_project_backend + + assert_command dfx canister create e2e_project_backend --with-cycles 0.5T --from-subaccount "$ALICE_SUBACCT1" + assert_command dfx canister id e2e_project_backend + assert_command dfx cycles balance --subaccount "$ALICE_SUBACCT1" --precise + assert_eq "2099900000000 cycles." + + # reset deployment status + rm -r .dfx + + # using dfx deploy + t=$(current_time_nanoseconds) + assert_command dfx deploy e2e_project_backend --with-cycles 1T --created-at-time "$t" + assert_command dfx canister id e2e_project_backend + E2E_PROJECT_BACKEND_CANISTER_ID=$(dfx canister id e2e_project_backend) + assert_command dfx cycles balance --precise + assert_eq "11399800000000 cycles." + # reset and forget about canister. If --created-at-time is a valid idempotency key we should end up with the same canister id + dfx canister uninstall-code e2e_project_backend + rm .dfx/local/canister_ids.json + assert_command dfx deploy e2e_project_backend --with-cycles 1T --created-at-time "$t" -vv + assert_command dfx canister id e2e_project_backend + assert_contains "$E2E_PROJECT_BACKEND_CANISTER_ID" + assert_command dfx cycles balance --precise + assert_eq "11399800000000 cycles." + dfx canister stop e2e_project_backend + dfx canister delete e2e_project_backend + + assert_command dfx deploy e2e_project_backend --with-cycles 0.5T --from-subaccount "$ALICE_SUBACCT1" + assert_command dfx canister id e2e_project_backend + assert_command dfx cycles balance --subaccount "$ALICE_SUBACCT1" --precise + assert_eq "1599800000000 cycles." + dfx canister stop e2e_project_backend + dfx canister delete e2e_project_backend + + assert_command dfx deploy --with-cycles 1T + assert_command dfx canister id e2e_project_backend + assert_command dfx canister id e2e_project_frontend + assert_not_contains "$(dfx canister id e2e_project_backend)" + assert_command dfx cycles balance --precise + assert_eq "9399600000000 cycles." +} diff --git a/e2e/tests-dfx/error_diagnosis.bash b/e2e/tests-dfx/error_diagnosis.bash index 5713607c9a..cf454be310 100644 --- a/e2e/tests-dfx/error_diagnosis.bash +++ b/e2e/tests-dfx/error_diagnosis.bash @@ -46,6 +46,7 @@ teardown() { } @test "Instruct user to set a wallet" { + skip "TODO(SDK-1331): remove this test" dfx_new hello install_asset greet assert_command dfx identity new alice --storage-mode plaintext diff --git a/e2e/tests-dfx/wallet.bash b/e2e/tests-dfx/wallet.bash index 56f9677204..b2c134f758 100644 --- a/e2e/tests-dfx/wallet.bash +++ b/e2e/tests-dfx/wallet.bash @@ -202,7 +202,7 @@ teardown() { # assert: no wallet configured export DFX_DISABLE_AUTO_WALLET=1 assert_command_fail dfx wallet balance - assert_match "command requires a configured wallet" + assert_match "No wallet configured" assert_command dfx wallet redeem-faucet-coupon --faucet "$(dfx canister id faucet)" 'valid-coupon' assert_match "Redeemed coupon valid-coupon for a new wallet" diff --git a/e2e/utils/cycles-ledger.bash b/e2e/utils/cycles-ledger.bash index 0409faa0e4..0abce204ce 100644 --- a/e2e/utils/cycles-ledger.bash +++ b/e2e/utils/cycles-ledger.bash @@ -1,4 +1,4 @@ -CYCLES_LEDGER_VERSION="0.2.1" +CYCLES_LEDGER_VERSION="0.2.6" build_artifact_url() { echo "https://github.com/dfinity/cycles-ledger/releases/download/cycles-ledger-v$CYCLES_LEDGER_VERSION/${1}" diff --git a/src/dfx/src/commands/canister/create.rs b/src/dfx/src/commands/canister/create.rs index 7793120e69..03e00d58df 100644 --- a/src/dfx/src/commands/canister/create.rs +++ b/src/dfx/src/commands/canister/create.rs @@ -5,14 +5,15 @@ use crate::lib::ic_attributes::{ get_compute_allocation, get_freezing_threshold, get_memory_allocation, get_reserved_cycles_limit, CanisterSettings, }; -use crate::lib::identity::wallet::get_or_create_wallet_canister; +use crate::lib::identity::wallet::{get_or_create_wallet_canister, GetOrCreateWalletCanisterError}; use crate::lib::operations::canister::create_canister; +use crate::lib::operations::cycles_ledger::CYCLES_LEDGER_ENABLED; use crate::lib::root_key::fetch_root_key_if_needed; -use crate::util::clap::parsers::cycle_amount_parser; use crate::util::clap::parsers::{ compute_allocation_parser, freezing_threshold_parser, memory_allocation_parser, reserved_cycles_limit_parser, }; +use crate::util::clap::parsers::{cycle_amount_parser, icrc_subaccount_parser}; use anyhow::{bail, Context}; use byte_unit::Byte; use candid::Principal as CanisterId; @@ -20,7 +21,8 @@ use clap::{ArgAction, Parser}; use dfx_core::error::identity::instantiate_identity_from_name::InstantiateIdentityFromNameError::GetIdentityPrincipalFailed; use dfx_core::identity::CallSender; use ic_agent::Identity as _; -use slog::info; +use icrc_ledger_types::icrc1::account::Subaccount; +use slog::{debug, info}; /// Creates an empty canister and associates the assigned Canister ID to the canister name. #[derive(Parser)] @@ -80,6 +82,17 @@ pub struct CanisterCreateOpts { /// Bypasses the Wallet canister. #[arg(long)] no_wallet: bool, + + /// 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- + //TODO(SDK-1331): unhide + #[arg(long, hide = true, conflicts_with = "all")] + created_at_time: Option, + + /// Subaccount of the selected identity to spend cycles from. + //TODO(SDK-1331): unhide + #[arg(long, value_parser = icrc_subaccount_parser, hide = true)] + from_subaccount: Option, } pub async fn exec( @@ -102,14 +115,30 @@ pub async fn exec( && !matches!(call_sender, CallSender::Wallet(_)) && !network.is_playground() { - let wallet = get_or_create_wallet_canister( + match get_or_create_wallet_canister( env, env.get_network_descriptor(), env.get_selected_identity().expect("No selected identity"), ) - .await?; - proxy_sender = CallSender::Wallet(*wallet.canister_id_()); - call_sender = &proxy_sender; + .await + { + Ok(wallet) => { + proxy_sender = CallSender::Wallet(*wallet.canister_id_()); + call_sender = &proxy_sender; + } + Err(err) => { + if CYCLES_LEDGER_ENABLED + && matches!( + err, + GetOrCreateWalletCanisterError::NoWalletConfigured { .. } + ) + { + debug!(env.get_logger(), "No wallet configured."); + } else { + bail!(err) + } + } + }; } let controllers: Option> = opts @@ -186,6 +215,7 @@ pub async fn exec( with_cycles, opts.specified_id, call_sender, + opts.from_subaccount, CanisterSettings { controllers, compute_allocation, @@ -193,6 +223,7 @@ pub async fn exec( freezing_threshold, reserved_cycles_limit, }, + opts.created_at_time, ) .await?; Ok(()) @@ -254,6 +285,7 @@ pub async fn exec( with_cycles, None, call_sender, + opts.from_subaccount, CanisterSettings { controllers: controllers.clone(), compute_allocation, @@ -261,6 +293,7 @@ pub async fn exec( freezing_threshold, reserved_cycles_limit, }, + opts.created_at_time, ) .await?; } diff --git a/src/dfx/src/commands/deploy.rs b/src/dfx/src/commands/deploy.rs index 5058ecb88f..b546047c57 100644 --- a/src/dfx/src/commands/deploy.rs +++ b/src/dfx/src/commands/deploy.rs @@ -8,7 +8,7 @@ use crate::lib::operations::canister::deploy_canisters::DeployMode::{ }; 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 crate::util::clap::parsers::{cycle_amount_parser, icrc_subaccount_parser}; use anyhow::{anyhow, bail, Context}; use candid::Principal; use clap::Parser; @@ -17,6 +17,7 @@ use dfx_core::config::model::network_descriptor::NetworkDescriptor; use dfx_core::identity::CallSender; use fn_error_context::context; use ic_utils::interfaces::management_canister::builders::InstallMode; +use icrc_ledger_types::icrc1::account::Subaccount; use slog::info; use std::collections::BTreeMap; use std::path::PathBuf; @@ -99,6 +100,17 @@ pub struct DeployOpts { /// Compute evidence and compare it against expected evidence #[arg(long, conflicts_with("by_proposal"))] compute_evidence: bool, + + /// 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- + //TODO(SDK-1331): unhide + #[arg(long, hide = true, requires = "canister_name")] + created_at_time: Option, + + /// Subaccount of the selected identity to spend cycles from. + //TODO(SDK-1331): unhide + #[arg(long, value_parser = icrc_subaccount_parser, hide = true)] + from_subaccount: Option, } pub fn exec(env: &dyn Environment, opts: DeployOpts) -> DfxResult { @@ -167,8 +179,10 @@ pub fn exec(env: &dyn Environment, opts: DeployOpts) -> DfxResult { &deploy_mode, opts.upgrade_unchanged, with_cycles, + opts.created_at_time, opts.specified_id, &call_sender, + opts.from_subaccount, opts.no_wallet, opts.yes, env_file, diff --git a/src/dfx/src/commands/mod.rs b/src/dfx/src/commands/mod.rs index 139ac8ae6c..f8fab66da1 100644 --- a/src/dfx/src/commands/mod.rs +++ b/src/dfx/src/commands/mod.rs @@ -36,6 +36,7 @@ pub enum DfxCommand { Build(build::CanisterBuildOpts), Cache(cache::CacheOpts), Canister(canister::CanisterOpts), + //TODO(SDK-1331): unhide #[command(hide = true)] Cycles(cycles::CyclesOpts), Deploy(deploy::DeployOpts), diff --git a/src/dfx/src/lib/ic_attributes/mod.rs b/src/dfx/src/lib/ic_attributes/mod.rs index 09d495338e..2722283d0a 100644 --- a/src/dfx/src/lib/ic_attributes/mod.rs +++ b/src/dfx/src/lib/ic_attributes/mod.rs @@ -9,7 +9,7 @@ use ic_utils::interfaces::management_canister::attributes::{ }; use std::convert::TryFrom; -#[derive(Default)] +#[derive(Default, Debug, Clone)] pub struct CanisterSettings { pub controllers: Option>, pub compute_allocation: Option, @@ -18,6 +18,32 @@ pub struct CanisterSettings { pub reserved_cycles_limit: Option, } +impl From + for ic_utils::interfaces::management_canister::builders::CanisterSettings +{ + fn from(value: CanisterSettings) -> Self { + Self { + controllers: value.controllers, + compute_allocation: value + .compute_allocation + .map(u8::from) + .map(candid::Nat::from), + memory_allocation: value + .memory_allocation + .map(u64::from) + .map(candid::Nat::from), + freezing_threshold: value + .freezing_threshold + .map(u64::from) + .map(candid::Nat::from), + reserved_cycles_limit: value + .reserved_cycles_limit + .map(u128::from) + .map(candid::Nat::from), + } + } +} + #[context("Failed to get compute allocation.")] pub fn get_compute_allocation( compute_allocation: Option, diff --git a/src/dfx/src/lib/identity/wallet.rs b/src/dfx/src/lib/identity/wallet.rs index d0b3abb454..c1cf51f8e5 100644 --- a/src/dfx/src/lib/identity/wallet.rs +++ b/src/dfx/src/lib/identity/wallet.rs @@ -1,4 +1,3 @@ -use crate::lib::diagnosis::DiagnosedError; use crate::lib::error::DfxResult; use crate::lib::root_key::fetch_root_key_if_needed; use crate::util::assets::wallet_wasm; @@ -8,13 +7,13 @@ use candid::Principal; use dfx_core::canister::build_wallet_canister; use dfx_core::config::directories::get_user_dfx_config_dir; use dfx_core::config::model::network_descriptor::{NetworkDescriptor, NetworkTypeDescriptor}; +use dfx_core::error::canister::CanisterBuilderError; use dfx_core::error::wallet_config::WalletConfigError; use dfx_core::error::wallet_config::WalletConfigError::{ EnsureWalletConfigDirFailed, GetWalletConfigPathFailed, SaveWalletConfigFailed, }; use dfx_core::identity::{Identity, WalletGlobalConfig, WalletNetworkMap, WALLET_CONFIG_FILENAME}; use dfx_core::json::save_json_file; -use fn_error_context::context; use ic_agent::agent::{RejectCode, RejectResponse}; use ic_agent::AgentError; use ic_utils::call::AsyncCall; @@ -23,29 +22,46 @@ use ic_utils::interfaces::{ManagementCanister, WalletCanister}; use slog::info; use std::collections::BTreeMap; use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum GetOrCreateWalletCanisterError { + #[error( + "No wallet configured for combination of identity '{identity}' and network '{network}'" + )] + NoWalletConfigured { identity: String, network: String }, + + #[error("Failed to create wallet: {0}")] + CreationFailed(String), + + #[error(transparent)] + WalletConfigError(#[from] WalletConfigError), + + #[error(transparent)] + CanisterBuilderError(#[from] CanisterBuilderError), +} /// Gets the currently configured wallet canister. If none exists yet and `create` is true, then this creates a new wallet. WARNING: Creating a new wallet costs ICP! /// /// While developing locally, this always creates a new wallet, even if `create` is false. /// This can be inhibited by setting the DFX_DISABLE_AUTO_WALLET env var. -#[context("Failed to get wallet for identity '{}' on network '{}'.", name, network.name)] pub async fn get_or_create_wallet( env: &dyn Environment, network: &NetworkDescriptor, name: &str, -) -> DfxResult { +) -> Result { match wallet_canister_id(network, name)? { None => { // If the network is not the IC, we ignore the error and create a new wallet for the identity. if !network.is_ic && std::env::var("DFX_DISABLE_AUTO_WALLET").is_err() { - create_wallet(env, network, name, None).await + create_wallet(env, network, name, None) + .await + .map_err(|err| GetOrCreateWalletCanisterError::CreationFailed(err.to_string())) } else { - Err(DiagnosedError::new(format!("This command requires a configured wallet, but the combination of identity '{}' and network '{}' has no wallet set.", name, network.name), - "To use an identity with a configured wallet you can do one of the following:\n\ - - Run the command for a network where you have a wallet configured. To do so, add '--network ' to your command.\n\ - - Switch to an identity that has a wallet configured using 'dfx identity use '.\n\ - - Configure a wallet for this identity/network combination: 'dfx identity set-wallet --network '.\n\ - - Or, if you're using mainnet, and you haven't set up a wallet yet: 'dfx quickstart'.".to_string())).context("Wallet not configured.") + Err(GetOrCreateWalletCanisterError::NoWalletConfigured { + identity: name.into(), + network: network.name.to_string(), + }) } } Some(principal) => Ok(principal), @@ -75,7 +91,6 @@ pub fn get_wallet_config_path( }) } -#[context("Failed to create wallet for identity '{}' on network '{}'.", name, network.name)] pub async fn create_wallet( env: &dyn Environment, network: &NetworkDescriptor, @@ -150,19 +165,18 @@ pub async fn create_wallet( /// While developing locally, this always creates a new wallet, even if `create` is false. /// This can be inhibited by setting the DFX_DISABLE_AUTO_WALLET env var. #[allow(clippy::needless_lifetimes)] -#[context("Failed to get wallet canister caller for identity '{}' on network '{}'.", name, network.name)] pub async fn get_or_create_wallet_canister<'env>( env: &'env dyn Environment, network: &NetworkDescriptor, name: &str, -) -> DfxResult> { +) -> Result, GetOrCreateWalletCanisterError> { // without this async block, #[context] gives a spurious error async { let wallet_canister_id = get_or_create_wallet(env, network, name).await?; let agent = env.get_agent(); build_wallet_canister(wallet_canister_id, agent) .await - .map_err(Into::into) + .map_err(GetOrCreateWalletCanisterError::CanisterBuilderError) } .await } diff --git a/src/dfx/src/lib/operations/canister/create_canister.rs b/src/dfx/src/lib/operations/canister/create_canister.rs index 1880100739..7e0f0c0fef 100644 --- a/src/dfx/src/lib/operations/canister/create_canister.rs +++ b/src/dfx/src/lib/operations/canister/create_canister.rs @@ -1,7 +1,8 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; -use crate::lib::ic_attributes::CanisterSettings; +use crate::lib::ic_attributes::CanisterSettings as DfxCanisterSettings; use crate::lib::operations::canister::motoko_playground::reserve_canister_with_playground; +use crate::lib::operations::cycles_ledger::{create_with_cycles_ledger, CYCLES_LEDGER_ENABLED}; use anyhow::{anyhow, bail, Context}; use candid::Principal; use dfx_core::canister::build_wallet_canister; @@ -12,14 +13,15 @@ use ic_agent::agent::{RejectCode, RejectResponse}; use ic_agent::agent_error::HttpErrorPayload; use ic_agent::{Agent, AgentError}; use ic_utils::interfaces::ManagementCanister; +use icrc_ledger_types::icrc1::account::Subaccount; use slog::info; use std::format; // The cycle fee for create request is 0.1T cycles. -const CANISTER_CREATE_FEE: u128 = 100_000_000_000_u128; +pub const CANISTER_CREATE_FEE: u128 = 100_000_000_000_u128; // We do not know the minimum cycle balance a canister should have. // For now create the canister with 3T cycle balance. -const CANISTER_INITIAL_CYCLE_BALANCE: u128 = 3_000_000_000_000_u128; +pub const CANISTER_INITIAL_CYCLE_BALANCE: u128 = 3_000_000_000_000_u128; #[context("Failed to create canister '{}'.", canister_name)] pub async fn create_canister( @@ -28,7 +30,9 @@ pub async fn create_canister( with_cycles: Option, specified_id: Option, call_sender: &CallSender, - settings: CanisterSettings, + from_subaccount: Option, + settings: DfxCanisterSettings, + created_at_time: Option, ) -> DfxResult { let log = env.get_logger(); info!(log, "Creating canister {}...", canister_name); @@ -76,7 +80,23 @@ pub async fn create_canister( let agent = env.get_agent(); let cid = match call_sender { CallSender::SelectedId => { - create_with_management_canister(env, agent, with_cycles, specified_id, settings).await + let auto_wallet_disabled = std::env::var("DFX_DISABLE_AUTO_WALLET").is_ok(); + let ic_network = env.get_network_descriptor().is_ic; + if CYCLES_LEDGER_ENABLED && (ic_network || auto_wallet_disabled) { + create_with_cycles_ledger( + env, + agent, + canister_name, + with_cycles, + from_subaccount, + settings, + created_at_time, + ) + .await + } else { + create_with_management_canister(env, agent, with_cycles, specified_id, settings) + .await + } } CallSender::Wallet(wallet_id) => { create_with_wallet(agent, wallet_id, with_cycles, settings).await @@ -100,7 +120,7 @@ async fn create_with_management_canister( agent: &Agent, with_cycles: Option, specified_id: Option, - settings: CanisterSettings, + settings: DfxCanisterSettings, ) -> DfxResult { let mgr = ManagementCanister::create(agent); let mut builder = mgr @@ -147,7 +167,7 @@ async fn create_with_wallet( agent: &Agent, wallet_id: &Principal, with_cycles: Option, - settings: CanisterSettings, + settings: DfxCanisterSettings, ) -> DfxResult { let wallet = build_wallet_canister(*wallet_id, agent).await?; let cycles = with_cycles.unwrap_or(CANISTER_CREATE_FEE + CANISTER_INITIAL_CYCLE_BALANCE); diff --git a/src/dfx/src/lib/operations/canister/deploy_canisters.rs b/src/dfx/src/lib/operations/canister/deploy_canisters.rs index b8d4cf65c8..ecf1baa015 100644 --- a/src/dfx/src/lib/operations/canister/deploy_canisters.rs +++ b/src/dfx/src/lib/operations/canister/deploy_canisters.rs @@ -4,7 +4,7 @@ use crate::lib::canister_info::CanisterInfo; use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::ic_attributes::CanisterSettings; -use crate::lib::identity::wallet::get_or_create_wallet_canister; +use crate::lib::identity::wallet::{get_or_create_wallet_canister, GetOrCreateWalletCanisterError}; use crate::lib::installers::assets::prepare_assets_for_proposal; use crate::lib::models::canister::CanisterPool; use crate::lib::operations::canister::deploy_canisters::DeployMode::{ @@ -23,6 +23,7 @@ use ic_utils::interfaces::management_canister::attributes::{ ComputeAllocation, FreezingThreshold, MemoryAllocation, ReservedCyclesLimit, }; use ic_utils::interfaces::management_canister::builders::InstallMode; +use icrc_ledger_types::icrc1::account::Subaccount; use slog::info; use std::convert::TryFrom; use std::path::{Path, PathBuf}; @@ -44,8 +45,10 @@ pub async fn deploy_canisters( deploy_mode: &DeployMode, upgrade_unchanged: bool, with_cycles: Option, + created_at_time: Option, specified_id: Option, call_sender: &CallSender, + from_subaccount: Option, no_wallet: bool, skip_consent: bool, env_file: Option, @@ -113,14 +116,22 @@ pub async fn deploy_canisters( { call_sender } else { - let wallet = get_or_create_wallet_canister( + match get_or_create_wallet_canister( env, env.get_network_descriptor(), env.get_selected_identity().expect("No selected identity"), ) - .await?; - proxy_sender = CallSender::Wallet(*wallet.canister_id_()); - &proxy_sender + .await + { + Ok(wallet) => { + proxy_sender = CallSender::Wallet(*wallet.canister_id_()); + &proxy_sender + } + Err(err) => match err { + GetOrCreateWalletCanisterError::NoWalletConfigured { .. } => call_sender, + _ => bail!(err), + }, + } }; register_canisters( env, @@ -129,6 +140,8 @@ pub async fn deploy_canisters( with_cycles, specified_id, create_call_sender, + from_subaccount, + created_at_time, &config, ) .await?; @@ -199,6 +212,8 @@ async fn register_canisters( with_cycles: Option, specified_id: Option, call_sender: &CallSender, + from_subaccount: Option, + created_at_time: Option, config: &Config, ) -> DfxResult { let canisters_to_create = canister_names @@ -255,6 +270,7 @@ async fn register_canisters( with_cycles, specified_id, call_sender, + from_subaccount, CanisterSettings { controllers, compute_allocation, @@ -262,6 +278,7 @@ async fn register_canisters( freezing_threshold, reserved_cycles_limit, }, + created_at_time, ) .await?; } diff --git a/src/dfx/src/lib/operations/canister/mod.rs b/src/dfx/src/lib/operations/canister/mod.rs index 0d7595690e..9e01743f42 100644 --- a/src/dfx/src/lib/operations/canister/mod.rs +++ b/src/dfx/src/lib/operations/canister/mod.rs @@ -1,4 +1,4 @@ -mod create_canister; +pub(crate) mod create_canister; pub(crate) mod deploy_canisters; pub(crate) mod install_canister; pub use create_canister::create_canister; @@ -167,25 +167,7 @@ pub async fn update_settings( MgmtMethod::UpdateSettings.as_ref(), In { canister_id, - settings: CanisterSettings { - controllers: settings.controllers, - compute_allocation: settings - .compute_allocation - .map(u8::from) - .map(candid::Nat::from), - memory_allocation: settings - .memory_allocation - .map(u64::from) - .map(candid::Nat::from), - freezing_threshold: settings - .freezing_threshold - .map(u64::from) - .map(candid::Nat::from), - reserved_cycles_limit: settings - .reserved_cycles_limit - .map(u128::from) - .map(candid::Nat::from), - }, + settings: settings.into(), }, call_sender, 0, diff --git a/src/dfx/src/lib/operations/cycles_ledger.rs b/src/dfx/src/lib/operations/cycles_ledger.rs index f0be18bac3..cc2cb45cce 100644 --- a/src/dfx/src/lib/operations/cycles_ledger.rs +++ b/src/dfx/src/lib/operations/cycles_ledger.rs @@ -1,21 +1,38 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + use crate::lib::cycles_ledger_types; use crate::lib::cycles_ledger_types::send::SendError; +use crate::lib::environment::Environment; use crate::lib::error::DfxResult; +use crate::lib::ic_attributes::CanisterSettings as DfxCanisterSettings; +use crate::lib::operations::canister::create_canister::{ + CANISTER_CREATE_FEE, CANISTER_INITIAL_CYCLE_BALANCE, +}; use crate::lib::retryable::retryable; -use anyhow::anyhow; +use anyhow::{anyhow, bail}; use backoff::future::retry; use backoff::ExponentialBackoff; -use candid::{Nat, Principal}; +use candid::{CandidType, Decode, Encode, Nat, Principal}; +use fn_error_context::context; use ic_agent::Agent; use ic_utils::call::SyncCall; +use ic_utils::interfaces::management_canister::builders::CanisterSettings; use ic_utils::Canister; use icrc_ledger_types::icrc1; +use icrc_ledger_types::icrc1::account::Subaccount; use icrc_ledger_types::icrc1::transfer::{BlockIndex, TransferError}; +use serde::Deserialize; use slog::{info, Logger}; +use thiserror::Error; + +/// Cycles ledger feature flag to turn off behavior that would be confusing while cycles ledger is not enabled yet. +//TODO(SDK-1331): feature flag can be removed +pub const CYCLES_LEDGER_ENABLED: bool = false; const ICRC1_BALANCE_OF_METHOD: &str = "icrc1_balance_of"; const ICRC1_TRANSFER_METHOD: &str = "icrc1_transfer"; const SEND_METHOD: &str = "send"; +const CREATE_CANISTER_METHOD: &str = "create_canister"; const CYCLES_LEDGER_CANISTER_ID: Principal = Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x02, 0x01, 0x01]); @@ -170,6 +187,139 @@ pub async fn send( Ok(block_index) } +#[context("Failed to create canister via cycles ledger.")] +pub async fn create_with_cycles_ledger( + env: &dyn Environment, + agent: &Agent, + canister_name: &str, + with_cycles: Option, + from_subaccount: Option, + settings: DfxCanisterSettings, + created_at_time: Option, +) -> DfxResult { + #[derive(CandidType, Clone, Debug)] + // TODO(FI-1022): Import types from cycles ledger crate once available + struct CreateCanisterArgs { + pub from_subaccount: Option, + pub created_at_time: Option, + pub amount: u128, + pub creation_args: Option, + } + #[derive(CandidType, Clone, Debug)] + struct CmcCreateCanisterArgs { + pub subnet_selection: Option, + pub settings: Option, + } + #[derive(CandidType, Clone, Debug)] + #[allow(dead_code)] + enum SubnetSelection { + /// Choose a random subnet that satisfies the specified properties + Filter(SubnetFilter), + /// Choose a specific subnet + Subnet { subnet: Principal }, + } + #[derive(CandidType, Clone, Debug)] + struct SubnetFilter { + pub subnet_type: Option, + } + #[derive(CandidType, Clone, Debug, Deserialize, Error)] + enum CreateCanisterError { + #[error("Insufficient funds. Current balance: {balance}")] + InsufficientFunds { balance: u128 }, + #[error("Local clock too far behind.")] + TooOld, + #[error("Local clock too far ahead.")] + CreatedInFuture { ledger_time: u64 }, + #[error("Cycles ledger temporarily unavailable.")] + TemporarilyUnavailable, + #[error("Duplicate of block {duplicate_of}.")] + Duplicate { + duplicate_of: Nat, + canister_id: Option, + }, + #[error("Cycles ledger failed to create canister: {error}")] + FailedToCreate { + fee_block: Option, + refund_block: Option, + error: String, + }, + #[error("Ledger error {error_code}: {message}")] + GenericError { error_code: Nat, message: String }, + } + #[derive(Deserialize, CandidType, Clone, Debug, PartialEq, Eq)] + struct CreateCanisterSuccess { + pub block_id: Nat, + pub canister_id: Principal, + } + + let cycles = with_cycles.unwrap_or(CANISTER_CREATE_FEE + CANISTER_INITIAL_CYCLE_BALANCE); + let created_at_time = created_at_time.or_else(|| { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64; + info!( + env.get_logger(), + "created-at-time for canister {canister_name} is {now}." + ); + Some(now) + }); + + let result = loop { + match agent + .update(&CYCLES_LEDGER_CANISTER_ID, CREATE_CANISTER_METHOD) + .with_arg( + Encode!(&CreateCanisterArgs { + from_subaccount, + created_at_time, + amount: cycles, + creation_args: Some(CmcCreateCanisterArgs { + settings: Some(settings.clone().into()), + subnet_selection: None, + }), + }) + .unwrap(), + ) + .call_and_wait() + .await + { + Ok(result) => break result, + Err(err) => { + if retryable(&err) { + info!(env.get_logger(), "Request error: {err:?}. Retrying..."); + } else { + bail!(err) + } + } + } + }; + let create_result = Decode!( + &result, + Result + ) + .map_err(|err| { + anyhow!( + "Failed to decode cycles ledger response: {}", + err.to_string() + ) + })?; + match create_result { + Ok(result) => Ok(result.canister_id), + Err(CreateCanisterError::Duplicate { + duplicate_of, + canister_id, + }) => { + if let Some(canister) = canister_id { + info!(env.get_logger(), "Duplicate of block {duplicate_of}. Canister already created with id {canister}."); + Ok(canister) + } else { + bail!("Duplicate of block {duplicate_of} but no canister id is available."); + } + } + Err(err) => bail!(err), + } +} + #[test] fn ledger_canister_id_text_representation() { assert_eq!( diff --git a/src/dfx/src/util/clap/parsers.rs b/src/dfx/src/util/clap/parsers.rs index 9b7184dffc..46918b0772 100644 --- a/src/dfx/src/util/clap/parsers.rs +++ b/src/dfx/src/util/clap/parsers.rs @@ -1,4 +1,5 @@ use byte_unit::{Byte, ByteUnit}; +use icrc_ledger_types::icrc1::account::Subaccount; use rust_decimal::Decimal; use std::{path::PathBuf, str::FromStr}; @@ -160,6 +161,14 @@ pub fn project_name_parser(name: &str) -> Result { } } +pub fn icrc_subaccount_parser(subaccount: &str) -> Result { + if let Ok(Ok(subaccount)) = hex::decode(subaccount).map(|bytes| bytes.try_into()) { + return Ok(subaccount); + } + + Err("Failed to parse subaccount. Expected 32 bytes of hex-encoded data.".to_string()) +} + pub fn hsm_key_id_parser(key_id: &str) -> Result { if key_id.len() % 2 != 0 { Err("Key id must consist of an even number of hex digits".to_string())