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

refactor(icrc2 cycles): Split icrc2 cycles payment into two #30

Merged
merged 4 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/example/paid_service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use ic_papi_api::cycles::cycles_ledger_canister_id;
use ic_papi_api::{PaymentError, PaymentType};
use ic_papi_guard::guards::{
attached_cycles::AttachedCyclesPayment,
caller_pays_icrc2_cycles::CallerPaysIcrc2CyclesPaymentGuard,
caller_pays_icrc2_tokens::CallerPaysIcrc2TokensPaymentGuard,
icrc2_cycles::Icrc2CyclesPaymentGuard,
};
use ic_papi_guard::guards::{PaymentContext, PaymentGuard, PaymentGuard2};
use state::{set_init_args, PAYMENT_GUARD};
Expand All @@ -35,7 +35,7 @@ async fn cost_1000_attached_cycles() -> Result<String, PaymentError> {
/// An API method that requires 1 billion cycles using an ICRC-2 approve with default parameters.
#[update()]
async fn caller_pays_1b_icrc2_cycles() -> Result<String, PaymentError> {
Icrc2CyclesPaymentGuard::default()
CallerPaysIcrc2CyclesPaymentGuard::default()
.deduct(1_000_000_000)
.await?;
Ok("Yes, you paid 1 billion cycles!".to_string())
Expand Down
9 changes: 5 additions & 4 deletions src/guard/src/guards/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ use ic_papi_api::{

use super::{
attached_cycles::AttachedCyclesPayment,
caller_pays_icrc2_cycles::CallerPaysIcrc2CyclesPaymentGuard,
caller_pays_icrc2_tokens::CallerPaysIcrc2TokensPaymentGuard,
icrc2_cycles::Icrc2CyclesPaymentGuard,
patron_pays_icrc2_cycles::PatronPaysIcrc2CyclesPaymentGuard,
patron_pays_icrc2_tokens::PatronPaysIcrc2TokensPaymentGuard, PaymentContext, PaymentGuard,
PaymentGuard2,
};
Expand Down Expand Up @@ -62,7 +63,7 @@ impl<const CAP: usize> PaymentGuard2 for AnyPaymentGuard<CAP> {
match payment_config {
PaymentWithConfig::AttachedCycles => AttachedCyclesPayment {}.deduct(fee).await,
PaymentWithConfig::CallerPaysIcrc2Cycles => {
Icrc2CyclesPaymentGuard {
CallerPaysIcrc2CyclesPaymentGuard {
payer_account: Account {
owner: caller,
subaccount: None,
Expand All @@ -74,10 +75,10 @@ impl<const CAP: usize> PaymentGuard2 for AnyPaymentGuard<CAP> {
.await
}
PaymentWithConfig::PatronPaysIcrc2Cycles(patron) => {
Icrc2CyclesPaymentGuard {
PatronPaysIcrc2CyclesPaymentGuard {
payer_account: patron,
spender_subaccount: Some(principal2account(&caller)),
..Icrc2CyclesPaymentGuard::default()
..PatronPaysIcrc2CyclesPaymentGuard::default()
}
.deduct(fee)
.await
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ use ic_papi_api::{caller::TokenAmount, cycles::cycles_ledger_canister_id, Accoun

/// Accepts cycles using an ICRC-2 approve followed by withdrawing the cycles to the current canister. Withdrawing
/// cycles to the current canister is specific to the cycles ledger canister; it is not part of the ICRC-2 standard.
pub struct Icrc2CyclesPaymentGuard {
pub struct CallerPaysIcrc2CyclesPaymentGuard {
/// The payer
pub payer_account: Account,
/// The spender, if different from the payer.
pub spender_subaccount: Option<serde_bytes::ByteBuf>,
/// Own canister ID
pub own_canister_id: Principal,
}
impl Icrc2CyclesPaymentGuard {
impl CallerPaysIcrc2CyclesPaymentGuard {
#[must_use]
pub fn default_account() -> Account {
Account {
Expand All @@ -40,7 +40,7 @@ impl Icrc2CyclesPaymentGuard {
}
}

impl Default for Icrc2CyclesPaymentGuard {
impl Default for CallerPaysIcrc2CyclesPaymentGuard {
fn default() -> Self {
Self {
payer_account: Self::default_account(),
Expand All @@ -50,7 +50,7 @@ impl Default for Icrc2CyclesPaymentGuard {
}
}

impl PaymentGuard for Icrc2CyclesPaymentGuard {
impl PaymentGuard for CallerPaysIcrc2CyclesPaymentGuard {
async fn deduct(&self, fee: TokenAmount) -> Result<(), PaymentError> {
// The patron must not be the vendor itself (this canister).
if self.payer_account.owner == self.own_canister_id {
Expand Down
3 changes: 2 additions & 1 deletion src/guard/src/guards/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use candid::Principal;
use ic_papi_api::{caller::TokenAmount, PaymentError, PaymentType};
pub mod any;
pub mod attached_cycles;
pub mod caller_pays_icrc2_cycles;
pub mod caller_pays_icrc2_tokens;
pub mod icrc2_cycles;
pub mod patron_pays_icrc2_cycles;
pub mod patron_pays_icrc2_tokens;

#[allow(async_fn_in_trait)]
Expand Down
92 changes: 92 additions & 0 deletions src/guard/src/guards/patron_pays_icrc2_cycles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! Code to receive cycles as payment, credited to the canister, using ICRC-2 and a cycles-ledger specific withdrawal method.
use super::{PaymentError, PaymentGuard};
use candid::{Nat, Principal};
use cycles_ledger_client::WithdrawFromArgs;
use ic_papi_api::{caller::TokenAmount, cycles::cycles_ledger_canister_id, Account};

/// Accepts cycles using an ICRC-2 approve followed by withdrawing the cycles to the current canister. Withdrawing
/// cycles to the current canister is specific to the cycles ledger canister; it is not part of the ICRC-2 standard.
pub struct PatronPaysIcrc2CyclesPaymentGuard {
/// The payer
pub payer_account: Account,
/// The spender, if different from the payer.
pub spender_subaccount: Option<serde_bytes::ByteBuf>,
/// Own canister ID
pub own_canister_id: Principal,
}
impl PatronPaysIcrc2CyclesPaymentGuard {
#[must_use]
pub fn default_account() -> Account {
Account {
owner: ic_cdk::caller(),
subaccount: None,
}
}
/// The normal cycles ledger canister ID.
///
/// - If the cycles ledger is listed in `dfx.json`, a normal `dfx build` will set the
/// environment variable `CANISTER_ID_CYCLES_LEDGER` and we use this to obtain the canister ID.
/// - Otherwise, we use the mainnet cycled ledger canister ID, which is `um5iw-rqaaa-aaaaq-qaaba-cai`.
///
/// # Panics
/// - If the `CANISTER_ID_CYCLES_LEDGER` environment variable is not a valid canister ID at
/// build time.
#[must_use]
pub fn default_cycles_ledger() -> Principal {
Principal::from_text(
option_env!("CANISTER_ID_CYCLES_LEDGER").unwrap_or("um5iw-rqaaa-aaaaq-qaaba-cai"),
)
.expect("Compile error: Failed to parse build env var 'CANISTER_ID_CYCLES_LEDGER' as a canister ID.")
}
}

impl Default for PatronPaysIcrc2CyclesPaymentGuard {
fn default() -> Self {
Self {
payer_account: Self::default_account(),
own_canister_id: ic_cdk::api::id(),
spender_subaccount: None,
}
}
}

impl PaymentGuard for PatronPaysIcrc2CyclesPaymentGuard {
async fn deduct(&self, fee: TokenAmount) -> Result<(), PaymentError> {
// The patron must not be the vendor itself (this canister).
if self.payer_account.owner == self.own_canister_id {
return Err(PaymentError::InvalidPatron);
}
// The cycles ledger has a special `withdraw_from` method, similar to `transfer_from`,
// but that adds the cycles to the canister rather than putting it into a ledger account.
cycles_ledger_client::Service(cycles_ledger_canister_id())
.withdraw_from(&WithdrawFromArgs {
to: self.own_canister_id,
amount: Nat::from(fee),
from: self.payer_account.clone(),
spender_subaccount: self.spender_subaccount.clone(),
created_at_time: None,
})
.await
.map_err(|(rejection_code, string)| {
eprintln!(
"Failed to reach ledger canister at {}: {rejection_code:?}: {string}",
cycles_ledger_canister_id()
);
PaymentError::LedgerUnreachable {
ledger: cycles_ledger_canister_id(),
}
})?
.0
.map_err(|error| {
eprintln!(
"Failed to withdraw from ledger canister at {}: {error:?}",
cycles_ledger_canister_id()
);
PaymentError::LedgerWithdrawFromError {
ledger: cycles_ledger_canister_id(),
error,
}
})
.map(|_| ())
}
}