Skip to content

Commit

Permalink
++
Browse files Browse the repository at this point in the history
  • Loading branch information
bitdivine committed Sep 10, 2024
1 parent 66473c0 commit 66504b9
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 27 deletions.
17 changes: 15 additions & 2 deletions src/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use candid::{CandidType, Deserialize, 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]
Expand All @@ -19,7 +20,7 @@ pub enum PaymentError {
},
}

#[derive(Debug, CandidType, Deserialize, Clone, Eq, PartialEq)]
#[derive(Debug, CandidType, Deserialize, Copy, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum PaymentType {
/// The caller is paying with cycles attached to the call.
Expand All @@ -28,7 +29,19 @@ pub enum PaymentType {
///
/// Note: The API does not require additional arguments to support this payment type.
AttachedCycles,
Icrc2Cycles(Icrc2Payer),
/// 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 fn principal2account(principal: &Principal) -> ByteBuf {
// TODO: This is NOT the right way.
let mut ans = principal.as_slice().to_vec();
while ans.len() < 32 {
ans.push(0);
}
ByteBuf::from(ans)
}

/// User's payment details for an ICRC2 payment.
Expand Down
12 changes: 10 additions & 2 deletions src/example/paid_service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ mod state;
use example_paid_service_api::InitArgs;
use ic_cdk::init;
use ic_cdk_macros::{export_candid, update};
use ic_papi_api::{PaymentError, PaymentType};
use ic_papi_api::{principal2account, PaymentError, PaymentType};
use ic_papi_guard::guards::PaymentGuard;
use ic_papi_guard::guards::{
attached_cycles::AttachedCyclesPayment, icrc2_cycles::Icrc2CyclesPaymentGuard,
};
use serde_bytes::ByteBuf;
use state::{payment_ledger, set_init_args};

