From b8e86cc54dfca2be0d08afbd2efd5be4eee8133d Mon Sep 17 00:00:00 2001 From: Max Murphy Date: Tue, 1 Oct 2024 11:33:04 +0200 Subject: [PATCH 1/4] Split the cycles payment guard --- src/example/paid_service/src/lib.rs | 2 +- src/guard/src/guards/any.rs | 2 +- src/guard/src/guards/icrc2_cycles.rs | 92 ---------------------------- src/guard/src/guards/mod.rs | 3 +- 4 files changed, 4 insertions(+), 95 deletions(-) delete mode 100644 src/guard/src/guards/icrc2_cycles.rs diff --git a/src/example/paid_service/src/lib.rs b/src/example/paid_service/src/lib.rs index d71a3b9..f37e858 100644 --- a/src/example/paid_service/src/lib.rs +++ b/src/example/paid_service/src/lib.rs @@ -8,7 +8,7 @@ use ic_papi_api::{PaymentError, PaymentType}; use ic_papi_guard::guards::{ attached_cycles::AttachedCyclesPayment, caller_pays_icrc2_tokens::CallerPaysIcrc2TokensPaymentGuard, - icrc2_cycles::Icrc2CyclesPaymentGuard, + caller_pays_icrc2_cycles::Icrc2CyclesPaymentGuard, }; use ic_papi_guard::guards::{PaymentContext, PaymentGuard, PaymentGuard2}; use state::{set_init_args, PAYMENT_GUARD}; diff --git a/src/guard/src/guards/any.rs b/src/guard/src/guards/any.rs index 9c467fb..92e7cae 100644 --- a/src/guard/src/guards/any.rs +++ b/src/guard/src/guards/any.rs @@ -9,7 +9,7 @@ use ic_papi_api::{ use super::{ attached_cycles::AttachedCyclesPayment, caller_pays_icrc2_tokens::CallerPaysIcrc2TokensPaymentGuard, - icrc2_cycles::Icrc2CyclesPaymentGuard, + caller_pays_icrc2_cycles::Icrc2CyclesPaymentGuard, patron_pays_icrc2_tokens::PatronPaysIcrc2TokensPaymentGuard, PaymentContext, PaymentGuard, PaymentGuard2, }; diff --git a/src/guard/src/guards/icrc2_cycles.rs b/src/guard/src/guards/icrc2_cycles.rs deleted file mode 100644 index 2db66ad..0000000 --- a/src/guard/src/guards/icrc2_cycles.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! 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 Icrc2CyclesPaymentGuard { - /// The payer - pub payer_account: Account, - /// The spender, if different from the payer. - pub spender_subaccount: Option, - /// Own canister ID - pub own_canister_id: Principal, -} -impl Icrc2CyclesPaymentGuard { - #[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 Icrc2CyclesPaymentGuard { - fn default() -> Self { - Self { - payer_account: Self::default_account(), - own_canister_id: ic_cdk::api::id(), - spender_subaccount: None, - } - } -} - -impl PaymentGuard for Icrc2CyclesPaymentGuard { - 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(|_| ()) - } -} diff --git a/src/guard/src/guards/mod.rs b/src/guard/src/guards/mod.rs index 25c92b7..d73c9f9 100644 --- a/src/guard/src/guards/mod.rs +++ b/src/guard/src/guards/mod.rs @@ -5,7 +5,8 @@ use ic_papi_api::{caller::TokenAmount, PaymentError, PaymentType}; pub mod any; pub mod attached_cycles; pub mod caller_pays_icrc2_tokens; -pub mod icrc2_cycles; +pub mod caller_pays_icrc2_cycles; +pub mod patron_pays_icrc2_cycles; pub mod patron_pays_icrc2_tokens; #[allow(async_fn_in_trait)] From 2163e72d7c3c50d7a3d17f7c951b9346b7470dcc Mon Sep 17 00:00:00 2001 From: Max Murphy Date: Tue, 1 Oct 2024 11:34:43 +0200 Subject: [PATCH 2/4] rename --- src/example/paid_service/src/lib.rs | 4 ++-- src/guard/src/guards/any.rs | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/example/paid_service/src/lib.rs b/src/example/paid_service/src/lib.rs index f37e858..78f2822 100644 --- a/src/example/paid_service/src/lib.rs +++ b/src/example/paid_service/src/lib.rs @@ -8,7 +8,7 @@ use ic_papi_api::{PaymentError, PaymentType}; use ic_papi_guard::guards::{ attached_cycles::AttachedCyclesPayment, caller_pays_icrc2_tokens::CallerPaysIcrc2TokensPaymentGuard, - caller_pays_icrc2_cycles::Icrc2CyclesPaymentGuard, + caller_pays_icrc2_cycles::CallerPaysIcrc2CyclesPaymentGuard, }; use ic_papi_guard::guards::{PaymentContext, PaymentGuard, PaymentGuard2}; use state::{set_init_args, PAYMENT_GUARD}; @@ -35,7 +35,7 @@ async fn cost_1000_attached_cycles() -> Result { /// 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 { - Icrc2CyclesPaymentGuard::default() + CallerPaysIcrc2CyclesPaymentGuard::default() .deduct(1_000_000_000) .await?; Ok("Yes, you paid 1 billion cycles!".to_string()) diff --git a/src/guard/src/guards/any.rs b/src/guard/src/guards/any.rs index 92e7cae..e16e1cf 100644 --- a/src/guard/src/guards/any.rs +++ b/src/guard/src/guards/any.rs @@ -7,11 +7,7 @@ use ic_papi_api::{ }; use super::{ - attached_cycles::AttachedCyclesPayment, - caller_pays_icrc2_tokens::CallerPaysIcrc2TokensPaymentGuard, - caller_pays_icrc2_cycles::Icrc2CyclesPaymentGuard, - patron_pays_icrc2_tokens::PatronPaysIcrc2TokensPaymentGuard, PaymentContext, PaymentGuard, - PaymentGuard2, + attached_cycles::AttachedCyclesPayment, caller_pays_icrc2_cycles::CallerPaysIcrc2CyclesPaymentGuard, caller_pays_icrc2_tokens::CallerPaysIcrc2TokensPaymentGuard, patron_pays_icrc2_cycles::PatronPaysIcrc2CyclesPaymentGuard, patron_pays_icrc2_tokens::PatronPaysIcrc2TokensPaymentGuard, PaymentContext, PaymentGuard, PaymentGuard2 }; /// A guard that accepts a user-specified payment type, providing the vendor supports it. @@ -62,7 +58,7 @@ impl PaymentGuard2 for AnyPaymentGuard { match payment_config { PaymentWithConfig::AttachedCycles => AttachedCyclesPayment {}.deduct(fee).await, PaymentWithConfig::CallerPaysIcrc2Cycles => { - Icrc2CyclesPaymentGuard { + CallerPaysIcrc2CyclesPaymentGuard { payer_account: Account { owner: caller, subaccount: None, @@ -74,10 +70,10 @@ impl PaymentGuard2 for AnyPaymentGuard { .await } PaymentWithConfig::PatronPaysIcrc2Cycles(patron) => { - Icrc2CyclesPaymentGuard { + PatronPaysIcrc2CyclesPaymentGuard { payer_account: patron, spender_subaccount: Some(principal2account(&caller)), - ..Icrc2CyclesPaymentGuard::default() + ..PatronPaysIcrc2CyclesPaymentGuard::default() } .deduct(fee) .await From da2152f3e7cb456fd5004aa812f436bede0c4a15 Mon Sep 17 00:00:00 2001 From: Max Murphy Date: Tue, 1 Oct 2024 11:41:18 +0200 Subject: [PATCH 3/4] Rename --- .../src/guards/caller_pays_icrc2_cycles.rs | 92 +++++++++++++++++++ .../src/guards/patron_pays_icrc2_cycles.rs | 92 +++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 src/guard/src/guards/caller_pays_icrc2_cycles.rs create mode 100644 src/guard/src/guards/patron_pays_icrc2_cycles.rs diff --git a/src/guard/src/guards/caller_pays_icrc2_cycles.rs b/src/guard/src/guards/caller_pays_icrc2_cycles.rs new file mode 100644 index 0000000..25cb50f --- /dev/null +++ b/src/guard/src/guards/caller_pays_icrc2_cycles.rs @@ -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 CallerPaysIcrc2CyclesPaymentGuard { + /// The payer + pub payer_account: Account, + /// The spender, if different from the payer. + pub spender_subaccount: Option, + /// Own canister ID + pub own_canister_id: Principal, +} +impl CallerPaysIcrc2CyclesPaymentGuard { + #[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 CallerPaysIcrc2CyclesPaymentGuard { + fn default() -> Self { + Self { + payer_account: Self::default_account(), + own_canister_id: ic_cdk::api::id(), + spender_subaccount: None, + } + } +} + +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 { + 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(|_| ()) + } +} diff --git a/src/guard/src/guards/patron_pays_icrc2_cycles.rs b/src/guard/src/guards/patron_pays_icrc2_cycles.rs new file mode 100644 index 0000000..c069575 --- /dev/null +++ b/src/guard/src/guards/patron_pays_icrc2_cycles.rs @@ -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, + /// 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(|_| ()) + } +} From 759693084bb02b906745047d7065f7346fe483dc Mon Sep 17 00:00:00 2001 From: Max Murphy Date: Tue, 1 Oct 2024 11:41:43 +0200 Subject: [PATCH 4/4] fmt --- src/example/paid_service/src/lib.rs | 2 +- src/guard/src/guards/any.rs | 7 ++++++- src/guard/src/guards/mod.rs | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/example/paid_service/src/lib.rs b/src/example/paid_service/src/lib.rs index 78f2822..3c6c5df 100644 --- a/src/example/paid_service/src/lib.rs +++ b/src/example/paid_service/src/lib.rs @@ -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_tokens::CallerPaysIcrc2TokensPaymentGuard, caller_pays_icrc2_cycles::CallerPaysIcrc2CyclesPaymentGuard, + caller_pays_icrc2_tokens::CallerPaysIcrc2TokensPaymentGuard, }; use ic_papi_guard::guards::{PaymentContext, PaymentGuard, PaymentGuard2}; use state::{set_init_args, PAYMENT_GUARD}; diff --git a/src/guard/src/guards/any.rs b/src/guard/src/guards/any.rs index e16e1cf..672fba4 100644 --- a/src/guard/src/guards/any.rs +++ b/src/guard/src/guards/any.rs @@ -7,7 +7,12 @@ use ic_papi_api::{ }; use super::{ - attached_cycles::AttachedCyclesPayment, caller_pays_icrc2_cycles::CallerPaysIcrc2CyclesPaymentGuard, caller_pays_icrc2_tokens::CallerPaysIcrc2TokensPaymentGuard, patron_pays_icrc2_cycles::PatronPaysIcrc2CyclesPaymentGuard, patron_pays_icrc2_tokens::PatronPaysIcrc2TokensPaymentGuard, PaymentContext, PaymentGuard, PaymentGuard2 + attached_cycles::AttachedCyclesPayment, + caller_pays_icrc2_cycles::CallerPaysIcrc2CyclesPaymentGuard, + caller_pays_icrc2_tokens::CallerPaysIcrc2TokensPaymentGuard, + patron_pays_icrc2_cycles::PatronPaysIcrc2CyclesPaymentGuard, + patron_pays_icrc2_tokens::PatronPaysIcrc2TokensPaymentGuard, PaymentContext, PaymentGuard, + PaymentGuard2, }; /// A guard that accepts a user-specified payment type, providing the vendor supports it. diff --git a/src/guard/src/guards/mod.rs b/src/guard/src/guards/mod.rs index d73c9f9..34d8f6c 100644 --- a/src/guard/src/guards/mod.rs +++ b/src/guard/src/guards/mod.rs @@ -4,8 +4,8 @@ use candid::Principal; use ic_papi_api::{caller::TokenAmount, PaymentError, PaymentType}; pub mod any; pub mod attached_cycles; -pub mod caller_pays_icrc2_tokens; pub mod caller_pays_icrc2_cycles; +pub mod caller_pays_icrc2_tokens; pub mod patron_pays_icrc2_cycles; pub mod patron_pays_icrc2_tokens;