Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: redeem cycles faucet coupon to cycles ledger #3515

32 changes: 31 additions & 1 deletion e2e/assets/faucet/main.mo
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,26 @@ 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 : (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.
var wallet_to_hand_out: ?Principal = null;
Expand Down Expand Up @@ -40,4 +55,19 @@ actor class Coupon() = self {
await IC0.deposit_cycles({ canister_id = wallet });
return amount;
};

// Redeem coupon code to cycle ledger
public shared (args) func redeem_to_cycles_ledger(code: Text, account: Account) : async DepositResult {
if (code == "invalid") {
throw(Error.reject("Code is expired or not redeemable"));
};
let CyclesLedgerCanister : CyclesLedger = actor("um5iw-rqaaa-aaaaq-qaaba-cai");
var amount = 10000000000000;
Cycles.add(amount);
let result = await CyclesLedgerCanister.deposit({
to = account;
memo = null
});
return result;
};
};
18 changes: 16 additions & 2 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 @@ -544,4 +544,18 @@ current_time_nanoseconds() {
assert_command dfx canister delete "${FRONTEND_ID}"
assert_command dfx cycles balance
assert_eq "22.379 TC (trillion cycles)."
}
}

@test "redeem-faucet-coupon without redeems into the cycles ledger" {
assert_command deploy_cycles_ledger
dfx_new hello
install_asset faucet
dfx deploy
dfx ledger fabricate-cycles --canister faucet --t 1000

dfx identity new --storage-mode plaintext no_wallet_identity
dfx identity use no_wallet_identity

assert_command dfx cycles redeem-faucet-coupon --faucet "$(dfx canister id faucet)" 'valid-coupon'
assert_match "Redeemed coupon 'valid-coupon' to the cycles ledger, current balance: .* TC .* for identity '$(dfx identity get-principal)'"
}
3 changes: 3 additions & 0 deletions src/dfx/src/commands/cycles/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use clap::Parser;
use tokio::runtime::Runtime;

mod balance;
mod redeem_faucet_coupon;
pub mod top_up;
mod transfer;

Expand All @@ -25,6 +26,7 @@ enum SubCommand {
Balance(balance::CyclesBalanceOpts),
TopUp(top_up::TopUpOpts),
Transfer(transfer::TransferOpts),
RedeemFaucetCoupon(redeem_faucet_coupon::RedeemFaucetCouponOpts),
}

pub fn exec(env: &dyn Environment, opts: CyclesOpts) -> DfxResult {
Expand All @@ -35,6 +37,7 @@ pub fn exec(env: &dyn Environment, opts: CyclesOpts) -> DfxResult {
SubCommand::Balance(v) => balance::exec(&agent_env, v).await,
SubCommand::TopUp(v) => top_up::exec(&agent_env, v).await,
SubCommand::Transfer(v) => transfer::exec(&agent_env, v).await,
SubCommand::RedeemFaucetCoupon(v) => redeem_faucet_coupon::exec(&agent_env, v).await,
}
})
}
90 changes: 90 additions & 0 deletions src/dfx/src/commands/cycles/redeem_faucet_coupon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use crate::lib::environment::Environment;
use crate::lib::error::DfxResult;
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 icrc_ledger_types::icrc1::account::Account;
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<String>,
}

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 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((
opts.coupon_code.clone(),
Account {
owner: identity,
subaccount: None,
},
))
.context("Failed to serialize 'redeem_to_cycles_ledger' arguments.")?,
)
.call_and_wait()
.await
.context("Failed 'redeem_to_cycles_ledger' call.")?;
#[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.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)]
mod tests {
use super::*;

#[test]
fn test_faucet_canister_id() {
assert_eq!(
DEFAULT_FAUCET_PRINCIPAL,
Principal::from_text("fg7gi-vyaaa-aaaal-qadca-cai").unwrap()
);
}
}
Loading