diff --git a/Cargo.lock b/Cargo.lock index 3da9d26..e07220e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -299,6 +299,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "cycles_ledger_funding" +version = "0.1.0" +dependencies = [ + "candid", + "canfund", + "ic-cdk", + "ic-cdk-macros", + "icrc-ledger-types", + "serde", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -830,6 +842,7 @@ version = "0.0.1" dependencies = [ "candid", "ic-ledger-types", + "icrc-ledger-types", "lazy_static", "pocket-ic", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index fd12999..17279aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "canfund-rs", "examples/simple_funding", "examples/advanced_funding", + "examples/cycles_ledger_funding", "tests/integration", ] resolver = "2" diff --git a/canfund-rs/src/api/ledger.rs b/canfund-rs/src/api/ledger.rs index 23ff4ad..24c51de 100644 --- a/canfund-rs/src/api/ledger.rs +++ b/canfund-rs/src/api/ledger.rs @@ -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 { @@ -25,6 +27,35 @@ impl LedgerCanister for IcLedgerCanister { } } +#[async_trait] +pub trait WithdrawableLedgerCanister: Send + Sync { + async fn withdraw(&self, args: WithdrawArgs) -> CallResult>; +} + +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> { + let (result,) = ic_cdk::call::<(WithdrawArgs,), (Result,)>( + self.canister_id, + "withdraw", + (args,), + ) + .await?; + + Ok(result) + } +} + #[cfg(test)] pub mod test { use std::sync::Arc; @@ -51,4 +82,26 @@ pub mod test { Ok(Ok(0)) } } + + #[derive(Default)] + pub struct TestCyclesLedgerCanister { + pub transfer_called_with: Arc>>, + pub returns_with: Option>>, + } + #[async_trait] + impl WithdrawableLedgerCanister for TestCyclesLedgerCanister { + async fn withdraw( + &self, + args: WithdrawArgs, + ) -> CallResult> { + 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())) + } + } } diff --git a/canfund-rs/src/operations/obtain.rs b/canfund-rs/src/operations/obtain.rs index 79be8d2..bceab89 100644 --- a/canfund-rs/src/operations/obtain.rs +++ b/canfund-rs/src/operations/obtain.rs @@ -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 { @@ -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 } } @@ -193,12 +197,93 @@ impl MintCycles { } } +pub struct WithdrawFromLedger { + pub ledger: Arc, + pub from_subaccount: Option, +} + +#[async_trait] +impl ObtainCycles for WithdrawFromLedger { + async fn obtain_cycles( + &self, + amount: u128, + target_canister_id: Principal, + ) -> Result { + 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 { + 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() { @@ -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) + )); + } } diff --git a/canfund-rs/src/types.rs b/canfund-rs/src/types.rs index fe66d59..67ce121 100644 --- a/canfund-rs/src/types.rs +++ b/canfund-rs/src/types.rs @@ -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); @@ -19,3 +22,44 @@ pub struct HttpResponse { #[serde(with = "serde_bytes")] pub body: Vec, } + +pub type NumCycles = Nat; +#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct WithdrawArgs { + #[serde(default)] + pub from_subaccount: Option, + pub to: Principal, + #[serde(default)] + pub created_at_time: Option, + 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, + rejection_code: RejectionCode, + rejection_reason: String, + }, + GenericError { + error_code: Nat, + message: String, + }, + InvalidReceiver { + receiver: Principal, + }, +} diff --git a/examples/cycles_ledger_funding/Cargo.toml b/examples/cycles_ledger_funding/Cargo.toml new file mode 100644 index 0000000..3a2d818 --- /dev/null +++ b/examples/cycles_ledger_funding/Cargo.toml @@ -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'] } diff --git a/examples/cycles_ledger_funding/cycles_ledger_funding.did b/examples/cycles_ledger_funding/cycles_ledger_funding.did new file mode 100644 index 0000000..d2ba2d6 --- /dev/null +++ b/examples/cycles_ledger_funding/cycles_ledger_funding.did @@ -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) +} diff --git a/examples/cycles_ledger_funding/src/lib.rs b/examples/cycles_ledger_funding/src/lib.rs new file mode 100644 index 0000000..3d99ea9 --- /dev/null +++ b/examples/cycles_ledger_funding/src/lib.rs @@ -0,0 +1,163 @@ +use candid::{self, CandidType, Deserialize, Nat, Principal}; +use canfund::api::ledger::CyclesLedgerCanister; +use canfund::operations::obtain::WithdrawFromLedger; +use canfund::{ + manager::{ + options::{ + CyclesThreshold, EstimatedRuntime, FundManagerOptions, FundStrategy, + ObtainCyclesOptions, + }, + RegisterOpts, + }, + operations::fetch::FetchCyclesBalanceFromCanisterStatus, + FundManager, +}; +use ic_cdk::api::call::call_with_payment128; +use ic_cdk::{id, query}; +use ic_cdk_macros::{init, post_upgrade, update}; +use icrc_ledger_types::icrc1::account::Account; +use icrc_ledger_types::icrc1::transfer::Memo; +use std::{cell::RefCell, sync::Arc}; + +thread_local! { + /// Monitor the cycles of canisters and top up if necessary. + pub static FUND_MANAGER: RefCell = RefCell::new(FundManager::new()); +} + +#[derive(CandidType, Deserialize)] +pub struct FundingConfig { + pub funded_canister_ids: Vec, +} + +#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct DepositArg { + pub cycles: u128, + pub to: Account, + pub memo: Option, +} + +#[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct DepositResult { + pub block_index: Nat, + pub balance: Nat, +} + +#[init] +fn initialize(config: FundingConfig) { + start_canister_cycles_monitoring(config); +} + +#[post_upgrade] +fn post_upgrade(config: FundingConfig) { + start_canister_cycles_monitoring(config); +} + +pub fn start_canister_cycles_monitoring(config: FundingConfig) { + FUND_MANAGER.with(|fund_manager| { + let mut fund_manager = fund_manager.borrow_mut(); + + let mut fund_manager_options = FundManagerOptions::new() + .with_interval_secs(12 * 60 * 60) // twice a day + .with_strategy(FundStrategy::BelowEstimatedRuntime( + EstimatedRuntime::new() + .with_min_runtime_secs(2 * 24 * 60 * 60) // 2 day + .with_fund_runtime_secs(5 * 24 * 60 * 60) // 3 days + .with_max_runtime_cycles_fund(1_000_000_000_000) + .with_fallback_min_cycles(400_000_000_000) + .with_fallback_fund_cycles(250_000_000_000), + )); + + fund_manager_options = + fund_manager_options.with_obtain_cycles_options(get_obtain_cycles_config()); + + fund_manager.with_options(fund_manager_options); + + for canister_id in config.funded_canister_ids { + fund_manager.register( + canister_id, + RegisterOpts::new() + .with_cycles_fetcher(Arc::new(FetchCyclesBalanceFromCanisterStatus::new())) + .with_obtain_cycles_options(get_obtain_cycles_config().unwrap()), + ); + } + + // The funding canister itself can also be monitored. + fund_manager.register( + id(), + RegisterOpts::new() + .with_cycles_fetcher(Arc::new(FetchCyclesBalanceFromCanisterStatus::new())) + .with_strategy(FundStrategy::BelowThreshold( + CyclesThreshold::new() + .with_min_cycles(500_000_000_000) + .with_fund_cycles(750_000_000_000), + )), + ); + + fund_manager.start(); + }); +} + +pub const MAINNET_CYCLES_LEDGER_CANISTER_ID: Principal = + Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x02, 0x01, 0x01]); +// Default subaccount for minting cycles is derived from the canister's account. +pub fn get_obtain_cycles_config() -> Option { + Some(ObtainCyclesOptions { + obtain_cycles: Arc::new(WithdrawFromLedger { + ledger: Arc::new(CyclesLedgerCanister::new(MAINNET_CYCLES_LEDGER_CANISTER_ID)), + from_subaccount: None, + }), + }) +} + +#[derive(CandidType, Deserialize)] +pub struct GetDepositedCyclesRetItem { + pub deposited_cycles: u128, + pub canister_id: Principal, +} + +#[query(name = "get_deposited_cycles")] +fn get_deposited_cycles() -> Vec { + FUND_MANAGER.with(|fund_manager| { + let fund_manager = fund_manager.borrow(); + + fund_manager + .get_canisters() + .iter() + .map(|(canister_id, record)| { + let deposited_cycles = record + .get_deposited_cycles() + .as_ref() + .map_or(0, |c| c.amount); + GetDepositedCyclesRetItem { + deposited_cycles, + canister_id: *canister_id, + } + }) + .collect() + }) +} + +#[update] +async fn deposit(arg: DepositArg) -> DepositResult { + #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] + pub struct CallDepositArg { + pub to: Account, + pub memo: Option, + } + + let call_arg = CallDepositArg { + to: arg.to, + memo: arg.memo, + }; + + let cycles = arg.cycles; + let (result,): (DepositResult,) = call_with_payment128( + MAINNET_CYCLES_LEDGER_CANISTER_ID, + "deposit", + (call_arg,), + cycles, + ) + .await + .expect("deposit call failed"); + result +} diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index d7df6e1..c8ebccf 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -8,6 +8,7 @@ edition = '2021' candid = { workspace = true } simple_funding = { path = '../../examples/simple_funding', version = '0.1.0' } ic-ledger-types = { workspace = true } +icrc-ledger-types = { workspace = true } lazy_static = { workspace = true } pocket-ic = { workspace = true } reqwest = { workspace = true } diff --git a/tests/integration/build.rs b/tests/integration/build.rs index d8b0f3e..706f091 100644 --- a/tests/integration/build.rs +++ b/tests/integration/build.rs @@ -260,6 +260,21 @@ pub fn download_cmc_wasm() { .expect("Failed to download cmc.wasm.gz"); } +pub fn download_cycles_ledger_wasm() { + if WASM_OUT_DIR.join("cycles_ledger.wasm.gz").exists() { + println!("Cycles ledger Wasm already available, skipping download"); + return; + } + + let cycles_ledger_wasm = "https://github.com/dfinity/cycles-ledger/releases/download/cycles-ledger-v1.0.3/cycles-ledger.wasm.gz"; + + download( + cycles_ledger_wasm, + &WASM_OUT_DIR.join("cycles_ledger.wasm.gz"), + ) + .expect("Failed to download cycles_ledger.wasm.gz"); +} + pub fn main() { for dep in DEPENDENCIES.iter() { println!("cargo:rerun-if-changed={}", dep.display()); @@ -269,6 +284,7 @@ pub fn main() { download_ic_wasm(); download_icp_ledger_wasm(); download_cmc_wasm(); + download_cycles_ledger_wasm(); // Build all wasm packages that are part of the benchmark suite for package in EXAMPLES_WASMS.iter() { diff --git a/tests/integration/src/cycles_monitor_tests.rs b/tests/integration/src/cycles_monitor_tests.rs index 8bb059d..89959f5 100644 --- a/tests/integration/src/cycles_monitor_tests.rs +++ b/tests/integration/src/cycles_monitor_tests.rs @@ -1,16 +1,18 @@ -use std::time::Duration; - -use ic_ledger_types::{AccountIdentifier, DEFAULT_SUBACCOUNT}; - use crate::interfaces::{ get_icp_account_balance, query_deposited_cycles, send_icp_to_account, ICP, }; use crate::setup::{ - create_canister_with_cycles, install_advanced_funding_canister, install_funded_canister, + create_canister_with_cycles, install_advanced_funding_canister, + install_cycles_ledger_funding_canister, install_funded_canister, install_simple_funding_canister, setup_new_env, }; use crate::utils::{advance_time_to_burn_cycles, controller_test_id}; use crate::TestEnv; +use candid::{CandidType, Deserialize, Encode}; +use ic_ledger_types::{AccountIdentifier, DEFAULT_SUBACCOUNT}; +use icrc_ledger_types::icrc1::account::Account; +use icrc_ledger_types::icrc1::transfer::Memo; +use std::time::Duration; #[test] fn successfully_monitors_funded_canister_and_tops_up() { @@ -193,3 +195,69 @@ fn can_mint_cycles_to_top_up_self() { && post_cycle_balance - pre_cycle_balance < 750_000_000_000 ); } + +#[test] +fn can_obtain_from_cycles_ledger_to_top_up_self() { + let TestEnv { + env, controller, .. + } = setup_new_env(); + + let cycles_ledger_funding_canister_id = + create_canister_with_cycles(&env, controller, 1_400_000_000_000); + + // install the funding canister to start monitoring itself and mint cycles + install_cycles_ledger_funding_canister( + &env, + controller, + cycles_ledger_funding_canister_id, + vec![], + ); + + #[derive(CandidType, Clone, Debug, PartialEq, Eq)] + pub struct DepositArg { + pub to: Account, + pub memo: Option, + pub cycles: u128, + } + + #[derive(CandidType, Deserialize, Clone, Debug, PartialEq, Eq)] + pub struct DepositResult { + pub block_index: u64, + pub balance: u64, + } + + let arg = DepositArg { + to: Account::from(cycles_ledger_funding_canister_id), + memo: None, + cycles: 1_000_000_000_000, + }; + + env.update_call( + cycles_ledger_funding_canister_id, + controller, + "deposit", + Encode!(&arg).unwrap(), + ) + .expect("deposit call failed"); + + let pre_cycle_balance = env.cycle_balance(cycles_ledger_funding_canister_id); + + env.tick(); + env.advance_time(Duration::from_secs(24 * 60 * 60)); + env.tick(); + env.tick(); + env.tick(); + env.tick(); + env.tick(); + env.tick(); + + let post_cycle_balance = env.cycle_balance(cycles_ledger_funding_canister_id); + + assert!(post_cycle_balance > pre_cycle_balance); + + // assert that while we lose some cycles during the process, it'll be roughly what we expect + assert!( + post_cycle_balance - pre_cycle_balance > 749_000_000_000 + && post_cycle_balance - pre_cycle_balance < 750_000_000_000 + ); +} diff --git a/tests/integration/src/lib.rs b/tests/integration/src/lib.rs index 15a3466..14f4a7c 100644 --- a/tests/integration/src/lib.rs +++ b/tests/integration/src/lib.rs @@ -19,4 +19,5 @@ pub struct TestEnv { pub struct CanisterIds { pub icp_ledger: Principal, pub cycles_minting_canister: Principal, + pub cycles_ledger_canister: Principal, } diff --git a/tests/integration/src/setup.rs b/tests/integration/src/setup.rs index 78c88a4..a3c43dd 100644 --- a/tests/integration/src/setup.rs +++ b/tests/integration/src/setup.rs @@ -28,6 +28,16 @@ pub struct CyclesCanisterInitPayload { pub last_purged_notification: Option, } +#[derive(Serialize, CandidType, Clone, Debug, PartialEq, Eq)] +pub struct CyclesLedgerCanisterInitPayload { + pub max_blocks_per_request: u64, +} + +#[derive(Serialize, CandidType, Clone, Debug, PartialEq, Eq)] +pub enum CyclesLedgerInitPayload { + Init(CyclesLedgerCanisterInitPayload), +} + pub fn setup_new_env() -> TestEnv { let path = match env::var_os("POCKET_IC_BIN") { None => { @@ -96,6 +106,18 @@ fn install_canisters(env: &PocketIc, controller: Principal, minter: Principal) - .unwrap(); assert_eq!(cmc_canister_id, MAINNET_CYCLES_MINTING_CANISTER_ID); + let cycles_ledger_canister_id = env + .create_canister_with_id( + Some(controller), + None, + Principal::from_text("um5iw-rqaaa-aaaaq-qaaba-cai").unwrap(), + ) + .unwrap(); + assert_eq!( + cycles_ledger_canister_id, + Principal::from_text("um5iw-rqaaa-aaaaq-qaaba-cai").unwrap() + ); + let nns_governance_canister_id = Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap(); let controller_account = AccountIdentifier::new(&controller, &DEFAULT_SUBACCOUNT); @@ -125,7 +147,7 @@ fn install_canisters(env: &PocketIc, controller: Principal, minter: Principal) - ledger_canister_id: Some(nns_ledger_canister_id), governance_canister_id: Some(nns_governance_canister_id), minting_account_id: Some(minting_account), - cycles_ledger_canister_id: None, + cycles_ledger_canister_id: Some(cycles_ledger_canister_id), last_purged_notification: Some(0), }); env.install_canister( @@ -135,9 +157,23 @@ fn install_canisters(env: &PocketIc, controller: Principal, minter: Principal) - Some(controller), ); + let cycles_ledger_canister_wasm = get_canister_wasm("cycles_ledger").to_vec(); + let cycles_ledger_init_args: CyclesLedgerInitPayload = + CyclesLedgerInitPayload::Init(CyclesLedgerCanisterInitPayload { + max_blocks_per_request: 100, + }); + + env.install_canister( + cycles_ledger_canister_id, + cycles_ledger_canister_wasm, + Encode!(&cycles_ledger_init_args).unwrap(), + Some(controller), + ); + CanisterIds { icp_ledger: nns_ledger_canister_id, cycles_minting_canister: cmc_canister_id, + cycles_ledger_canister: cycles_ledger_canister_id, } } @@ -178,6 +214,25 @@ pub fn install_advanced_funding_canister( ); } +pub fn install_cycles_ledger_funding_canister( + env: &PocketIc, + sender: Principal, + effective_canister_id: Principal, + funded_canister_ids: Vec, +) { + let funding_canister_wasm = get_canister_wasm("cycles_ledger_funding").to_vec(); + let funding_canister_args = FundingConfig { + funded_canister_ids, + }; + + env.install_canister( + effective_canister_id, + funding_canister_wasm, + Encode!(&funding_canister_args).unwrap(), + Some(sender), + ); +} + pub fn install_funded_canister(env: &PocketIc, controller: Principal, cycles: u128) -> Principal { // simple canister to burn cycles and trigger funding rules let funded_canister_id = create_canister_with_cycles(env, controller, cycles);