From 56fed421e84d07fe8852ca7788d8e99fb36d0d5c Mon Sep 17 00:00:00 2001 From: Marcin Nowak-Liebiediew Date: Tue, 23 Jan 2024 15:09:00 +0100 Subject: [PATCH] rework --- e2e/assets/faucet/main.mo | 22 +-- e2e/tests-dfx/cycles-ledger.bash | 90 +---------- .../commands/cycles/redeem_faucet_coupon.rs | 149 ++---------------- src/dfx/src/commands/wallet/mod.rs | 5 +- .../commands/wallet/redeem_faucet_coupon.rs | 117 ++++++++++++++ 5 files changed, 154 insertions(+), 229 deletions(-) create mode 100644 src/dfx/src/commands/wallet/redeem_faucet_coupon.rs diff --git a/e2e/assets/faucet/main.mo b/e2e/assets/faucet/main.mo index 9d0d66bd47..8febbdc399 100644 --- a/e2e/assets/faucet/main.mo +++ b/e2e/assets/faucet/main.mo @@ -2,22 +2,25 @@ import Cycles "mo:base/ExperimentalCycles"; import Error "mo:base/Error"; import Principal "mo:base/Principal"; import Text "mo:base/Text"; +import Debug "mo:base/Debug"; + actor class Coupon() = self { type Management = actor { - deposit_cycles : ({canister_id : Principal}) -> async (); + deposit_cycles : ({canister_id : Principal}) -> async (); }; type CyclesLedger = actor { - deposit : ({code: Text; account: Account}) -> async (DepositResult); - }; - type DepositResult = { - balance : Nat; - block_index : Nat; + deposit : (DepositArgs) -> async (DepositResult); }; type Account = { owner : Principal; subaccount : ?Blob; }; + type DepositArgs = { + to : Account; + memo : ?Blob; + }; + type DepositResult = { balance : Nat; block_index : Nat }; // Uploading wasm is hard. This is much easier to handle. @@ -61,9 +64,10 @@ actor class Coupon() = self { let CyclesLedgerCanister : CyclesLedger = actor("um5iw-rqaaa-aaaaq-qaaba-cai"); var amount = 10000000000000; Cycles.add(amount); - return await CyclesLedgerCanister.deposit({ - code = code; - account = account + let result = await CyclesLedgerCanister.deposit({ + to = account; + memo = null }); + return result; }; }; diff --git a/e2e/tests-dfx/cycles-ledger.bash b/e2e/tests-dfx/cycles-ledger.bash index 359bc1a27a..444fcecdd5 100644 --- a/e2e/tests-dfx/cycles-ledger.bash +++ b/e2e/tests-dfx/cycles-ledger.bash @@ -525,7 +525,7 @@ current_time_nanoseconds() { # setup done dfx identity use alice - # shellcheck disable=SC2031 + # shellcheck disable=SC2031,SC2030 export DFX_DISABLE_AUTO_WALLET=1 assert_command dfx canister create --all --with-cycles 10T assert_command dfx cycles balance --precise @@ -546,47 +546,7 @@ current_time_nanoseconds() { assert_eq "22.379 TC (trillion cycles)." } -@test "redeem-faucet-coupon can set a new wallet and top up an existing one" { - dfx_new hello - install_asset faucet - dfx deploy - dfx ledger fabricate-cycles --canister faucet --t 1000 - - dfx identity new --storage-mode plaintext faucet_testing - dfx identity use faucet_testing - - # prepare wallet to hand out - dfx wallet balance # this creates a new wallet with user faucet_testing as controller - dfx canister call faucet set_wallet_to_hand_out "(principal \"$(dfx identity get-wallet)\")" # register the wallet as the wallet that the faucet will return - rm "$E2E_SHARED_LOCAL_NETWORK_DATA_DIRECTORY/wallets.json" # forget about the currently configured wallet - - # assert: no wallet configured - # shellcheck disable=SC2031 - export DFX_DISABLE_AUTO_WALLET=1 - assert_command_fail dfx wallet balance - assert_match "No wallet configured" - - assert_command dfx cycles redeem-faucet-coupon --new-cycles-wallet --faucet "$(dfx canister id faucet)" 'valid-coupon' - assert_match "Redeemed coupon valid-coupon for 100.000 TC .* to a new wallet" - - # only succeeds if wallet is correctly set - assert_command dfx wallet balance - # checking only balance before the dot, rest may fluctuate - # balance may be 99.??? TC if cycles accounting is done, or 100.000 TC if not - assert_match "99\.|100\." - - unset DFX_DISABLE_AUTO_WALLET - - assert_command dfx cycles redeem-faucet-coupon --faucet "$(dfx canister id faucet)" 'another-valid-coupon' - assert_match "Redeemed coupon code another-valid-coupon for 10.000 TC .* to the existing wallet" - - assert_command dfx wallet balance - # checking only balance before the dot, rest may fluctuate - # balance may be 109.??? TC if cycles accounting is done, or 110.000 TC if not - assert_match "109\.|110\." -} - -@test "redeem-faucet-coupon without --new-cycles-wallet redeems into the cycles ledger when no wallet exists" { +@test "redeem-faucet-coupon without redeems into the cycles ledger" { assert_command deploy_cycles_ledger dfx_new hello install_asset faucet @@ -596,50 +556,6 @@ current_time_nanoseconds() { dfx identity new --storage-mode plaintext no_wallet_identity dfx identity use no_wallet_identity - # ensure no wallet is set for the identity - # rm "$DFX_CONFIG_ROOT/.config/dfx/identity/no_wallet_identity/wallets.json" - - # redeem a faucet coupon without specifying --new-cycles-wallet assert_command dfx cycles redeem-faucet-coupon --faucet "$(dfx canister id faucet)" 'valid-coupon' - assert_match "Redeemed coupon code valid-coupon for .* TC .* to the cycles ledger.", -} - -@test "redeem-faucet-coupon without specifying --new-cycles-wallet redeems into existing wallet" { - dfx_new hello - install_asset faucet - dfx deploy - dfx ledger fabricate-cycles --canister faucet --t 1000 - - dfx identity new --storage-mode plaintext wallet_identity - dfx identity use wallet_identity - - # create a new wallet for wallet_identity - assert_command dfx wallet balance - - # ensure a wallet is configured for the identity - assert_command dfx identity get-wallet - - # redeem a faucet coupon without specifying --new-cycles-wallet - assert_command dfx cycles redeem-faucet-coupon --faucet "$(dfx canister id faucet)" 'valid-coupon' - assert_match "Redeemed coupon code valid-coupon for .* TC .* to the existing wallet .*", -} - -@test "redeem-faucet-coupon with --new-cycles-wallet fails when wallet already exists" { - dfx_new hello - install_asset faucet - dfx deploy - dfx ledger fabricate-cycles --canister faucet --t 1000 - - dfx identity new --storage-mode plaintext existing_wallet_identity - dfx identity use existing_wallet_identity - - # create a new wallet for existing_wallet_identity - assert_command dfx wallet balance - - # ensure a wallet is configured for the identity - assert_command dfx identity get-wallet - - # try to redeem a faucet coupon with the --new-cycles-wallet flag set to 'true' - assert_command_fail dfx cycles redeem-faucet-coupon --faucet "$(dfx canister id faucet)" --new-cycles-wallet 'valid-coupon' - assert_match "A cycles wallet already exists for the current identity." + assert_match "Redeemed coupon 'valid-coupon' to the cycles ledger, current balance: .* TC .* for identity '$(dfx identity get-principal)'" } diff --git a/src/dfx/src/commands/cycles/redeem_faucet_coupon.rs b/src/dfx/src/commands/cycles/redeem_faucet_coupon.rs index 239e0c5be9..c637d01895 100644 --- a/src/dfx/src/commands/cycles/redeem_faucet_coupon.rs +++ b/src/dfx/src/commands/cycles/redeem_faucet_coupon.rs @@ -1,15 +1,11 @@ -use crate::commands::wallet::get_wallet; -use crate::lib::diagnosis::DiagnosedError; use crate::lib::environment::Environment; use crate::lib::error::DfxResult; -use crate::lib::identity::wallet::{set_wallet_id, GetOrCreateWalletCanisterError}; use crate::lib::root_key::fetch_root_key_if_needed; use crate::util::{format_as_trillions, pretty_thousand_separators}; use anyhow::{anyhow, bail, Context}; use candid::{encode_args, CandidType, Decode, Deserialize, Principal}; use clap::Parser; -use ic_agent::Agent; -use ic_utils::interfaces::WalletCanister; +use icrc_ledger_types::icrc1::account::Account; use slog::{info, warn}; pub const DEFAULT_FAUCET_PRINCIPAL: Principal = @@ -24,10 +20,6 @@ pub struct RedeemFaucetCouponOpts { /// Alternative faucet address. If not set, this uses the DFINITY faucet. #[arg(long)] faucet: Option, - - /// Redeem coupon to a new cycles wallet, creates a if the identity does not have one, otherwise returns an error. - #[arg(long, default_value = "false")] - new_cycles_wallet: bool, } pub async fn exec(env: &dyn Environment, opts: RedeemFaucetCouponOpts) -> DfxResult { @@ -48,125 +40,6 @@ pub async fn exec(env: &dyn Environment, opts: RedeemFaucetCouponOpts) -> DfxRes } info!(log, "Redeeming coupon. This may take up to 30 seconds..."); - let wallet = get_wallet(env) - .await - .map_err(|e| e.downcast::()); - let coupon_code = opts.coupon_code; - match wallet { - Ok(_) if opts.new_cycles_wallet => { - bail!("A cycles wallet already exists for the current identity. Use the wallet to redeem the coupon."); - } - // identity already has a wallet - faucet should top up the wallet - Ok(wallet_canister) => { - let redeemed_cycles = - redeem_to_existing_wallet(agent, &wallet_canister, &faucet_principal, &coupon_code) - .await?; - info!( - log, - "Redeemed coupon code {coupon_code} for {} TC (trillion cycles) to the existing wallet {}", - pretty_thousand_separators(format_as_trillions(redeemed_cycles)), - wallet_canister.canister_id_() - ); - } - // identity has no wallet yet - faucet will provide one - Err(Ok(GetOrCreateWalletCanisterError::NoWalletConfigured { .. })) - if opts.new_cycles_wallet => - { - let (redeemed_cycles, new_wallet_address) = - create_wallet_and_redeem(agent, env, &faucet_principal, &coupon_code).await?; - info!( - log, - "Redeemed coupon {coupon_code} for {} TC (trillion cycles) to a new wallet {new_wallet_address}.", - pretty_thousand_separators(format_as_trillions(redeemed_cycles)) - ); - } - Err(_) if opts.new_cycles_wallet => { - bail!("Failed to create a new cycles wallet."); - } - // identity has no wallet yet - faucet will redeem the coupon to the cycles ledger - Err(_) => { - let redeemed_cycles = - redeem_to_cycles_ledger(agent, env, &faucet_principal, &coupon_code).await?; - info!( - log, - "Redeemed coupon code {coupon_code} for {} TC (trillion cycles) to the cycles ledger.", - pretty_thousand_separators(format_as_trillions(redeemed_cycles)) - ); - } - }; - - Ok(()) -} - -async fn redeem_to_existing_wallet( - agent: &Agent, - wallet_canister: &WalletCanister<'_>, - faucet_principal: &Principal, - coupon_code: &str, -) -> DfxResult { - let wallet_principal = wallet_canister.canister_id_(); - let response = agent - .update(&faucet_principal, "redeem_to_wallet") - .with_arg( - encode_args((coupon_code, wallet_principal)) - .context("Failed to serialize redeem_to_wallet arguments.")?, - ) - .call_and_wait() - .await - .context("Failed redeem_to_wallet call.")?; - let redeemed_cycles = - Decode!(&response, u128).context("Failed to decode redeem_to_wallet response.")?; - Ok(redeemed_cycles) -} - -async fn create_wallet_and_redeem( - agent: &Agent, - env: &dyn Environment, - faucet_principal: &Principal, - coupon_code: &str, -) -> DfxResult<(u128, Principal)> { - let identity = env - .get_selected_identity() - .with_context(|| anyhow!("No identity selected."))?; - let response = agent - .update(&faucet_principal, "redeem") - .with_arg(encode_args((coupon_code,)).context("Failed to serialize 'redeem' arguments.")?) - .call_and_wait() - .await - .context("Failed 'redeem' call.")?; - let new_wallet_address = - Decode!(&response, Principal).context("Failed to decode 'redeem' response.")?; - set_wallet_id(env.get_network_descriptor(), &identity, new_wallet_address) - .with_context(|| { - DiagnosedError::new( - format!( - "dfx failed while trying to set your new wallet, '{}'", - &new_wallet_address - ), - format!("Please save your new wallet's ID '{}' and set the wallet manually afterwards using 'dfx identity set-wallet'.", &new_wallet_address), - ) - })?; - let redeemed_cycles = WalletCanister::create(agent, new_wallet_address.clone()) - .await - .unwrap() - .wallet_balance() - .await - .unwrap() - .amount; - Ok((redeemed_cycles, new_wallet_address)) -} - -async fn redeem_to_cycles_ledger( - agent: &Agent, - env: &dyn Environment, - faucet_principal: &Principal, - coupon_code: &str, -) -> DfxResult { - #[derive(CandidType, Deserialize)] - struct Account { - owner: Principal, - subaccount: Option>, - } let identity = env .get_selected_identity_principal() .with_context(|| anyhow!("No identity selected."))?; @@ -174,7 +47,7 @@ async fn redeem_to_cycles_ledger( .update(&faucet_principal, "redeem_to_cycles_ledger") .with_arg( encode_args(( - coupon_code, + opts.coupon_code.clone(), Account { owner: identity, subaccount: None, @@ -185,10 +58,22 @@ async fn redeem_to_cycles_ledger( .call_and_wait() .await .context("Failed 'redeem_to_cycles_ledger' call.")?; - let result = Decode!(&response, (u128, u128)) + #[derive(CandidType, Deserialize)] + struct DepositResponse { + balance: u128, + block_index: u128, + } + let result = Decode!(&response, DepositResponse) .context("Failed to decode 'redeem_to_cycles_ledger' response.")?; - let redeemed_cycles = result.0; - Ok(redeemed_cycles) + let redeemed_cycles = result.balance; + info!( + log, + "Redeemed coupon '{}' to the cycles ledger, current balance: {} TC (trillions of cycles) for identity '{}'.", + opts.coupon_code.clone(), + pretty_thousand_separators(format_as_trillions(redeemed_cycles)), + identity, + ); + Ok(()) } #[cfg(test)] diff --git a/src/dfx/src/commands/wallet/mod.rs b/src/dfx/src/commands/wallet/mod.rs index 0a5f96ff75..bbc2538f4f 100644 --- a/src/dfx/src/commands/wallet/mod.rs +++ b/src/dfx/src/commands/wallet/mod.rs @@ -21,6 +21,7 @@ mod custodians; mod deauthorize; mod list_addresses; mod name; +mod redeem_faucet_coupon; mod remove_controller; mod send; mod set_name; @@ -47,6 +48,7 @@ enum SubCommand { Custodians(custodians::CustodiansOpts), Deauthorize(deauthorize::DeauthorizeOpts), Name(name::NameOpts), + RedeemFaucetCoupon(redeem_faucet_coupon::RedeemFaucetCouponOpts), RemoveController(remove_controller::RemoveControllerOpts), Send(send::SendOpts), SetName(set_name::SetNameOpts), @@ -66,6 +68,7 @@ pub fn exec(env: &dyn Environment, opts: WalletOpts) -> DfxResult { SubCommand::Custodians(v) => custodians::exec(&agent_env, v).await, SubCommand::Deauthorize(v) => deauthorize::exec(&agent_env, v).await, SubCommand::Name(v) => name::exec(&agent_env, v).await, + SubCommand::RedeemFaucetCoupon(v) => redeem_faucet_coupon::exec(&agent_env, v).await, SubCommand::RemoveController(v) => remove_controller::exec(&agent_env, v).await, SubCommand::Send(v) => send::exec(&agent_env, v).await, SubCommand::SetName(v) => set_name::exec(&agent_env, v).await, @@ -115,7 +118,7 @@ where } #[context("Failed to setup wallet caller.")] -pub(crate) async fn get_wallet(env: &dyn Environment) -> DfxResult> { +async fn get_wallet(env: &dyn Environment) -> DfxResult> { let identity_name = env .get_selected_identity() .expect("No selected identity.") diff --git a/src/dfx/src/commands/wallet/redeem_faucet_coupon.rs b/src/dfx/src/commands/wallet/redeem_faucet_coupon.rs new file mode 100644 index 0000000000..3ba18bcfda --- /dev/null +++ b/src/dfx/src/commands/wallet/redeem_faucet_coupon.rs @@ -0,0 +1,117 @@ +use crate::commands::wallet::get_wallet; +use crate::lib::diagnosis::DiagnosedError; +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::identity::wallet::set_wallet_id; +use crate::lib::root_key::fetch_root_key_if_needed; +use crate::util::{format_as_trillions, pretty_thousand_separators}; +use anyhow::{anyhow, bail, Context}; +use candid::{encode_args, Decode, Principal}; +use clap::Parser; +use slog::{info, warn}; + +pub const DEFAULT_FAUCET_PRINCIPAL: Principal = + Principal::from_slice(&[0, 0, 0, 0, 1, 112, 0, 196, 1, 1]); + +/// Redeem a code at the cycles faucet. +#[derive(Parser)] +pub struct RedeemFaucetCouponOpts { + /// The coupon code to redeem at the faucet. + coupon_code: String, + + /// Alternative faucet address. If not set, this uses the DFINITY faucet. + #[arg(long)] + faucet: Option, +} + +pub async fn exec(env: &dyn Environment, opts: RedeemFaucetCouponOpts) -> DfxResult { + let log = env.get_logger(); + + let faucet_principal = if let Some(alternative_faucet) = opts.faucet { + let canister_id_store = env.get_canister_id_store()?; + Principal::from_text(&alternative_faucet) + .or_else(|_| canister_id_store.get(&alternative_faucet))? + } else { + DEFAULT_FAUCET_PRINCIPAL + }; + let agent = env.get_agent(); + if fetch_root_key_if_needed(env).await.is_err() { + bail!("Failed to connect to the local replica. Did you forget to use '--network ic'?"); + } else if !env.get_network_descriptor().is_ic { + warn!(log, "Trying to redeem a wallet coupon on a local replica. Did you forget to use '--network ic'?"); + } + + info!(log, "Redeeming coupon. This may take up to 30 seconds..."); + let wallet = get_wallet(env).await; + match wallet { + // identity has a wallet already - faucet should top up the wallet + Ok(wallet_canister) => { + let wallet_principal = wallet_canister.canister_id_(); + let response = agent + .update(&faucet_principal, "redeem_to_wallet") + .with_arg( + encode_args((opts.coupon_code.clone(), wallet_principal)) + .context("Failed to serialize redeem_to_wallet arguments.")?, + ) + .call_and_wait() + .await + .context("Failed redeem_to_wallet call.")?; + let redeemed_cycles = + Decode!(&response, u128).context("Failed to decode redeem_to_wallet response.")?; + info!( + log, + "Redeemed coupon code {} for {} TC (trillion cycles).", + opts.coupon_code, + pretty_thousand_separators(format_as_trillions(redeemed_cycles)) + ); + + Ok(()) + } + // identity has no wallet yet - faucet will provide one + _ => { + let identity = env + .get_selected_identity() + .with_context(|| anyhow!("No identity selected."))?; + let response = agent + .update(&faucet_principal, "redeem") + .with_arg( + encode_args((opts.coupon_code.clone(),)) + .context("Failed to serialize 'redeem' arguments.")?, + ) + .call_and_wait() + .await + .context("Failed 'redeem' call.")?; + let new_wallet_address = + Decode!(&response, Principal).context("Failed to decode 'redeem' response.")?; + info!( + log, + "Redeemed coupon {} for a new wallet: {}", opts.coupon_code, &new_wallet_address + ); + set_wallet_id(env.get_network_descriptor(), identity, new_wallet_address) + .with_context(|| { + DiagnosedError::new( + format!( + "dfx failed while trying to set your new wallet, '{}'", + &new_wallet_address + ), + format!("Please save your new wallet's ID '{}' and set the wallet manually afterwards using 'dfx identity set-wallet'.", &new_wallet_address), + ) + })?; + info!(log, "New wallet set."); + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_faucet_canister_id() { + assert_eq!( + DEFAULT_FAUCET_PRINCIPAL, + Principal::from_text("fg7gi-vyaaa-aaaal-qadca-cai").unwrap() + ); + } +}