Skip to content

Commit

Permalink
refactor(api): Move code out of lib.rs (#12)
Browse files Browse the repository at this point in the history
# Motivation
The api lib.rs is going to become one large unmanageable mess very
quickly if all the code is in one file.


# Changes
- Move api definitions into smaller files.

# Tests
See CI
  • Loading branch information
bitdivine committed Sep 18, 2024
1 parent 1d84d39 commit 9c7a366
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 72 deletions.
19 changes: 19 additions & 0 deletions src/api/src/caller.rs
Original file line number Diff line number Diff line change
@@ -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),
}
21 changes: 21 additions & 0 deletions src/api/src/error.rs
Original file line number Diff line number Diff line change
@@ -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,
},
}
57 changes: 7 additions & 50 deletions src/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<Account>,
/// The spender, if different from the payer.
pub spender_subaccount: Option<serde_bytes::ByteBuf>,
/// 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<Principal>,
/// Corresponds to the `created_at_time` field in ICRC2.
pub created_at_time: Option<u64>,
}
20 changes: 20 additions & 0 deletions src/api/src/vendor.rs
Original file line number Diff line number Diff line change
@@ -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<Account>,
/// The spender, if different from the payer.
pub spender_subaccount: Option<serde_bytes::ByteBuf>,
/// 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<Principal>,
/// Corresponds to the `created_at_time` field in ICRC2.
pub created_at_time: Option<u64>,
}
29 changes: 19 additions & 10 deletions src/example/paid_service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ 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 cost_1b_icrc2_from_caller() -> Result<String, PaymentError> {
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())
}
Expand All @@ -46,16 +48,23 @@ async fn cost_1b(payment: PaymentType) -> Result<String, PaymentError> {
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),
Expand Down
6 changes: 3 additions & 3 deletions src/example/paid_service/tests/it/icrc2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ fn caller_pays_by_named_icrc2() {
// Call the API
let response: Result<String, PaymentError> = 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,
Expand All @@ -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!(
Expand Down Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions src/guard/src/guards/icrc2_cycles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(|_| ())
Expand Down

0 comments on commit 9c7a366

Please sign in to comment.