Skip to content

Commit

Permalink
refactor(guards): Separate cycle and token payment paths (#21)
Browse files Browse the repository at this point in the history
# Motivation
The cycle payment path withdraws cycles to the canister. This path is
suitable where the purpose of the payment is to fund the canister's own
running costs. This path uses `withdraw_from` which is available from
the cycles ledger but is not in the ICRC-2 standard.

# Changes
- Separate code and tests for cycle and token payment

# Tests
Pocket-ic tests are included for every path.
  • Loading branch information
bitdivine authored Sep 26, 2024
1 parent ceeded4 commit 12684b2
Show file tree
Hide file tree
Showing 22 changed files with 1,201 additions and 655 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"candid": "https://github.com/dfinity/cycles-ledger/releases/download/cycles-ledger-v1.0.1/cycles-ledger.did",
"wasm": "https://github.com/dfinity/cycles-ledger/releases/download/cycles-ledger-v1.0.1/cycles-ledger.wasm.gz",
"init_arg": "( variant { Init = record { index_id = null; max_blocks_per_request = 9_999 : nat64 }},)",
"specified_id": "um5iw-rqaaa-aaaaq-qaaba-cai",
"remote": {
"id": {
"ic": "um5iw-rqaaa-aaaaq-qaaba-cai"
Expand Down
2 changes: 1 addition & 1 deletion src/api/src/caller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub enum PaymentType {
PatronPaysIcrc2Cycles(PatronPaysIcrc2Cycles),
/// The caller is paying with tokens from their main account on the specified ledger.
CallerPaysIcrc2Tokens(CallerPaysIcrc2Tokens),
/// A patron is paying, on behalf of the caller, from their main account on the specified ledger.
/// A patron is paying, on behalf of the caller, from an account on the specified ledger.
PatronPaysIcrc2Tokens(PatronPaysIcrc2Tokens),
}

Expand Down
34 changes: 20 additions & 14 deletions src/api/src/cycles.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
use candid::Principal;

/// The cycles ledger canister ID on mainnet.
const MAINNET_CYCLES_LEDGER_CANISTER_ID: &str = "um5iw-rqaaa-aaaaq-qaaba-cai";

/// The cycles ledger canister ID.
///
/// - If a `cycles_ledger` canister is listed in `dfx.json`, the `dfx build` command will set the
/// environment variable `CANISTER_ID_CYCLES_LEDGER` and we use this to obtain the canister ID.
/// - Otherwise, the mainnet cycles ledger canister ID is used.
const CYCLES_LEDGER_CANISTER_ID: &str = if let Some(id) = option_env!("CANISTER_ID_CYCLES_LEDGER") {
id
} else {
MAINNET_CYCLES_LEDGER_CANISTER_ID
};
/// Note: This canister ID should also be used in test environments. The dfx.json
/// can implement this with:
/// ```
/// { "canisters": {
/// "cycles_ledger": {
/// ...
/// "specified_id": "um5iw-rqaaa-aaaaq-qaaba-cai",
/// "remote": {
/// "id": {
/// "ic": "um5iw-rqaaa-aaaaq-qaaba-cai"
/// }
/// }
/// },
/// ...
/// }
/// ```
pub const MAINNET_CYCLES_LEDGER_CANISTER_ID: &str = "um5iw-rqaaa-aaaaq-qaaba-cai";

/// The `CYCLES_LEDGER_CANISTER_ID` as a `Principal`.
/// The `MAINNET_CYCLES_LEDGER_CANISTER_ID` as a `Principal`.
///
/// # Panics
/// - If the `CYCLES_LEDGER_CANISTER_ID` is not a valid `Principal`.
/// - If the `MAINNET_CYCLES_LEDGER_CANISTER_ID` is not a valid `Principal`.
#[must_use]
pub fn cycles_ledger_canister_id() -> Principal {
Principal::from_text(CYCLES_LEDGER_CANISTER_ID)
Principal::from_text(MAINNET_CYCLES_LEDGER_CANISTER_ID)
.expect("Invalid cycles ledger canister ID provided at compile time.")
}
8 changes: 6 additions & 2 deletions src/api/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Payment API error types.
use candid::{CandidType, Deserialize, Principal};
pub use cycles_ledger_client::Account;
use cycles_ledger_client::WithdrawFromError;
use cycles_ledger_client::{TransferFromError, WithdrawFromError};

use crate::caller::TokenAmount;

Expand All @@ -12,10 +12,14 @@ pub enum PaymentError {
LedgerUnreachable {
ledger: Principal,
},
LedgerError {
LedgerWithdrawFromError {
ledger: Principal,
error: WithdrawFromError,
},
LedgerTransferFromError {
ledger: Principal,
error: TransferFromError,
},
InsufficientFunds {
needed: TokenAmount,
available: TokenAmount,
Expand Down
2 changes: 1 addition & 1 deletion src/declarations/cycles_ledger/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ pub struct TransferFromArgs {
pub created_at_time: Option<u64>,
pub amount: candid::Nat,
}
#[derive(CandidType, Deserialize, Debug, Clone)]
#[derive(CandidType, Deserialize, Debug, Clone, Eq, PartialEq)]
pub enum TransferFromError {
GenericError {
message: String,
Expand Down
30 changes: 22 additions & 8 deletions src/example/paid_service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ mod state;
use example_paid_service_api::InitArgs;
use ic_cdk::init;
use ic_cdk_macros::{export_candid, update};
use ic_papi_api::cycles::cycles_ledger_canister_id;
use ic_papi_api::{PaymentError, PaymentType};
use ic_papi_guard::guards::{
attached_cycles::AttachedCyclesPayment, icrc2_cycles::Icrc2CyclesPaymentGuard,
attached_cycles::AttachedCyclesPayment,
caller_pays_icrc2_tokens::CallerPaysIcrc2TokensPaymentGuard,
icrc2_cycles::Icrc2CyclesPaymentGuard,
};
use ic_papi_guard::guards::{PaymentContext, PaymentGuard, PaymentGuard2};
use state::{payment_ledger, set_init_args, PAYMENT_GUARD};
use state::{set_init_args, PAYMENT_GUARD};

#[init]
fn init(init_args: Option<InitArgs>) {
Expand All @@ -31,15 +34,26 @@ 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 guard = Icrc2CyclesPaymentGuard {
ledger_canister_id: payment_ledger(),
..Icrc2CyclesPaymentGuard::default()
};
guard.deduct(1_000_000_000).await?;
async fn caller_pays_1b_icrc2_cycles() -> Result<String, PaymentError> {
Icrc2CyclesPaymentGuard::default()
.deduct(1_000_000_000)
.await?;
Ok("Yes, you paid 1 billion cycles!".to_string())
}

/// An API method that requires 1 billion tokens (in this case cycles) using an ICRC-2 approve with default parameters.
///
/// The tokens will be transferred to the vendor's main account on the ledger.
#[update()]
async fn caller_pays_1b_icrc2_tokens() -> Result<String, PaymentError> {
CallerPaysIcrc2TokensPaymentGuard {
ledger: cycles_ledger_canister_id(),
}
.deduct(1_000_000_000)
.await?;
Ok("Yes, you paid 1 billion tokens!".to_string())
}

/// An API method that requires 1 billion cycles, paid in whatever way the client chooses.
#[update()]
async fn cost_1b(payment: PaymentType) -> Result<String, PaymentError> {
Expand Down
Loading

0 comments on commit 12684b2

Please sign in to comment.