Skip to content

Commit

Permalink
Add integration test for obtaining cycles from the cycles ledger
Browse files Browse the repository at this point in the history
  • Loading branch information
jedna committed Jan 8, 2025
1 parent 995591f commit 0e38fe0
Show file tree
Hide file tree
Showing 13 changed files with 564 additions and 12 deletions.
13 changes: 13 additions & 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"canfund-rs",
"examples/simple_funding",
"examples/advanced_funding",
"examples/cycles_ledger_funding",
"tests/integration",
]
resolver = "2"
Expand Down
53 changes: 53 additions & 0 deletions canfund-rs/src/api/ledger.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::types::{WithdrawArgs, WithdrawError};
use async_trait::async_trait;
use candid::Principal;
use ic_cdk::api::call::CallResult;
use ic_ledger_types::{transfer, TransferArgs, TransferResult};
use icrc_ledger_types::icrc1::transfer::BlockIndex;

#[async_trait]
pub trait LedgerCanister: Send + Sync {
Expand All @@ -25,6 +27,35 @@ impl LedgerCanister for IcLedgerCanister {
}
}

#[async_trait]
pub trait WithdrawableLedgerCanister: Send + Sync {
async fn withdraw(&self, args: WithdrawArgs) -> CallResult<Result<BlockIndex, WithdrawError>>;
}

pub struct CyclesLedgerCanister {
canister_id: Principal,
}

impl CyclesLedgerCanister {
pub fn new(canister_id: Principal) -> Self {
Self { canister_id }
}
}

#[async_trait]
impl WithdrawableLedgerCanister for CyclesLedgerCanister {
async fn withdraw(&self, args: WithdrawArgs) -> CallResult<Result<BlockIndex, WithdrawError>> {
let (result,) = ic_cdk::call::<(WithdrawArgs,), (Result<BlockIndex, WithdrawError>,)>(
self.canister_id,
"withdraw",
(args,),
)
.await?;

Ok(result)
}
}

#[cfg(test)]
pub mod test {
use std::sync::Arc;
Expand All @@ -51,4 +82,26 @@ pub mod test {
Ok(Ok(0))
}
}

#[derive(Default)]
pub struct TestCyclesLedgerCanister {
pub transfer_called_with: Arc<RwLock<Vec<WithdrawArgs>>>,
pub returns_with: Option<CallResult<Result<BlockIndex, WithdrawError>>>,
}
#[async_trait]
impl WithdrawableLedgerCanister for TestCyclesLedgerCanister {
async fn withdraw(
&self,
args: WithdrawArgs,
) -> CallResult<Result<BlockIndex, WithdrawError>> {
let mut locked = self.transfer_called_with.write().await;
locked.push(args);

if let Some(value) = &self.returns_with {
return value.clone();
}

Ok(Ok(0_u64.into()))
}
}
}
116 changes: 111 additions & 5 deletions canfund-rs/src/operations/obtain.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use std::sync::Arc;

use async_trait::async_trait;
use candid::Principal;
use ic_ledger_types::{Memo, Subaccount, Tokens, TransferArgs};

use crate::api::cmc::GetIcpXdrResult;
use crate::api::ledger::WithdrawableLedgerCanister;
use crate::api::{
cmc::{CyclesMintingCanister, NotifyError, NotifyTopUpResult},
ledger::LedgerCanister,
};
use crate::types::{WithdrawArgs, WithdrawError};
use async_trait::async_trait;
use candid::Principal;
use ic_ledger_types::{Memo, Subaccount, Tokens, TransferArgs};
use icrc_ledger_types::icrc1::account;
use icrc_ledger_types::icrc1::transfer::BlockIndex;

#[derive(Debug)]
pub struct ObtainCycleError {
Expand Down Expand Up @@ -56,7 +59,8 @@ impl ObtainCycles for MintCycles {

// notify the CMC canister about the transfer so it can mint cycles
// retry if the transaction is still processing
self.notify_cmc_top_up(block_index, target_canister_id).await
self.notify_cmc_top_up(block_index, target_canister_id)
.await
}
}

Expand Down Expand Up @@ -193,12 +197,93 @@ impl MintCycles {
}
}

pub struct WithdrawFromLedger {
pub ledger: Arc<dyn WithdrawableLedgerCanister>,
pub from_subaccount: Option<account::Subaccount>,
}

#[async_trait]
impl ObtainCycles for WithdrawFromLedger {
async fn obtain_cycles(
&self,
amount: u128,
target_canister_id: Principal,
) -> Result<u128, ObtainCycleError> {
self.withdraw(amount, target_canister_id).await?;
Ok(amount)
}
}

