diff --git a/e2e/tests-dfx/cycles-ledger.bash b/e2e/tests-dfx/cycles-ledger.bash index f015872900..0d12dd70a0 100644 --- a/e2e/tests-dfx/cycles-ledger.bash +++ b/e2e/tests-dfx/cycles-ledger.bash @@ -444,6 +444,7 @@ current_time_nanoseconds() { # using dfx canister create dfx identity use alice + # shellcheck disable=SC2030 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" @@ -501,3 +502,46 @@ current_time_nanoseconds() { assert_command dfx cycles balance --precise assert_eq "9399600000000 cycles." } + +@test "canister deletion" { + 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) + + 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 = 22_400_000_000_000;})" --identity cycle-giver + + cd .. + dfx_new + # setup done + + dfx identity use alice + # shellcheck disable=SC2031 + export DFX_DISABLE_AUTO_WALLET=1 + assert_command dfx canister create --all --with-cycles 10T + assert_command dfx cycles balance --precise + assert_eq "2399800000000 cycles." + + # delete by name + assert_command dfx canister stop --all + assert_command dfx canister delete e2e_project_backend + assert_command dfx cycles balance + assert_eq "12.389 TC (trillion cycles)." + + # delete by id + FRONTEND_ID=$(dfx canister id e2e_project_frontend) + rm .dfx/local/canister_ids.json + assert_command dfx canister stop "${FRONTEND_ID}" + assert_command dfx canister delete "${FRONTEND_ID}" + assert_command dfx cycles balance + assert_eq "22.379 TC (trillion cycles)." +} \ No newline at end of file diff --git a/e2e/tests-dfx/deps.bash b/e2e/tests-dfx/deps.bash index 540aa1329a..5ef85acc16 100644 --- a/e2e/tests-dfx/deps.bash +++ b/e2e/tests-dfx/deps.bash @@ -127,7 +127,7 @@ Failed to download from url: http://example.com/c.wasm." cd ../onchain dfx canister stop a - dfx canister delete a + dfx canister delete a --no-withdrawal cd ../app assert_command_fail dfx deps pull --network local @@ -325,11 +325,11 @@ candid:args => (nat)" # delete onchain canisters so that the replica has no canisters as a clean local replica cd ../onchain dfx canister stop a - dfx canister delete a + dfx canister delete a --no-withdrawal dfx canister stop b - dfx canister delete b + dfx canister delete b --no-withdrawal dfx canister stop c - dfx canister delete c + dfx canister delete c --no-withdrawal cd ../app assert_command dfx deps init # b is set here @@ -355,10 +355,10 @@ Installing canister: $CANISTER_ID_C (dep_c)" # deployed pull dependencies can be stopped and deleted assert_command dfx canister stop dep_b --identity anonymous - assert_command dfx canister delete dep_b --identity anonymous + assert_command dfx canister delete dep_b --identity anonymous --no-withdrawal assert_command dfx canister stop $CANISTER_ID_A --identity anonymous - assert_command dfx canister delete $CANISTER_ID_A --identity anonymous + assert_command dfx canister delete $CANISTER_ID_A --identity anonymous --no-withdrawal # error cases ## set wrong init argument @@ -397,11 +397,11 @@ Installing canister: $CANISTER_ID_C (dep_c)" # delete onchain canisters so that the replica has no canisters as a clean local replica cd ../onchain dfx canister stop a - dfx canister delete a + dfx canister delete a --no-withdrawal dfx canister stop b - dfx canister delete b + dfx canister delete b --no-withdrawal dfx canister stop c - dfx canister delete c + dfx canister delete c --no-withdrawal cd ../app assert_command_fail dfx canister create dep_b @@ -434,7 +434,7 @@ Installing canister: $CANISTER_ID_C (dep_c)" # start a clean local replica dfx canister stop app - dfx canister delete app + dfx canister delete app --no-withdrawal assert_command dfx deploy # only deploy app canister } @@ -443,8 +443,8 @@ Installing canister: $CANISTER_ID_C (dep_c)" # verify the help message assert_command dfx deps pull -h - assert_contains "Pull canisters upon which the project depends. This command connects to the \"ic\" mainnet by default. -You can still choose other network by setting \`--network\`" + assert_contains "Pull canisters upon which the project depends. This command connects to the \"ic\" mainnet by default." + assert_contains "You can still choose other network by setting \`--network\`" assert_command dfx deps pull assert_contains "There are no pull dependencies defined in dfx.json" diff --git a/src/dfx/src/commands/canister/delete.rs b/src/dfx/src/commands/canister/delete.rs index cb29aff404..af19f9937c 100644 --- a/src/dfx/src/commands/canister/delete.rs +++ b/src/dfx/src/commands/canister/delete.rs @@ -7,10 +7,14 @@ use crate::lib::operations::canister; use crate::lib::operations::canister::{ deposit_cycles, start_canister, stop_canister, update_settings, }; +use crate::lib::operations::cycles_ledger::{ + wallet_deposit_to_cycles_ledger, CYCLES_LEDGER_ENABLED, +}; use crate::lib::root_key::fetch_root_key_if_needed; use crate::util::assets::wallet_wasm; use crate::util::blob_from_arguments; -use anyhow::Context; +use crate::util::clap::parsers::icrc_subaccount_parser; +use anyhow::{bail, Context}; use candid::Principal; use clap::Parser; use dfx_core::canister::build_wallet_canister; @@ -23,6 +27,7 @@ use ic_utils::interfaces::management_canister::builders::InstallMode; use ic_utils::interfaces::management_canister::CanisterStatus; use ic_utils::interfaces::ManagementCanister; use ic_utils::Argument; +use icrc_ledger_types::icrc1::account::{Account, Subaccount}; use num_traits::cast::ToPrimitive; use slog::info; use std::convert::TryFrom; @@ -71,6 +76,11 @@ pub struct CanisterDeleteOpts { /// Auto-confirm deletion for a non-stopped canister. #[arg(long, short)] yes: bool, + + /// Subaccount of the selected identity to deposit cycles to. + //TODO(SDK-1331): unhide + #[arg(long, value_parser = icrc_subaccount_parser, hide = true)] + to_subaccount: Option, } #[context("Failed to delete canister '{}'.", canister)] @@ -83,6 +93,7 @@ async fn delete_canister( withdraw_cycles_to_canister: Option, withdraw_cycles_to_dank: bool, withdraw_cycles_to_dank_principal: Option, + to_cycles_ledger_subaccount: Option, ) -> DfxResult { let log = env.get_logger(); let mut canister_id_store = env.get_canister_id_store()?; @@ -99,19 +110,23 @@ async fn delete_canister( let to_dank = withdraw_cycles_to_dank || withdraw_cycles_to_dank_principal.is_some(); // Get the canister to transfer the cycles to. - let target_canister_id = if no_withdrawal { - None + let withdraw_target = if no_withdrawal { + WithdrawTarget::NoWithdrawal } else if to_dank { - Some(DANK_PRINCIPAL) + WithdrawTarget::Dank } else { match withdraw_cycles_to_canister { Some(ref target_canister_id) => { - Some(Principal::from_text(target_canister_id).with_context(|| { - format!("Failed to read canister id {:?}.", target_canister_id) - })?) + let canister_id = + Principal::from_text(target_canister_id).with_context(|| { + format!("Failed to read canister id {:?}.", target_canister_id) + })?; + WithdrawTarget::Canister { canister_id } } None => match call_sender { - CallSender::Wallet(wallet_id) => Some(*wallet_id), + CallSender::Wallet(wallet_id) => WithdrawTarget::Canister { + canister_id: *wallet_id, + }, CallSender::SelectedId => { let network = env.get_network_descriptor(); let agent_env = create_agent_environment(env, Some(network.name.clone()))?; @@ -120,7 +135,19 @@ async fn delete_canister( .expect("No selected identity.") .to_string(); // If there is no wallet, then do not attempt to withdraw the cycles. - wallet_canister_id(network, &identity_name)? + match wallet_canister_id(network, &identity_name)? { + Some(canister_id) => WithdrawTarget::Canister { canister_id }, + None if CYCLES_LEDGER_ENABLED => { + let Some(my_principal) = env.get_selected_identity_principal() else { bail!("Identity has no principal attached") }; + WithdrawTarget::CyclesLedger { + to: Account { + owner: my_principal, + subaccount: to_cycles_ledger_subaccount, + }, + } + } + _ => WithdrawTarget::NoWithdrawal, + } } }, } @@ -135,11 +162,10 @@ async fn delete_canister( }; fetch_root_key_if_needed(env).await?; - if let Some(target_canister_id) = target_canister_id { + if withdraw_target != WithdrawTarget::NoWithdrawal { info!( log, - "Beginning withdrawal of cycles to canister {}; on failure try --no-wallet --no-withdrawal.", - target_canister_id + "Beginning withdrawal of cycles; on failure try --no-wallet --no-withdrawal." ); // Determine how many cycles we can withdraw. @@ -197,40 +223,55 @@ async fn delete_canister( break; } let cycles_to_withdraw = cycles - margin; - let result = if !to_dank { - info!( - log, - "Attempting to transfer {} cycles to canister {}.", - cycles_to_withdraw, - target_canister_id - ); - // Transfer cycles from the source canister to the target canister using the temporary wallet. - deposit_cycles( - env, - target_canister_id, - &CallSender::Wallet(canister_id), - cycles_to_withdraw, - ) - .await - } else { - info!( - log, - "Attempting to transfer {} cycles to dank principal {}.", - cycles_to_withdraw, - dank_target_principal - ); - let wallet = build_wallet_canister(canister_id, agent).await?; - let opt_principal = Some(dank_target_principal); - wallet - .call( + let result = match withdraw_target { + WithdrawTarget::NoWithdrawal => Ok(()), + WithdrawTarget::Dank => { + info!( + log, + "Attempting to transfer {} cycles to dank principal {}.", + cycles_to_withdraw, + dank_target_principal + ); + let wallet = build_wallet_canister(canister_id, agent).await?; + let opt_principal = Some(dank_target_principal); + wallet + .call( + DANK_PRINCIPAL, + "mint", + Argument::from_candid((opt_principal,)), + cycles_to_withdraw, + ) + .call_and_wait() + .await + .context("Failed mint call.") + } + WithdrawTarget::Canister { + canister_id: target_canister_id, + } => { + info!( + log, + "Attempting to transfer {} cycles to canister {}.", + cycles_to_withdraw, + target_canister_id + ); + // Transfer cycles from the source canister to the target canister using the temporary wallet. + deposit_cycles( + env, target_canister_id, - "mint", - Argument::from_candid((opt_principal,)), + &CallSender::Wallet(canister_id), cycles_to_withdraw, ) - .call_and_wait() .await - .context("Failed mint call.") + } + WithdrawTarget::CyclesLedger { to } => { + wallet_deposit_to_cycles_ledger( + agent, + canister_id, + cycles_to_withdraw, + to, + ) + .await + } }; if result.is_ok() { info!(log, "Successfully withdrew {} cycles.", cycles_to_withdraw); @@ -239,7 +280,7 @@ async fn delete_canister( info!(log, "Not enough margin. Trying again with more margin."); attempts += 1; } else { - // Unforseen error. Report it back to user + // Unforeseen error. Report it back to user result?; } } @@ -293,6 +334,7 @@ pub async fn exec( opts.withdraw_cycles_to_canister, opts.withdraw_cycles_to_dank, opts.withdraw_cycles_to_dank_principal, + opts.to_subaccount, ) .await } else if opts.all { @@ -307,6 +349,7 @@ pub async fn exec( opts.withdraw_cycles_to_canister.clone(), opts.withdraw_cycles_to_dank, opts.withdraw_cycles_to_dank_principal.clone(), + opts.to_subaccount, ) .await?; } @@ -316,3 +359,11 @@ pub async fn exec( unreachable!() } } + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum WithdrawTarget { + NoWithdrawal, + Dank, + CyclesLedger { to: Account }, + Canister { canister_id: Principal }, +} diff --git a/src/dfx/src/lib/operations/cycles_ledger.rs b/src/dfx/src/lib/operations/cycles_ledger.rs index cc2cb45cce..265e476323 100644 --- a/src/dfx/src/lib/operations/cycles_ledger.rs +++ b/src/dfx/src/lib/operations/cycles_ledger.rs @@ -9,17 +9,18 @@ use crate::lib::operations::canister::create_canister::{ CANISTER_CREATE_FEE, CANISTER_INITIAL_CYCLE_BALANCE, }; use crate::lib::retryable::retryable; -use anyhow::{anyhow, bail}; +use anyhow::{anyhow, bail, Context}; use backoff::future::retry; use backoff::ExponentialBackoff; use candid::{CandidType, Decode, Encode, Nat, Principal}; +use dfx_core::canister::build_wallet_canister; 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 ic_utils::{Argument, Canister}; use icrc_ledger_types::icrc1; -use icrc_ledger_types::icrc1::account::Subaccount; +use icrc_ledger_types::icrc1::account::{Account, Subaccount}; use icrc_ledger_types::icrc1::transfer::{BlockIndex, TransferError}; use serde::Deserialize; use slog::{info, Logger}; @@ -33,6 +34,7 @@ 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_DEPOSIT_METHOD: &str = "deposit"; const CYCLES_LEDGER_CANISTER_ID: Principal = Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x02, 0x01, 0x01]); @@ -320,6 +322,32 @@ pub async fn create_with_cycles_ledger( } } +pub async fn wallet_deposit_to_cycles_ledger( + agent: &Agent, + wallet_id: Principal, + cycles_to_withdraw: u128, + to: Account, +) -> DfxResult { + // TODO(FI-1022): Import types from cycles ledger crate once available + #[derive(CandidType)] + pub struct DepositArg { + pub to: Account, + pub memo: Option>, + } + + build_wallet_canister(wallet_id, agent) + .await? + .call128( + CYCLES_LEDGER_CANISTER_ID, + CYCLES_LEDGER_DEPOSIT_METHOD, + Argument::from_candid((DepositArg { to, memo: None },)), + cycles_to_withdraw, + ) + .call_and_wait() + .await + .context("Failed deposit call.") +} + #[test] fn ledger_canister_id_text_representation() { assert_eq!(