Skip to content

Commit

Permalink
feat: withdraw cycles to cycles ledger (#3506)
Browse files Browse the repository at this point in the history
  • Loading branch information
sesi200 authored Jan 17, 2024
1 parent 81cab15 commit cda396c
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 58 deletions.
44 changes: 44 additions & 0 deletions e2e/tests-dfx/cycles-ledger.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)."
}
24 changes: 12 additions & 12 deletions e2e/tests-dfx/deps.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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"
Expand Down
137 changes: 94 additions & 43 deletions src/dfx/src/commands/canister/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Subaccount>,
}

#[context("Failed to delete canister '{}'.", canister)]
Expand All @@ -83,6 +93,7 @@ async fn delete_canister(
withdraw_cycles_to_canister: Option<String>,
withdraw_cycles_to_dank: bool,
withdraw_cycles_to_dank_principal: Option<String>,
to_cycles_ledger_subaccount: Option<Subaccount>,
) -> DfxResult {
let log = env.get_logger();
let mut canister_id_store = env.get_canister_id_store()?;
Expand All @@ -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()))?;
Expand All @@ -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,
}
}
},
}
Expand All @@ -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.
Expand Down Expand Up @@ -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);
Expand All @@ -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?;
}
}
Expand Down Expand Up @@ -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 {
Expand 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?;
}
Expand All @@ -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 },
}
Loading

0 comments on commit cda396c

Please sign in to comment.