diff --git a/scripts/bind.sh b/scripts/bind.sh new file mode 100755 index 0000000000..a09b1b769f --- /dev/null +++ b/scripts/bind.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +[[ "${1:-}" != "--help" ]] || { + cat <<-EOF + Generates candid files and bindings. + + Prerequisites: + - Deploy all canisters to the 'local' network. Or otherwise ensure that: + - Wasm for each canister is at: '.dfx/local/canisters/$CANISTER/$CANISTER.wasm.gz' + Note: You may need to set '"gzip": true' for canisters in 'dfx.json'. + - Candid for each canister is at: '.dfx/local/canisters/$CANISTER/$CANISTER.did' + EOF + + exit 0 +} + +# Generate candid for the backend +scripts/did.sh # TODO: Use local Wasm as input. +# Generate rust bindings +scripts/bind/rust.sh +# Format +scripts/format.sh diff --git a/scripts/bind/rust.sh b/scripts/bind/rust.sh new file mode 100755 index 0000000000..1584dd7675 --- /dev/null +++ b/scripts/bind/rust.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +[[ "${1:-}" != "--help" ]] || { + cat <<-EOF + Generates rust canister bindings. + + Usage: + $(basename $0) [canister_name..] + EOF + + exit 0 +} + +# If no canisters are specified, generate bindings for all. +if (($# == 0)); then + mapfile -t canisters < <(jq -r '.canisters|keys|.[]' dfx.json) +else + canisters=("${@}") +fi + +for canister in "${canisters[@]}"; do + canister_binding_config="./scripts/bind/rust/${canister}.toml" + if test -f "$canister_binding_config"; then + echo "INFO: Creating rust bindings for $canister..." + mkdir -p "src/backend/src/bind" + didc bind -t rs ".dfx/local/canisters/$canister/$canister.did" --config "$canister_binding_config" >"src/backend/src/bind/$canister.rs" + else + echo "INFO: No rust binding script for $canister at $canister_binding_config" + fi +done diff --git a/scripts/bind/rust/cycles_ledger.toml b/scripts/bind/rust/cycles_ledger.toml new file mode 100644 index 0000000000..567649eed0 --- /dev/null +++ b/scripts/bind/rust/cycles_ledger.toml @@ -0,0 +1,3 @@ +[rust] +visibility = "pub(crate)" +attributes = "#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)]" diff --git a/src/backend/src/bind.rs b/src/backend/src/bind.rs new file mode 100644 index 0000000000..e5ec3ece61 --- /dev/null +++ b/src/backend/src/bind.rs @@ -0,0 +1,3 @@ +//! Bindings to call other canisters. + +pub mod cycles_ledger; diff --git a/src/backend/src/bind/cycles_ledger.rs b/src/backend/src/bind/cycles_ledger.rs new file mode 100644 index 0000000000..d4404a492b --- /dev/null +++ b/src/backend/src/bind/cycles_ledger.rs @@ -0,0 +1,518 @@ +// This is an experimental feature to generate Rust binding from Candid. +// You may want to manually adjust some of the types. +#![allow(dead_code, unused_imports)] +use candid::{self, CandidType, Deserialize, Principal}; +use ic_cdk::api::call::CallResult as Result; + +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum ChangeIndexId { + SetTo(Principal), + Unset, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct UpgradeArgs { + pub(crate) change_index_id: Option, + pub(crate) max_blocks_per_request: Option, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct InitArgs { + pub(crate) index_id: Option, + pub(crate) max_blocks_per_request: u64, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum LedgerArgs { + Upgrade(Option), + Init(InitArgs), +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct SubnetFilter { + pub(crate) subnet_type: Option, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum SubnetSelection { + Filter(SubnetFilter), + Subnet { subnet: Principal }, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct CanisterSettings { + pub(crate) freezing_threshold: Option, + pub(crate) controllers: Option>, + pub(crate) reserved_cycles_limit: Option, + pub(crate) memory_allocation: Option, + pub(crate) compute_allocation: Option, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct CmcCreateCanisterArgs { + pub(crate) subnet_selection: Option, + pub(crate) settings: Option, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct CreateCanisterArgs { + pub(crate) from_subaccount: Option, + pub(crate) created_at_time: Option, + pub(crate) amount: candid::Nat, + pub(crate) creation_args: Option, +} +pub(crate) type BlockIndex = candid::Nat; +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct CreateCanisterSuccess { + pub(crate) block_id: BlockIndex, + pub(crate) canister_id: Principal, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum CreateCanisterError { + GenericError { + message: String, + error_code: candid::Nat, + }, + TemporarilyUnavailable, + Duplicate { + duplicate_of: candid::Nat, + canister_id: Option, + }, + CreatedInFuture { + ledger_time: u64, + }, + FailedToCreate { + error: String, + refund_block: Option, + fee_block: Option, + }, + TooOld, + InsufficientFunds { + balance: candid::Nat, + }, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct Account { + pub(crate) owner: Principal, + pub(crate) subaccount: Option, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct CreateCanisterFromArgs { + pub(crate) spender_subaccount: Option, + pub(crate) from: Account, + pub(crate) created_at_time: Option, + pub(crate) amount: candid::Nat, + pub(crate) creation_args: Option, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum RejectionCode { + NoError, + CanisterError, + SysTransient, + DestinationInvalid, + Unknown, + SysFatal, + CanisterReject, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum CreateCanisterFromError { + FailedToCreateFrom { + create_from_block: Option, + rejection_code: RejectionCode, + refund_block: Option, + approval_refund_block: Option, + rejection_reason: String, + }, + GenericError { + message: String, + error_code: candid::Nat, + }, + TemporarilyUnavailable, + InsufficientAllowance { + allowance: candid::Nat, + }, + Duplicate { + duplicate_of: candid::Nat, + canister_id: Option, + }, + CreatedInFuture { + ledger_time: u64, + }, + TooOld, + InsufficientFunds { + balance: candid::Nat, + }, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct DepositArgs { + pub(crate) to: Account, + pub(crate) memo: Option, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct DepositResult { + pub(crate) balance: candid::Nat, + pub(crate) block_index: BlockIndex, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct HttpRequest { + pub(crate) url: String, + pub(crate) method: String, + pub(crate) body: serde_bytes::ByteBuf, + pub(crate) headers: Vec<(String, String)>, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct HttpResponse { + pub(crate) body: serde_bytes::ByteBuf, + pub(crate) headers: Vec<(String, String)>, + pub(crate) status_code: u16, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum MetadataValue { + Int(candid::Int), + Nat(candid::Nat), + Blob(serde_bytes::ByteBuf), + Text(String), +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct SupportedStandard { + pub(crate) url: String, + pub(crate) name: String, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct TransferArgs { + pub(crate) to: Account, + pub(crate) fee: Option, + pub(crate) memo: Option, + pub(crate) from_subaccount: Option, + pub(crate) created_at_time: Option, + pub(crate) amount: candid::Nat, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum TransferError { + GenericError { + message: String, + error_code: candid::Nat, + }, + TemporarilyUnavailable, + BadBurn { + min_burn_amount: candid::Nat, + }, + Duplicate { + duplicate_of: candid::Nat, + }, + BadFee { + expected_fee: candid::Nat, + }, + CreatedInFuture { + ledger_time: u64, + }, + TooOld, + InsufficientFunds { + balance: candid::Nat, + }, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct AllowanceArgs { + pub(crate) account: Account, + pub(crate) spender: Account, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct Allowance { + pub(crate) allowance: candid::Nat, + pub(crate) expires_at: Option, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct ApproveArgs { + pub(crate) fee: Option, + pub(crate) memo: Option, + pub(crate) from_subaccount: Option, + pub(crate) created_at_time: Option, + pub(crate) amount: candid::Nat, + pub(crate) expected_allowance: Option, + pub(crate) expires_at: Option, + pub(crate) spender: Account, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum ApproveError { + GenericError { + message: String, + error_code: candid::Nat, + }, + TemporarilyUnavailable, + Duplicate { + duplicate_of: candid::Nat, + }, + BadFee { + expected_fee: candid::Nat, + }, + AllowanceChanged { + current_allowance: candid::Nat, + }, + CreatedInFuture { + ledger_time: u64, + }, + TooOld, + Expired { + ledger_time: u64, + }, + InsufficientFunds { + balance: candid::Nat, + }, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct TransferFromArgs { + pub(crate) to: Account, + pub(crate) fee: Option, + pub(crate) spender_subaccount: Option, + pub(crate) from: Account, + pub(crate) memo: Option, + pub(crate) created_at_time: Option, + pub(crate) amount: candid::Nat, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum TransferFromError { + GenericError { + message: String, + error_code: candid::Nat, + }, + TemporarilyUnavailable, + InsufficientAllowance { + allowance: candid::Nat, + }, + BadBurn { + min_burn_amount: candid::Nat, + }, + Duplicate { + duplicate_of: candid::Nat, + }, + BadFee { + expected_fee: candid::Nat, + }, + CreatedInFuture { + ledger_time: u64, + }, + TooOld, + InsufficientFunds { + balance: candid::Nat, + }, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct GetArchivesArgs { + pub(crate) from: Option, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct GetArchivesResultItem { + pub(crate) end: candid::Nat, + pub(crate) canister_id: Principal, + pub(crate) start: candid::Nat, +} +pub(crate) type GetArchivesResult = Vec; +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct GetBlocksArgsItem { + pub(crate) start: candid::Nat, + pub(crate) length: candid::Nat, +} +pub(crate) type GetBlocksArgs = Vec; +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum Value { + Int(candid::Int), + Map(Vec<(String, Box)>), + Nat(candid::Nat), + Nat64(u64), + Blob(serde_bytes::ByteBuf), + Text(String), + Array(Vec>), +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct GetBlocksResultBlocksItem { + pub(crate) id: candid::Nat, + pub(crate) block: Box, +} +candid::define_function!(pub(crate) GetBlocksResultArchivedBlocksItemCallback : ( + GetBlocksArgs, + ) -> (GetBlocksResult) query); +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct GetBlocksResultArchivedBlocksItem { + pub(crate) args: GetBlocksArgs, + pub(crate) callback: GetBlocksResultArchivedBlocksItemCallback, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct GetBlocksResult { + pub(crate) log_length: candid::Nat, + pub(crate) blocks: Vec, + pub(crate) archived_blocks: Vec, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct DataCertificate { + pub(crate) certificate: serde_bytes::ByteBuf, + pub(crate) hash_tree: serde_bytes::ByteBuf, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct SupportedBlockType { + pub(crate) url: String, + pub(crate) block_type: String, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct WithdrawArgs { + pub(crate) to: Principal, + pub(crate) from_subaccount: Option, + pub(crate) created_at_time: Option, + pub(crate) amount: candid::Nat, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum WithdrawError { + FailedToWithdraw { + rejection_code: RejectionCode, + fee_block: Option, + rejection_reason: String, + }, + GenericError { + message: String, + error_code: candid::Nat, + }, + TemporarilyUnavailable, + Duplicate { + duplicate_of: candid::Nat, + }, + BadFee { + expected_fee: candid::Nat, + }, + InvalidReceiver { + receiver: Principal, + }, + CreatedInFuture { + ledger_time: u64, + }, + TooOld, + InsufficientFunds { + balance: candid::Nat, + }, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) struct WithdrawFromArgs { + pub(crate) to: Principal, + pub(crate) spender_subaccount: Option, + pub(crate) from: Account, + pub(crate) created_at_time: Option, + pub(crate) amount: candid::Nat, +} +#[derive(CandidType, Deserialize, Debug, Eq, PartialEq, Clone)] +pub(crate) enum WithdrawFromError { + GenericError { + message: String, + error_code: candid::Nat, + }, + TemporarilyUnavailable, + InsufficientAllowance { + allowance: candid::Nat, + }, + Duplicate { + duplicate_of: BlockIndex, + }, + InvalidReceiver { + receiver: Principal, + }, + CreatedInFuture { + ledger_time: u64, + }, + TooOld, + FailedToWithdrawFrom { + withdraw_from_block: Option, + rejection_code: RejectionCode, + refund_block: Option, + approval_refund_block: Option, + rejection_reason: String, + }, + InsufficientFunds { + balance: candid::Nat, + }, +} + +pub struct Service(pub Principal); +impl Service { + pub async fn create_canister( + &self, + arg0: &CreateCanisterArgs, + ) -> Result<(std::result::Result,)> { + ic_cdk::call(self.0, "create_canister", (arg0,)).await + } + pub async fn create_canister_from( + &self, + arg0: &CreateCanisterFromArgs, + ) -> Result<(std::result::Result,)> { + ic_cdk::call(self.0, "create_canister_from", (arg0,)).await + } + pub async fn deposit(&self, arg0: &DepositArgs) -> Result<(DepositResult,)> { + ic_cdk::call(self.0, "deposit", (arg0,)).await + } + pub async fn http_request(&self, arg0: &HttpRequest) -> Result<(HttpResponse,)> { + ic_cdk::call(self.0, "http_request", (arg0,)).await + } + pub async fn icrc_1_balance_of(&self, arg0: &Account) -> Result<(candid::Nat,)> { + ic_cdk::call(self.0, "icrc1_balance_of", (arg0,)).await + } + pub async fn icrc_1_decimals(&self) -> Result<(u8,)> { + ic_cdk::call(self.0, "icrc1_decimals", ()).await + } + pub async fn icrc_1_fee(&self) -> Result<(candid::Nat,)> { + ic_cdk::call(self.0, "icrc1_fee", ()).await + } + pub async fn icrc_1_metadata(&self) -> Result<(Vec<(String, MetadataValue)>,)> { + ic_cdk::call(self.0, "icrc1_metadata", ()).await + } + pub async fn icrc_1_minting_account(&self) -> Result<(Option,)> { + ic_cdk::call(self.0, "icrc1_minting_account", ()).await + } + pub async fn icrc_1_name(&self) -> Result<(String,)> { + ic_cdk::call(self.0, "icrc1_name", ()).await + } + pub async fn icrc_1_supported_standards(&self) -> Result<(Vec,)> { + ic_cdk::call(self.0, "icrc1_supported_standards", ()).await + } + pub async fn icrc_1_symbol(&self) -> Result<(String,)> { + ic_cdk::call(self.0, "icrc1_symbol", ()).await + } + pub async fn icrc_1_total_supply(&self) -> Result<(candid::Nat,)> { + ic_cdk::call(self.0, "icrc1_total_supply", ()).await + } + pub async fn icrc_1_transfer( + &self, + arg0: &TransferArgs, + ) -> Result<(std::result::Result,)> { + ic_cdk::call(self.0, "icrc1_transfer", (arg0,)).await + } + pub async fn icrc_2_allowance(&self, arg0: &AllowanceArgs) -> Result<(Allowance,)> { + ic_cdk::call(self.0, "icrc2_allowance", (arg0,)).await + } + pub async fn icrc_2_approve( + &self, + arg0: &ApproveArgs, + ) -> Result<(std::result::Result,)> { + ic_cdk::call(self.0, "icrc2_approve", (arg0,)).await + } + pub async fn icrc_2_transfer_from( + &self, + arg0: &TransferFromArgs, + ) -> Result<(std::result::Result,)> { + ic_cdk::call(self.0, "icrc2_transfer_from", (arg0,)).await + } + pub async fn icrc_3_get_archives( + &self, + arg0: &GetArchivesArgs, + ) -> Result<(GetArchivesResult,)> { + ic_cdk::call(self.0, "icrc3_get_archives", (arg0,)).await + } + pub async fn icrc_3_get_blocks(&self, arg0: &GetBlocksArgs) -> Result<(GetBlocksResult,)> { + ic_cdk::call(self.0, "icrc3_get_blocks", (arg0,)).await + } + pub async fn icrc_3_get_tip_certificate(&self) -> Result<(Option,)> { + ic_cdk::call(self.0, "icrc3_get_tip_certificate", ()).await + } + pub async fn icrc_3_supported_block_types(&self) -> Result<(Vec,)> { + ic_cdk::call(self.0, "icrc3_supported_block_types", ()).await + } + pub async fn withdraw( + &self, + arg0: &WithdrawArgs, + ) -> Result<(std::result::Result,)> { + ic_cdk::call(self.0, "withdraw", (arg0,)).await + } + pub async fn withdraw_from( + &self, + arg0: &WithdrawFromArgs, + ) -> Result<(std::result::Result,)> { + ic_cdk::call(self.0, "withdraw_from", (arg0,)).await + } +} diff --git a/src/backend/src/lib.rs b/src/backend/src/lib.rs index b01240c547..e25b254789 100644 --- a/src/backend/src/lib.rs +++ b/src/backend/src/lib.rs @@ -47,6 +47,7 @@ use user_profile::{add_credential, create_profile, find_profile}; use user_profile_model::UserProfileModel; mod assertions; +mod bind; mod bitcoin_api; mod bitcoin_utils; mod config; @@ -147,12 +148,25 @@ fn set_config(arg: InitArg) { }); } +fn start_periodic_housekeeping_timers() { + let hour = Duration::from_secs(60 * 60); + let _ = set_timer_interval(hour, || ic_cdk::spawn(hourly_housekeeping_tasks())); +} + +/// Runs hourly housekeeping tasks: +/// - Top up the cycles ledger. +async fn hourly_housekeeping_tasks() { + let _ = top_up_cycles_ledger(None).await; + // TODO: Add monitoring for how many cycles have been topped up and whether topping up is failing. +} + #[init] pub fn init(arg: Arg) { match arg { Arg::Init(arg) => set_config(arg), Arg::Upgrade => ic_cdk::trap("upgrade args in init"), } + start_periodic_housekeeping_timers(); } /// Post-upgrade handler. @@ -171,6 +185,7 @@ pub fn post_upgrade(arg: Option) { }); } } + start_periodic_housekeeping_timers(); } /// Gets the canister configuration. diff --git a/src/backend/src/signer.rs b/src/backend/src/signer.rs index c2eb698aaa..7766b7434d 100644 --- a/src/backend/src/signer.rs +++ b/src/backend/src/signer.rs @@ -5,14 +5,22 @@ use crate::{ }; use bitcoin::{Address, CompressedPublicKey, Network}; use candid::{CandidType, Deserialize, Nat, Principal}; -use ic_cdk::api::management_canister::{ - bitcoin::BitcoinNetwork, - ecdsa::{ecdsa_public_key, EcdsaCurve, EcdsaKeyId, EcdsaPublicKeyArgument}, +use ic_cdk::api::{ + call::call_with_payment128, + management_canister::{ + bitcoin::BitcoinNetwork, + ecdsa::{ecdsa_public_key, EcdsaCurve, EcdsaKeyId, EcdsaPublicKeyArgument}, + }, +}; +use ic_cycles_ledger_client::{ + Account, ApproveArgs, ApproveError, DepositArgs, DepositResult, Service as CyclesLedgerService, }; -use ic_cycles_ledger_client::{Account, ApproveArgs, ApproveError, Service as CyclesLedgerService}; use ic_ledger_types::Subaccount; use serde_bytes::ByteBuf; -use shared::types::signer::topup::{TopUpCyclesLedgerRequest, TopUpCyclesLedgerResult}; +use shared::types::signer::topup::{ + TopUpCyclesLedgerError, TopUpCyclesLedgerRequest, TopUpCyclesLedgerResponse, + TopUpCyclesLedgerResult, +}; #[derive(CandidType, Deserialize, Debug, Clone, Eq, PartialEq)] pub enum AllowSigningError { @@ -148,10 +156,49 @@ pub async fn btc_principal_to_p2wpkh_address( } /// Tops up the cycles ledger. -/// -/// # Errors -/// Errors are enumerated by: `TopUpCyclesLedgerError` -#[allow(clippy::unused_async)] // TODO: Remove once the code is implemented pub async fn top_up_cycles_ledger(request: TopUpCyclesLedgerRequest) -> TopUpCyclesLedgerResult { - todo!("Add code that tops up the cycles ledger per this request: {request:?}") + let cycles_ledger = CyclesLedgerService(*CYCLES_LEDGER); + let account = Account { + owner: ic_cdk::id(), + subaccount: None, + }; + let (ledger_balance,): (Nat,) = cycles_ledger + .icrc_1_balance_of(&account) + .await + .map_err(|_| TopUpCyclesLedgerError::CouldNotGetBalanceFromCyclesLedger)?; + + if ledger_balance < request.threshold() { + // Decide how many cycles to keep and how many to send to teh cycles ledger. + let own_canister_cycle_balance = Nat::from(ic_cdk::api::canister_balance128()); + let to_send = own_canister_cycle_balance.clone() / Nat::from(100u32) + * Nat::from(request.percentage()); + let to_retain = own_canister_cycle_balance - to_send.clone(); + + // Top up the cycles ledger. + let arg = DepositArgs { + to: account, + memo: None, + }; + let to_send_128: u128 = + to_send.clone().0.try_into().unwrap_or_else(|err| { + unreachable!("Failed to convert cycle amount to u128: {}", err) + }); + let (result,): (DepositResult,) = + call_with_payment128(*CYCLES_LEDGER, "deposit", (arg,), to_send_128) + .await + .expect("Unable to call deposit"); + let ledger_balance = result.balance; + + Ok(TopUpCyclesLedgerResponse { + ledger_balance, + backend_cycles: to_retain, + topped_up: to_send, + }) + } else { + Ok(TopUpCyclesLedgerResponse { + ledger_balance: Nat::from(0u32), + backend_cycles: ledger_balance, + topped_up: Nat::from(0u32), + }) + } } diff --git a/src/backend/tests/it/signer/top_up.rs b/src/backend/tests/it/signer/top_up.rs new file mode 100644 index 0000000000..431a86aa8c --- /dev/null +++ b/src/backend/tests/it/signer/top_up.rs @@ -0,0 +1,7 @@ +//! Tests that the backend tops up the cycles ledger, as needed to pay for the signer. + +#[test] +fn test_top_up() { + + // Test that the backend tops up the cycles ledger, as needed to pay for the signer. +} diff --git a/src/shared/src/types.rs b/src/shared/src/types.rs index 4acbb107b1..79ded50d1b 100644 --- a/src/shared/src/types.rs +++ b/src/shared/src/types.rs @@ -269,13 +269,13 @@ pub mod signer { #[derive(CandidType, Deserialize, Debug, Clone, Eq, PartialEq)] pub struct TopUpCyclesLedgerResponse { /// The ledger balance after topping up. - ledger_balance: Nat, + pub ledger_balance: Nat, /// The backend canister cycles after topping up. - backend_cycles: Nat, + pub backend_cycles: Nat, /// The amount topped up. /// - Zero if the ledger balance was already sufficient. /// - The requested amount otherwise. - topped_up: Nat, + pub topped_up: Nat, } pub type TopUpCyclesLedgerResult =