diff --git a/src/api/src/caller.rs b/src/api/src/caller.rs new file mode 100644 index 0000000..b3d1451 --- /dev/null +++ b/src/api/src/caller.rs @@ -0,0 +1,19 @@ +//! Types used primarily by the caller of the payment API. +use candid::{CandidType, Deserialize, Principal}; +pub use cycles_ledger_client::Account; + +/// How a caller states that they will pay. +#[derive(Debug, CandidType, Deserialize, Copy, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum PaymentType { + /// The caller is paying with cycles attached to the call. + /// + /// Note: This is not available for ingress messages. + /// + /// Note: The API does not require additional arguments to support this payment type. + AttachedCycles, + /// The caller is paying with cycles from their main account on the cycles ledger. + CallerIcrc2Cycles, + /// A patron is paying, on behalf of the caller, from their main account on the cycles ledger. + PatronIcrc2Cycles(Principal), +} diff --git a/src/api/src/error.rs b/src/api/src/error.rs new file mode 100644 index 0000000..019869f --- /dev/null +++ b/src/api/src/error.rs @@ -0,0 +1,21 @@ +//! Payment API error types. +use candid::{CandidType, Deserialize, Principal}; +pub use cycles_ledger_client::Account; +use cycles_ledger_client::WithdrawFromError; + +#[derive(Debug, CandidType, Deserialize, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum PaymentError { + UnsupportedPaymentType, + LedgerUnreachable { + ledger: Principal, + }, + LedgerError { + ledger: Principal, + error: WithdrawFromError, + }, + InsufficientFunds { + needed: u64, + available: u64, + }, +} diff --git a/src/api/src/lib.rs b/src/api/src/lib.rs index 1f6f8c3..a5bff2f 100644 --- a/src/api/src/lib.rs +++ b/src/api/src/lib.rs @@ -1,39 +1,13 @@ -use candid::{CandidType, Deserialize, Principal}; +use candid::Principal; pub use cycles_ledger_client::Account; -use cycles_ledger_client::WithdrawFromError; use serde_bytes::ByteBuf; -#[derive(Debug, CandidType, Deserialize, Clone, Eq, PartialEq)] -#[non_exhaustive] -pub enum PaymentError { - UnsupportedPaymentType, - LedgerUnreachable { - ledger: Principal, - }, - LedgerError { - ledger: Principal, - error: WithdrawFromError, - }, - InsufficientFunds { - needed: u64, - available: u64, - }, -} - -#[derive(Debug, CandidType, Deserialize, Copy, Clone, Eq, PartialEq)] -#[non_exhaustive] -pub enum PaymentType { - /// The caller is paying with cycles attached to the call. - /// - /// Note: This is not available for ingress messages. - /// - /// Note: The API does not require additional arguments to support this payment type. - AttachedCycles, - /// The caller is paying with cycles from their main account on the (by default cycles) ledger. - CallerIcrc2, - /// A patron is paying, on behalf of the caller, from their main account on the (by default cycles) ledger. - PatronIcrc2(Principal), -} +pub mod caller; +pub mod error; +pub mod vendor; +pub use caller::PaymentType; +pub use error::PaymentError; +pub use vendor::Icrc2Payer; pub fn principal2account(principal: &Principal) -> ByteBuf { // TODO: This is NOT the right way. @@ -43,20 +17,3 @@ pub fn principal2account(principal: &Principal) -> ByteBuf { } ByteBuf::from(ans) } - -/// User's payment details for an ICRC2 payment. -#[derive(Debug, CandidType, Deserialize, Clone, Eq, PartialEq)] -pub struct Icrc2Payer { - /// The customer's principal and (optionally) subaccount. - /// - /// By default, the caller's main account is used. - pub account: Option, - /// The spender, if different from the payer. - pub spender_subaccount: Option, - /// The ledger canister ID. - /// - /// Note: This is included in order to improve error messages if the caller tries to use the wrong ledger. - pub ledger_canister_id: Option, - /// Corresponds to the `created_at_time` field in ICRC2. - pub created_at_time: Option, -} diff --git a/src/api/src/vendor.rs b/src/api/src/vendor.rs new file mode 100644 index 0000000..0101de1 --- /dev/null +++ b/src/api/src/vendor.rs @@ -0,0 +1,20 @@ +//! Types used primartily by the vendor of the payment API. +use candid::{CandidType, Deserialize, Principal}; +pub use cycles_ledger_client::Account; + +/// User's payment details for an ICRC2 payment. +#[derive(Debug, CandidType, Deserialize, Clone, Eq, PartialEq)] +pub struct Icrc2Payer { + /// The customer's principal and (optionally) subaccount. + /// + /// By default, the caller's main account is used. + pub account: Option, + /// The spender, if different from the payer. + pub spender_subaccount: Option, + /// The ledger canister ID. + /// + /// Note: This is included in order to improve error messages if the caller tries to use the wrong ledger. + pub ledger_canister_id: Option, + /// Corresponds to the `created_at_time` field in ICRC2. + pub created_at_time: Option, +} diff --git a/src/example/paid_service/src/lib.rs b/src/example/paid_service/src/lib.rs index b5c8125..9dda2a5 100644 --- a/src/example/paid_service/src/lib.rs +++ b/src/example/paid_service/src/lib.rs @@ -32,8 +32,10 @@ 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 cost_1b_icrc2_from_caller() -> Result { - let mut guard = Icrc2CyclesPaymentGuard::new(); - guard.ledger_canister_id = payment_ledger(); + let guard = Icrc2CyclesPaymentGuard { + ledger_canister_id: payment_ledger(), + ..Icrc2CyclesPaymentGuard::default() + }; guard.deduct(1_000_000_000).await?; Ok("Yes, you paid 1 billion cycles!".to_string()) } @@ -46,16 +48,23 @@ async fn cost_1b(payment: PaymentType) -> Result { PaymentType::AttachedCycles => { AttachedCyclesPayment::default().deduct(fee).await?; } - PaymentType::CallerIcrc2 => { - let mut guard = Icrc2CyclesPaymentGuard::new(); - guard.ledger_canister_id = payment_ledger(); + PaymentType::CallerIcrc2Cycles => { + let guard = Icrc2CyclesPaymentGuard { + ledger_canister_id: payment_ledger(), + ..Icrc2CyclesPaymentGuard::default() + }; guard.deduct(fee).await?; } - PaymentType::PatronIcrc2(patron) => { - let mut guard = Icrc2CyclesPaymentGuard::new(); - guard.ledger_canister_id = payment_ledger(); - guard.payer_account.owner = patron; - guard.spender_subaccount = Some(principal2account(&ic_cdk::caller())); + PaymentType::PatronIcrc2Cycles(patron) => { + let guard = Icrc2CyclesPaymentGuard { + ledger_canister_id: payment_ledger(), + payer_account: ic_papi_api::Account { + owner: patron, + subaccount: None, + }, + spender_subaccount: Some(principal2account(&ic_cdk::caller())), + ..Icrc2CyclesPaymentGuard::default() + }; guard.deduct(fee).await?; } _ => return Err(PaymentError::UnsupportedPaymentType), diff --git a/src/example/paid_service/tests/it/icrc2.rs b/src/example/paid_service/tests/it/icrc2.rs index 038ce9e..4eeeefa 100644 --- a/src/example/paid_service/tests/it/icrc2.rs +++ b/src/example/paid_service/tests/it/icrc2.rs @@ -368,7 +368,7 @@ fn caller_pays_by_named_icrc2() { // Call the API let response: Result = setup .paid_service - .update(setup.user, api_method, PaymentType::CallerIcrc2) + .update(setup.user, api_method, PaymentType::CallerIcrc2Cycles) .expect("Failed to call the paid service"); assert_eq!( response, @@ -394,7 +394,7 @@ fn caller_pays_by_named_icrc2() { .update( setup.unauthorized_user, api_method, - PaymentType::CallerIcrc2, + PaymentType::CallerIcrc2Cycles, ) .expect("Failed to call the paid service"); assert_eq!( @@ -439,7 +439,7 @@ fn patron_pays_by_named_icrc2() { // Ok, now we should be able to make an API call with EITHER an ICRC-2 approve or attached cycles, by declaring the payment type. // In this test, we will exercise the ICRC-2 approve. let api_method = "cost_1b"; - let payment_arg = PaymentType::PatronIcrc2(setup.user); + let payment_arg = PaymentType::PatronIcrc2Cycles(setup.user); let api_fee = 1_000_000_000u128; let repetitions = 3; // Pre-approve payments diff --git a/src/guard/src/guards/icrc2_cycles.rs b/src/guard/src/guards/icrc2_cycles.rs index 1af3e26..243e097 100644 --- a/src/guard/src/guards/icrc2_cycles.rs +++ b/src/guard/src/guards/icrc2_cycles.rs @@ -28,8 +28,10 @@ impl Icrc2CyclesPaymentGuard { ) .expect("Failed to parse cycles ledger canister ID") } - /// A default payment guard for ICRC-2 cycles. - pub fn new() -> Self { +} + +impl Default for Icrc2CyclesPaymentGuard { + fn default() -> Self { Self { payer_account: Self::default_account(), ledger_canister_id: Self::default_cycles_ledger(), @@ -61,16 +63,14 @@ impl PaymentGuard for Icrc2CyclesPaymentGuard { } })? .0 - .map_err(|e| { + .map_err(|error| { eprintln!( - "Failed to withdraw from ledger canister at {}: {e:?}", + "Failed to withdraw from ledger canister at {}: {error:?}", self.ledger_canister_id ); - match e { - error => PaymentError::LedgerError { - ledger: self.ledger_canister_id, - error, - }, + PaymentError::LedgerError { + ledger: self.ledger_canister_id, + error, } }) .map(|_| ())