#[init]
Expand Down Expand Up @@ -46,11 +47,18 @@ async fn cost_1b(payment: PaymentType) -> Result<String, PaymentError> {
PaymentType::AttachedCycles => {
AttachedCyclesPayment::default().deduct(fee).await?;
}
PaymentType::Icrc2Cycles(_payer) => {
PaymentType::CallerIcrc2 => {
let mut guard = Icrc2CyclesPaymentGuard::new();
guard.ledger_canister_id = payment_ledger();
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()));
guard.deduct(fee).await?;
}
_ => return Err(PaymentError::UnsupportedPaymentType),
};
Ok("Yes, you paid 1 billion cycles!".to_string())
Expand Down
124 changes: 101 additions & 23 deletions src/example/paid_service/tests/it/icrc2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ use crate::util::cycles_ledger::{
use crate::util::pic_canister::{PicCanister, PicCanisterBuilder, PicCanisterTrait};
use candid::{encode_one, Nat, Principal};
use example_paid_service_api::InitArgs;
use ic_papi_api::{Icrc2Payer, PaymentError, PaymentType};
use ic_papi_api::{principal2account, Icrc2Payer, PaymentError, PaymentType};
use pocket_ic::PocketIc;
use serde_bytes::ByteBuf;
use std::sync::Arc;

pub struct CallerPaysWithIcRc2TestSetup {
Expand Down Expand Up @@ -352,19 +353,7 @@ fn caller_pays_by_named_icrc2() {
// Call the API
let response: Result<String, PaymentError> = setup
.paid_service
.update(
setup.user,
api_method,
(PaymentType::Icrc2Cycles(Icrc2Payer {
account: Some(ic_papi_api::Account {
owner: setup.user,
subaccount: None,
}),
spender_subaccount: None,
ledger_canister_id: None,
created_at_time: None,
})),
)
.update(setup.user, api_method, PaymentType::CallerIcrc2)
.expect("Failed to call the paid service");
assert_eq!(
response,
Expand All @@ -390,15 +379,7 @@ fn caller_pays_by_named_icrc2() {
.update(
setup.unauthorized_user,
api_method,
(PaymentType::Icrc2Cycles(Icrc2Payer {
account: Some(ic_papi_api::Account {
owner: setup.user,
subaccount: None,
}),
spender_subaccount: None,
ledger_canister_id: None,
created_at_time: None,
})),
PaymentType::CallerIcrc2,
)
.expect("Failed to call the paid service");
assert_eq!(
Expand All @@ -419,3 +400,100 @@ fn caller_pays_by_named_icrc2() {
}
}
}

/// Here `user`` is a patron, and pays on behalf of `user2`.
#[test]
fn patron_pays_by_named_icrc2() {
let setup = CallerPaysWithIcRc2TestSetup::default();
// Add cycles to the wallet
// .. At first the balance should be zero.
setup.assert_user_balance_eq(
0u32,
"Initially the user balance in the ledger should be zero".to_string(),
);
// .. Get enough to play with lots of transactions.
const LEDGER_FEE: u128 = 100_000_000; // The documented fee: https://internetcomputer.org/docs/current/developer-docs/defi/cycles/cycles-ledger#fees
let mut expected_user_balance = 100_000_000_000; // Lots of funds to play with.
setup.fund_user(expected_user_balance);
setup.assert_user_balance_eq(
expected_user_balance,
"Test setup failed when providing the user with funds".to_string(),
);
// 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 api_fee = 1_000_000_000u128;
// Pre-approve payment
setup
.ledger
.icrc_2_approve(
setup.user,
&ApproveArgs {
spender: Account {
owner: setup.paid_service.canister_id(),
subaccount: Some(principal2account(&setup.user2)),
},
amount: Nat::from(expected_user_balance),
..ApproveArgs::default()
},
)
.expect("Failed to call the ledger to approve")
.expect("Failed to approve the paid service to spend the user's ICRC-2 tokens");
// Check that the user has been charged for the approve.
expected_user_balance -= LEDGER_FEE;
setup.assert_user_balance_eq(
expected_user_balance,
"Expected the user balance to be charged for the ICRC2 approve".to_string(),
);
// Now make several identical API calls
for _repetition in 0..5 {
// Check the balance beforehand
let service_canister_cycles_before =
setup.pic.cycle_balance(setup.paid_service.canister_id);
// Call the API
let payment_arg = PaymentType::PatronIcrc2(setup.user);
let response: Result<String, PaymentError> = setup
.paid_service
.update(setup.user2, api_method, payment_arg)
.expect("Failed to call the paid service");
assert_eq!(
response,
Ok("Yes, you paid 1 billion cycles!".to_string()),
"Should have succeeded with a generous prepayment",
);
let service_canister_cycles_after = setup.pic.cycle_balance(setup.paid_service.canister_id);
assert!(
service_canister_cycles_after > service_canister_cycles_before,
"The service canister needs to charge more to cover its cycle cost! Loss: {}",
service_canister_cycles_before - service_canister_cycles_after
);
expected_user_balance -= api_fee + LEDGER_FEE;
setup.assert_user_balance_eq(
expected_user_balance,
"Expected the user balance to be the initial balance minus the ledger and API fees"
.to_string(),
);
// But an unauthorized user should not be able to make the same call.
{
let response: Result<String, PaymentError> = setup
.paid_service
.update(setup.unauthorized_user, api_method, payment_arg)
.expect("Failed to call the paid service");
assert_eq!(
response,
Err(PaymentError::LedgerError {
ledger: setup.ledger.canister_id(),
error: cycles_ledger_client::WithdrawFromError::InsufficientAllowance {
allowance: Nat::from(0u32),
}
}),
"Should have succeeded with a generous prepayment",
);
setup.assert_user_balance_eq(
expected_user_balance,
"The user should not have been charged for unauthorized spending attempts"
.to_string(),
);
}
}
}
1 change: 1 addition & 0 deletions src/guard/src/guards/icrc2_cycles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ impl Icrc2CyclesPaymentGuard {
)
.expect("Failed to parse cycles ledger canister ID")
}
/// A default payment guard for ICRC-2 cycles.
pub fn new() -> Self {
Self {
payer_account: Self::default_account(),
Expand Down

0 comments on commit 66504b9

Please sign in to comment.