diff --git a/rs/bitcoin/ckbtc/minter/src/lib.rs b/rs/bitcoin/ckbtc/minter/src/lib.rs index 7e3a3a96056..8c73f931f32 100644 --- a/rs/bitcoin/ckbtc/minter/src/lib.rs +++ b/rs/bitcoin/ckbtc/minter/src/lib.rs @@ -858,7 +858,7 @@ pub async fn sign_transaction( ecdsa_public_key: &ECDSAPublicKey, output_account: &BTreeMap, unsigned_tx: tx::UnsignedTransaction, -) -> Result { +) -> Result { use crate::address::{derivation_path, derive_public_key}; let mut signed_inputs = Vec::with_capacity(unsigned_tx.inputs.len()); @@ -1286,8 +1286,7 @@ pub trait CanisterRuntime { /// Fetches all unspent transaction outputs (UTXOs) associated with the provided address in the specified Bitcoin network. async fn bitcoin_get_utxos( &self, - request: &GetUtxosRequest, - cycles: u64, + request: GetUtxosRequest, ) -> Result; async fn check_transaction( @@ -1328,10 +1327,9 @@ impl CanisterRuntime for IcCanisterRuntime { async fn bitcoin_get_utxos( &self, - request: &GetUtxosRequest, - cycles: u64, + request: GetUtxosRequest, ) -> Result { - management::call("bitcoin_get_utxos", cycles, &request).await + management::bitcoin_get_utxos(request).await } async fn check_transaction( diff --git a/rs/bitcoin/ckbtc/minter/src/management.rs b/rs/bitcoin/ckbtc/minter/src/management.rs index 66ddcd183d2..82ffab97dbc 100644 --- a/rs/bitcoin/ckbtc/minter/src/management.rs +++ b/rs/bitcoin/ckbtc/minter/src/management.rs @@ -1,5 +1,4 @@ //! This module contains async functions for interacting with the management canister. - use crate::logs::P0; use crate::ECDSAPublicKey; use crate::{tx, CanisterRuntime}; @@ -8,15 +7,19 @@ use ic_btc_checker::{ CheckAddressArgs, CheckAddressResponse, CheckTransactionArgs, CheckTransactionResponse, }; use ic_btc_interface::{ - Address, GetCurrentFeePercentilesRequest, GetUtxosRequest, GetUtxosResponse, - MillisatoshiPerByte, Network, Utxo, UtxosFilterInRequest, + Address, GetUtxosRequest, GetUtxosResponse, MillisatoshiPerByte, Network, OutPoint, Txid, Utxo, + UtxosFilterInRequest, }; use ic_canister_log::log; -use ic_cdk::api::call::RejectionCode; +use ic_cdk::api::{ + call::RejectionCode, + management_canister::bitcoin::{BitcoinNetwork, UtxoFilter}, +}; use ic_management_canister_types::{ DerivationPath, ECDSAPublicKeyArgs, ECDSAPublicKeyResponse, EcdsaCurve, EcdsaKeyId, }; use serde::de::DeserializeOwned; +use serde_bytes::ByteBuf; use std::fmt; /// Represents an error from a management canister call, such as @@ -37,6 +40,13 @@ impl CallError { pub fn reason(&self) -> &Reason { &self.reason } + + pub fn from_cdk_error(method: &str, (code, msg): (RejectionCode, String)) -> CallError { + CallError { + method: String::from(method), + reason: Reason::from_reject(code, msg), + } + } } impl fmt::Display for CallError { @@ -142,19 +152,8 @@ pub async fn get_utxos( source: CallSource, runtime: &R, ) -> Result { - // NB. The minimum number of cycles that need to be sent with the call is 10B (4B) for - // Bitcoin mainnet (Bitcoin testnet): - // https://internetcomputer.org/docs/current/developer-docs/integrations/bitcoin/bitcoin-how-it-works#api-fees--pricing - let get_utxos_cost_cycles = match network { - Network::Mainnet => 10_000_000_000, - Network::Testnet | Network::Regtest => 4_000_000_000, - }; - - // Calls "bitcoin_get_utxos" method with the specified argument on the - // management canister. async fn bitcoin_get_utxos( - req: &GetUtxosRequest, - cycles: u64, + req: GetUtxosRequest, source: CallSource, runtime: &R, ) -> Result { @@ -163,16 +162,15 @@ pub async fn get_utxos( CallSource::Minter => &crate::metrics::GET_UTXOS_MINTER_CALLS, } .with(|cell| cell.set(cell.get() + 1)); - runtime.bitcoin_get_utxos(req, cycles).await + runtime.bitcoin_get_utxos(req).await } let mut response = bitcoin_get_utxos( - &GetUtxosRequest { + GetUtxosRequest { address: address.to_string(), network: network.into(), filter: Some(UtxosFilterInRequest::MinConfirmations(min_confirmations)), }, - get_utxos_cost_cycles, source, runtime, ) @@ -183,12 +181,11 @@ pub async fn get_utxos( // Continue fetching until there are no more pages. while let Some(page) = response.next_page { response = bitcoin_get_utxos( - &GetUtxosRequest { + GetUtxosRequest { address: address.to_string(), network: network.into(), filter: Some(UtxosFilterInRequest::Page(page)), }, - get_utxos_cost_cycles, source, runtime, ) @@ -202,22 +199,65 @@ pub async fn get_utxos( Ok(response) } +/// Fetches a subset of UTXOs for the specified address. +pub async fn bitcoin_get_utxos(request: GetUtxosRequest) -> Result { + fn cdk_get_utxos_request( + request: GetUtxosRequest, + ) -> ic_cdk::api::management_canister::bitcoin::GetUtxosRequest { + ic_cdk::api::management_canister::bitcoin::GetUtxosRequest { + address: request.address, + network: cdk_network(request.network.into()), + filter: request.filter.map(|filter| match filter { + UtxosFilterInRequest::MinConfirmations(confirmations) + | UtxosFilterInRequest::min_confirmations(confirmations) => { + UtxoFilter::MinConfirmations(confirmations) + } + UtxosFilterInRequest::Page(bytes) | UtxosFilterInRequest::page(bytes) => { + UtxoFilter::Page(bytes.into_vec()) + } + }), + } + } + + fn parse_cdk_get_utxos_response( + response: ic_cdk::api::management_canister::bitcoin::GetUtxosResponse, + ) -> GetUtxosResponse { + GetUtxosResponse { + utxos: response + .utxos + .into_iter() + .map(|utxo| Utxo { + outpoint: OutPoint { + txid: Txid::try_from(utxo.outpoint.txid.as_slice()) + .unwrap_or_else(|_| panic!("Unable to parse TXID")), + vout: utxo.outpoint.vout, + }, + value: utxo.value, + height: utxo.height, + }) + .collect(), + tip_block_hash: response.tip_block_hash, + tip_height: response.tip_height, + next_page: response.next_page.map(ByteBuf::from), + } + } + + ic_cdk::api::management_canister::bitcoin::bitcoin_get_utxos(cdk_get_utxos_request(request)) + .await + .map(|(response,)| parse_cdk_get_utxos_response(response)) + .map_err(|err| CallError::from_cdk_error("bitcoin_get_utxos", err)) +} + /// Returns the current fee percentiles on the Bitcoin network. pub async fn get_current_fees(network: Network) -> Result, CallError> { - let cost_cycles = match network { - Network::Mainnet => 100_000_000, - Network::Testnet => 40_000_000, - Network::Regtest => 0, - }; - - call( - "bitcoin_get_current_fee_percentiles", - cost_cycles, - &GetCurrentFeePercentilesRequest { - network: network.into(), + ic_cdk::api::management_canister::bitcoin::bitcoin_get_current_fee_percentiles( + ic_cdk::api::management_canister::bitcoin::GetCurrentFeePercentilesRequest { + network: cdk_network(network), }, ) .await + .map(|(result,)| result) + .map_err(|err| CallError::from_cdk_error("bitcoin_get_current_fee_percentiles", err)) } /// Sends the transaction to the network the management canister interacts with. @@ -225,27 +265,14 @@ pub async fn send_transaction( transaction: &tx::SignedTransaction, network: Network, ) -> Result<(), CallError> { - use ic_cdk::api::management_canister::bitcoin::BitcoinNetwork; - - let cdk_network = match network { - Network::Mainnet => BitcoinNetwork::Mainnet, - Network::Testnet => BitcoinNetwork::Testnet, - Network::Regtest => BitcoinNetwork::Regtest, - }; - - let tx_bytes = transaction.serialize(); - ic_cdk::api::management_canister::bitcoin::bitcoin_send_transaction( ic_cdk::api::management_canister::bitcoin::SendTransactionRequest { - transaction: tx_bytes, - network: cdk_network, + transaction: transaction.serialize(), + network: cdk_network(network), }, ) .await - .map_err(|(code, msg)| CallError { - method: "bitcoin_send_transaction".to_string(), - reason: Reason::from_reject(code, msg), - }) + .map_err(|err| CallError::from_cdk_error("bitcoin_send_transaction", err)) } /// Fetches the ECDSA public key of the canister. @@ -342,3 +369,11 @@ pub async fn check_transaction( })?; Ok(res) } + +fn cdk_network(network: Network) -> BitcoinNetwork { + match network { + Network::Mainnet => BitcoinNetwork::Mainnet, + Network::Testnet => BitcoinNetwork::Testnet, + Network::Regtest => BitcoinNetwork::Regtest, + } +} diff --git a/rs/bitcoin/ckbtc/minter/src/test_fixtures.rs b/rs/bitcoin/ckbtc/minter/src/test_fixtures.rs index 1e17f12dc13..cee6585f3a8 100644 --- a/rs/bitcoin/ckbtc/minter/src/test_fixtures.rs +++ b/rs/bitcoin/ckbtc/minter/src/test_fixtures.rs @@ -136,7 +136,7 @@ pub mod mock { fn id(&self) -> Principal; fn time(&self) -> u64; fn global_timer_set(&self, timestamp: u64); - async fn bitcoin_get_utxos(&self, request: &GetUtxosRequest, cycles: u64) -> Result; + async fn bitcoin_get_utxos(&self, request: GetUtxosRequest) -> Result; async fn check_transaction(&self, btc_checker_principal: Principal, utxo: &Utxo, cycle_payment: u128, ) -> Result; async fn mint_ckbtc(&self, amount: u64, to: Account, memo: Memo) -> Result; } diff --git a/rs/bitcoin/mock/src/main.rs b/rs/bitcoin/mock/src/main.rs index 03307ab51b1..ea5c8dbbeca 100644 --- a/rs/bitcoin/mock/src/main.rs +++ b/rs/bitcoin/mock/src/main.rs @@ -87,7 +87,7 @@ fn set_tip_height(tip_height: u32) { #[update] fn bitcoin_get_utxos(utxos_request: GetUtxosRequest) -> GetUtxosResponse { read_state(|s| { - assert_eq!(utxos_request.network, s.network.into()); + assert_eq!(Network::from(utxos_request.network), s.network); let mut utxos = s .address_to_utxos @@ -98,10 +98,14 @@ fn bitcoin_get_utxos(utxos_request: GetUtxosRequest) -> GetUtxosResponse { .cloned() .collect::>(); - if let Some(UtxosFilterInRequest::MinConfirmations(min_confirmations)) = - utxos_request.filter - { - utxos.retain(|u| s.tip_height + 1 >= u.height + min_confirmations); + if let Some(filter) = utxos_request.filter { + match filter { + UtxosFilterInRequest::MinConfirmations(min_confirmations) + | UtxosFilterInRequest::min_confirmations(min_confirmations) => { + utxos.retain(|u| s.tip_height + 1 >= u.height + min_confirmations) + } + UtxosFilterInRequest::Page(_) | UtxosFilterInRequest::page(_) => {} + } } GetUtxosResponse {