Skip to content

Commit

Permalink
rework
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcin Nowak-Liebiediew committed Jan 23, 2024
1 parent 97e9e33 commit 56fed42
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 229 deletions.
22 changes: 13 additions & 9 deletions e2e/assets/faucet/main.mo
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
};
};
90 changes: 3 additions & 87 deletions e2e/tests-dfx/cycles-ledger.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)'"
}
149 changes: 17 additions & 132 deletions src/dfx/src/commands/cycles/redeem_faucet_coupon.rs
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -24,10 +20,6 @@ pub struct RedeemFaucetCouponOpts {
/// Alternative faucet address. If not set, this uses the DFINITY faucet.
#[arg(long)]
faucet: Option<String>,

/// 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 {
Expand All @@ -48,133 +40,14 @@ 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::<GetOrCreateWalletCanisterError>());
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<u128> {
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<u128> {
#[derive(CandidType, Deserialize)]
struct Account {
owner: Principal,
subaccount: Option<Vec<u8>>,
}
let identity = env
.get_selected_identity_principal()
.with_context(|| anyhow!("No identity selected."))?;
let response = agent
.update(&faucet_principal, "redeem_to_cycles_ledger")
.with_arg(
encode_args((
coupon_code,
opts.coupon_code.clone(),
Account {
owner: identity,
subaccount: None,
Expand All @@ -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)]
Expand Down
5 changes: 4 additions & 1 deletion src/dfx/src/commands/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
Expand All @@ -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,
Expand Down Expand Up @@ -115,7 +118,7 @@ where
}

#[context("Failed to setup wallet caller.")]
pub(crate) async fn get_wallet(env: &dyn Environment) -> DfxResult<WalletCanister<'_>> {
async fn get_wallet(env: &dyn Environment) -> DfxResult<WalletCanister<'_>> {
let identity_name = env
.get_selected_identity()
.expect("No selected identity.")
Expand Down
Loading

0 comments on commit 56fed42

Please sign in to comment.