impl WithdrawFromLedger {
/// # Errors
/// Returns an error if the withdrawal fails.
pub async fn withdraw(
&self,
amount: u128,
to: Principal,
) -> Result<BlockIndex, ObtainCycleError> {
let call_result = self
.ledger
.withdraw(WithdrawArgs {
amount: amount.into(),
from_subaccount: self.from_subaccount,
to,
created_at_time: None,
})
.await
.map_err(|err| ObtainCycleError {
details: format!("rejection_code: {:?}, err: {}", err.0, err.1),
can_retry: true,
})?;

call_result.map_err(|err| ObtainCycleError {
details: match &err {
WithdrawError::BadFee { expected_fee } => {
format!("Bad fee, expected: {expected_fee}")
}
WithdrawError::InsufficientFunds { balance } => {
format!("Insufficient balance, balance: {balance}")
}
WithdrawError::TooOld => "Tx too old".to_string(),
WithdrawError::CreatedInFuture { .. } => "Tx created in future".to_string(),
WithdrawError::Duplicate { duplicate_of } => {
format!("Tx duplicate, duplicate_of: {duplicate_of}")
}
WithdrawError::FailedToWithdraw {
rejection_code,
rejection_reason,
..
} => {
format!(
"Failed to withdraw. Code:{rejection_code:?}, reason:{rejection_reason}"
)
}
WithdrawError::TemporarilyUnavailable => {
"Ledger temporarily unavailable".to_string()
}
WithdrawError::GenericError {
error_code,
message,
} => {
format!("Error occurred. Code: {error_code}, message: {message}")
}
WithdrawError::InvalidReceiver { receiver } => {
format!("Invalid receiver: {receiver}")
}
},
can_retry: matches!(&err, WithdrawError::CreatedInFuture { .. }),
})
}
}

#[cfg(test)]
mod test {
use ic_cdk::api::call::RejectionCode;

use super::*;
use crate::api::ledger::test::TestCyclesLedgerCanister;
use crate::api::{cmc::test::TestCmcCanister, ledger::test::TestLedgerCanister};
use crate::types::NumCycles;

#[tokio::test]
async fn test_obtain_by_minting() {
Expand Down Expand Up @@ -277,4 +362,25 @@ mod test {
}
}
}

#[tokio::test]
async fn test_obtain_from_ledger() {
let ledger = Arc::new(TestCyclesLedgerCanister::default());

let obtain = WithdrawFromLedger {
ledger: ledger.clone(),
from_subaccount: None,
};

obtain
.obtain_cycles(1_000_000_000_000, Principal::anonymous())
.await
.expect("obtain_cycles failed");

// calls to transfer ICP to the CMC account
assert!(matches!(
ledger.transfer_called_with.read().await.first(),
Some(WithdrawArgs { amount, .. }) if amount == &NumCycles::from(1_000_000_000_000u64)
));
}
}
46 changes: 45 additions & 1 deletion canfund-rs/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use candid::{CandidType, Deserialize};
use candid::{CandidType, Deserialize, Nat, Principal};
use ic_cdk::api::call::RejectionCode;
use icrc_ledger_types::icrc1::account::Subaccount;
use icrc_ledger_types::icrc1::transfer::BlockIndex;

#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct HeaderField(pub String, pub String);
Expand All @@ -19,3 +22,44 @@ pub struct HttpResponse {
#[serde(with = "serde_bytes")]
pub body: Vec<u8>,
}

pub type NumCycles = Nat;
#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct WithdrawArgs {
#[serde(default)]
pub from_subaccount: Option<Subaccount>,
pub to: Principal,
#[serde(default)]
pub created_at_time: Option<u64>,
pub amount: NumCycles,
}

#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum WithdrawError {
BadFee {
expected_fee: NumCycles,
},
InsufficientFunds {
balance: NumCycles,
},
TooOld,
CreatedInFuture {
ledger_time: u64,
},
TemporarilyUnavailable,
Duplicate {
duplicate_of: BlockIndex,
},
FailedToWithdraw {
fee_block: Option<Nat>,
rejection_code: RejectionCode,
rejection_reason: String,
},
GenericError {
error_code: Nat,
message: String,
},
InvalidReceiver {
receiver: Principal,
},
}
16 changes: 16 additions & 0 deletions examples/cycles_ledger_funding/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "cycles_ledger_funding"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
path = "src/lib.rs"

[dependencies]
candid = { workspace = true }
canfund = { path = "../../canfund-rs" }
ic-cdk = { workspace = true }
ic-cdk-macros = { workspace = true }
icrc-ledger-types = { workspace = true }
serde = { workspace = true, features = ['derive'] }
15 changes: 15 additions & 0 deletions examples/cycles_ledger_funding/cycles_ledger_funding.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
type FundingConfig = record {
funded_canister_ids : vec principal;
};

type DepositArg = record { to : Account; memo : opt vec nat8; cycles : nat };

type DepositResult = record { balance : nat; block_index : nat };

service : (FundingConfig) -> {
// A method to retrieve the total of deposited cycles per canister.
get_deposited_cycles : () -> (vec record { canister_id: principal; deposited_cycles: nat128 }) query;

// A method to facilitate the deposit of cycles to the cycles ledger as Pocket IC cannot directly call with payment.
deposit : (DepositArg) -> (DepositResult)
}
Loading

0 comments on commit 0e38fe0

Please sign in to comment.