diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eaba61e..efaf090c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +* Removed the Bitcoin query methods from `ManagementCanister`. Users should use `BitcoinCanister` for that. +* Added `BitcoinCanister` to `ic-utils`. + ## [0.36.0] - 2024-06-04 * Added a default request timeout to `ReqwestTransport`. diff --git a/ic-utils/src/interfaces.rs b/ic-utils/src/interfaces.rs index 1a4d9651..6935ac56 100644 --- a/ic-utils/src/interfaces.rs +++ b/ic-utils/src/interfaces.rs @@ -1,7 +1,9 @@ +pub mod bitcoin_canister; pub mod http_request; pub mod management_canister; pub mod wallet; +pub use bitcoin_canister::BitcoinCanister; pub use http_request::HttpRequestCanister; pub use management_canister::ManagementCanister; pub use wallet::WalletCanister; diff --git a/ic-utils/src/interfaces/bitcoin_canister.rs b/ic-utils/src/interfaces/bitcoin_canister.rs new file mode 100644 index 00000000..184c7ce8 --- /dev/null +++ b/ic-utils/src/interfaces/bitcoin_canister.rs @@ -0,0 +1,279 @@ +//! The canister interface for the [Bitcoin canister](https://github.com/dfinity/bitcoin-canister). + +use std::ops::Deref; + +use candid::{CandidType, Principal}; +use ic_agent::{Agent, AgentError}; +use serde::Deserialize; + +use crate::{ + call::{AsyncCall, SyncCall}, + Canister, +}; + +/// The canister interface for the IC [Bitcoin canister](https://github.com/dfinity/bitcoin-canister). +#[derive(Debug)] +pub struct BitcoinCanister<'agent> { + canister: Canister<'agent>, + network: BitcoinNetwork, +} + +impl<'agent> Deref for BitcoinCanister<'agent> { + type Target = Canister<'agent>; + fn deref(&self) -> &Self::Target { + &self.canister + } +} +const MAINNET_ID: Principal = + Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, 0xa0, 0x00, 0x04, 0x01, 0x01]); +const TESTNET_ID: Principal = + Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, 0xa0, 0x00, 0x01, 0x01, 0x01]); + +impl<'agent> BitcoinCanister<'agent> { + /// Create a `BitcoinCanister` interface from an existing canister object. + pub fn from_canister(canister: Canister<'agent>, network: BitcoinNetwork) -> Self { + Self { canister, network } + } + /// Create a `BitcoinCanister` interface pointing to the specified canister ID. + pub fn create(agent: &'agent Agent, canister_id: Principal, network: BitcoinNetwork) -> Self { + Self::from_canister( + Canister::builder() + .with_agent(agent) + .with_canister_id(canister_id) + .build() + .expect("all required fields should be set"), + network, + ) + } + /// Create a `BitcoinCanister` interface for the Bitcoin mainnet canister on the IC mainnet. + pub fn mainnet(agent: &'agent Agent) -> Self { + Self::for_network(agent, BitcoinNetwork::Mainnet).expect("valid network") + } + /// Create a `BitcoinCanister` interface for the Bitcoin testnet canister on the IC mainnet. + pub fn testnet(agent: &'agent Agent) -> Self { + Self::for_network(agent, BitcoinNetwork::Testnet).expect("valid network") + } + /// Create a `BitcoinCanister` interface for the specified Bitcoin network on the IC mainnet. Errors if `Regtest` is specified. + pub fn for_network(agent: &'agent Agent, network: BitcoinNetwork) -> Result { + let canister_id = match network { + BitcoinNetwork::Mainnet => MAINNET_ID, + BitcoinNetwork::Testnet => TESTNET_ID, + BitcoinNetwork::Regtest => { + return Err(AgentError::MessageError( + "No applicable canister ID for regtest".to_string(), + )) + } + }; + Ok(Self::create(agent, canister_id, network)) + } + + /// Gets the BTC balance (in satoshis) of a particular Bitcoin address, filtering by number of confirmations. + /// Most applications should require 6 confirmations. + pub fn get_balance( + &self, + address: &str, + min_confirmations: Option, + ) -> impl 'agent + AsyncCall { + #[derive(CandidType)] + struct In<'a> { + address: &'a str, + network: BitcoinNetwork, + min_confirmations: Option, + } + self.update("bitcoin_get_balance") + .with_arg(GetBalance { + address, + network: self.network, + min_confirmations, + }) + .build() + } + + /// Gets the BTC balance (in satoshis) of a particular Bitcoin address, filtering by number of confirmations. + /// Most applications should require 6 confirmations. + pub fn get_balance_query( + &self, + address: &str, + min_confirmations: Option, + ) -> impl 'agent + SyncCall { + self.query("bitcoin_get_balance_query") + .with_arg(GetBalance { + address, + network: self.network, + min_confirmations, + }) + .build() + } + + /// Fetch the list of [UTXOs](https://en.wikipedia.org/wiki/Unspent_transaction_output) for a Bitcoin address, + /// filtering by number of confirmations. Most applications should require 6 confirmations. + /// + /// This method is paginated. If not all the results can be returned, then `next_page` will be set to `Some`, + /// and its value can be passed to this method to get the next page. + pub fn get_utxos( + &self, + address: &str, + filter: Option, + ) -> impl 'agent + AsyncCall { + self.update("bitcoin_get_utxos") + .with_arg(GetUtxos { + address, + network: self.network, + filter, + }) + .build() + } + + /// Fetch the list of [UTXOs](https://en.wikipedia.org/wiki/Unspent_transaction_output) for a Bitcoin address, + /// filtering by number of confirmations. Most applications should require 6 confirmations. + /// + /// This method is paginated. If not all the results can be returned, then `next_page` will be set to `Some`, + /// and its value can be passed to this method to get the next page. + pub fn get_utxos_query( + &self, + address: &str, + filter: Option, + ) -> impl 'agent + SyncCall { + self.query("bitcoin_get_utxos_query") + .with_arg(GetUtxos { + address, + network: self.network, + filter, + }) + .build() + } + + /// Gets the transaction fee percentiles for the last 10,000 transactions. In the returned vector, `v[i]` is the `i`th percentile fee, + /// measured in millisatoshis/vbyte, and `v[0]` is the smallest fee. + pub fn get_current_fee_percentiles(&self) -> impl 'agent + AsyncCall,)> { + #[derive(CandidType)] + struct In { + network: BitcoinNetwork, + } + self.update("bitcoin_get_current_fee_percentiles") + .with_arg(In { + network: self.network, + }) + .build() + } + /// Gets the block headers for the specified range of blocks. If `end_height` is `None`, the returned `tip_height` provides the tip at the moment + /// the chain was queried. + pub fn get_block_headers( + &self, + start_height: u32, + end_height: Option, + ) -> impl 'agent + AsyncCall { + #[derive(CandidType)] + struct In { + start_height: u32, + end_height: Option, + } + self.update("bitcoin_get_block_headers") + .with_arg(In { + start_height, + end_height, + }) + .build() + } + /// Submits a new Bitcoin transaction. No guarantees are made about the outcome. + pub fn send_transaction(&self, transaction: Vec) -> impl 'agent + AsyncCall { + #[derive(CandidType, Deserialize)] + struct In { + network: BitcoinNetwork, + #[serde(with = "serde_bytes")] + transaction: Vec, + } + self.update("bitcoin_send_transaction") + .with_arg(In { + network: self.network, + transaction, + }) + .build() + } +} + +#[derive(Debug, CandidType)] +struct GetBalance<'a> { + address: &'a str, + network: BitcoinNetwork, + min_confirmations: Option, +} + +#[derive(Debug, CandidType)] +struct GetUtxos<'a> { + address: &'a str, + network: BitcoinNetwork, + filter: Option, +} + +/// The Bitcoin network that a Bitcoin transaction is placed on. +#[derive(Clone, Copy, Debug, CandidType, Deserialize, PartialEq, Eq)] +pub enum BitcoinNetwork { + /// The BTC network. + #[serde(rename = "mainnet")] + Mainnet, + /// The TESTBTC network. + #[serde(rename = "testnet")] + Testnet, + /// The REGTEST network. + /// + /// This is only available when developing with local replica. + #[serde(rename = "regtest")] + Regtest, +} + +/// Defines how to filter results from [`BitcoinCanister::get_utxos_query`]. +#[derive(Debug, Clone, CandidType, Deserialize)] +pub enum UtxosFilter { + /// Filter by the minimum number of UTXO confirmations. Most applications should set this to 6. + #[serde(rename = "min_confirmations")] + MinConfirmations(u32), + /// When paginating results, use this page. Provided by [`GetUtxosResponse.next_page`](GetUtxosResponse). + #[serde(rename = "page")] + Page(#[serde(with = "serde_bytes")] Vec), +} + +/// Unique output descriptor of a Bitcoin transaction. +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct UtxoOutpoint { + /// The ID of the transaction. Not necessarily unique on its own. + #[serde(with = "serde_bytes")] + pub txid: Vec, + /// The index of the outpoint within the transaction. + pub vout: u32, +} + +/// A Bitcoin [`UTXO`](https://en.wikipedia.org/wiki/Unspent_transaction_output), produced by a transaction. +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct Utxo { + /// The transaction outpoint that produced this UTXO. + pub outpoint: UtxoOutpoint, + /// The BTC quantity, in satoshis. + pub value: u64, + /// The block index this transaction was placed at. + pub height: u32, +} + +/// Response type for the [`BitcoinCanister::get_utxos_query`] function. +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct GetUtxosResponse { + /// A list of UTXOs available for the specified address. + pub utxos: Vec, + /// The hash of the tip. + #[serde(with = "serde_bytes")] + pub tip_block_hash: Vec, + /// The block index of the tip of the chain known to the IC. + pub tip_height: u32, + /// If `Some`, then `utxos` does not contain the entire results of the query. + /// Call `bitcoin_get_utxos_query` again using `UtxosFilter::Page` for the next page of results. + pub next_page: Option>, +} + +/// Response type for the ``. +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct GetBlockHeadersResponse { + /// The tip of the chain, current to when the headers were fetched. + pub tip_height: u32, + /// The headers of the requested block range. + pub block_headers: Vec>, +} diff --git a/ic-utils/src/interfaces/management_canister.rs b/ic-utils/src/interfaces/management_canister.rs index 0671fb8d..178c2641 100644 --- a/ic-utils/src/interfaces/management_canister.rs +++ b/ic-utils/src/interfaces/management_canister.rs @@ -73,17 +73,13 @@ pub enum MgmtMethod { EcdsaPublicKey, /// There is no corresponding agent function as only canisters can call it. SignWithEcdsa, - /// There is no corresponding agent function as only canisters can call it. + /// There is no corresponding agent function as only canisters can call it. Use [`BitcoinCanister`](super::BitcoinCanister) instead. BitcoinGetBalance, - /// See [`ManagementCanister::bitcoin_get_balance_query`]. - BitcoinGetBalanceQuery, - /// There is no corresponding agent function as only canisters can call it. + /// There is no corresponding agent function as only canisters can call it. Use [`BitcoinCanister`](super::BitcoinCanister) instead. BitcoinGetUtxos, - /// See [`ManagementCanister::bitcoin_get_utxos_query`]. - BitcoinGetUtxosQuery, - /// There is no corresponding agent function as only canisters can call it. + /// There is no corresponding agent function as only canisters can call it. Use [`BitcoinCanister`](super::BitcoinCanister) instead. BitcoinSendTransaction, - /// There is no corresponding agent function as only canisters can call it. + /// There is no corresponding agent function as only canisters can call it. Use [`BitcoinCanister`](super::BitcoinCanister) instead. BitcoinGetCurrentFeePercentiles, /// There is no corresponding agent function as only canisters can call it. NodeMetricsHistory, @@ -235,67 +231,6 @@ pub type StoreChunksResult = Vec; /// Return type of [ManagementCanister::upload_chunk]. pub type UploadChunkResult = ChunkHash; -/// The Bitcoin network that a Bitcoin transaction is placed on. -#[derive(Clone, Copy, Debug, CandidType, Deserialize, PartialEq, Eq)] -pub enum BitcoinNetwork { - /// The BTC network. - #[serde(rename = "mainnet")] - Mainnet, - /// The TESTBTC network. - #[serde(rename = "testnet")] - Testnet, - /// The REGTEST network. - /// - /// This is only available when developing with local replica. - #[serde(rename = "regtest")] - Regtest, -} - -/// Defines how to filter results from [`bitcoin_get_utxos_query`](ManagementCanister::bitcoin_get_utxos_query). -#[derive(Debug, Clone, CandidType, Deserialize)] -pub enum UtxosFilter { - /// Filter by the minimum number of UTXO confirmations. Most applications should set this to 6. - #[serde(rename = "min_confirmations")] - MinConfirmations(u32), - /// When paginating results, use this page. Provided by [`GetUtxosResponse.next_page`](GetUtxosResponse). - #[serde(rename = "page")] - Page(#[serde(with = "serde_bytes")] Vec), -} - -/// Unique output descriptor of a Bitcoin transaction. -#[derive(Debug, Clone, CandidType, Deserialize)] -pub struct UtxoOutpoint { - /// The ID of the transaction. Not necessarily unique on its own. - pub txid: Vec, - /// The index of the outpoint within the transaction. - pub vout: u32, -} - -/// A Bitcoin [`UTXO`](https://en.wikipedia.org/wiki/Unspent_transaction_output), produced by a transaction. -#[derive(Debug, Clone, CandidType, Deserialize)] -pub struct Utxo { - /// The transaction outpoint that produced this UTXO. - pub outpoint: UtxoOutpoint, - /// The BTC quantity, in satoshis. - pub value: u64, - /// The block index this transaction was placed at. - pub height: u32, -} - -/// Response type for the `bitcoin_get_utxos_query` function. -#[derive(Debug, Clone, CandidType, Deserialize)] -pub struct GetUtxosResponse { - /// A list of UTXOs available for the specified address. - pub utxos: Vec, - /// The hash of the tip. - pub tip_block_hash: Vec, - /// The block index of the tip of the chain known to the IC. - pub tip_height: u32, - /// If `Some`, then `utxos` does not contain the entire results of the query. - /// Call `bitcoin_get_utxos_query` again using `UtxosFilter::Page` for the next page of results. - pub next_page: Option>, -} - impl<'agent> ManagementCanister<'agent> { /// Get the status of a canister. pub fn canister_status( @@ -545,55 +480,4 @@ impl<'agent> ManagementCanister<'agent> { .with_effective_canister_id(*canister_id) .build() } - - /// Gets the BTC balance (in satoshis) of a particular Bitcoin address, filtering by number of confirmations. - /// Most applications should require 6 confirmations. - pub fn bitcoin_get_balance_query( - &self, - address: &str, - network: BitcoinNetwork, - min_confirmations: Option, - ) -> impl 'agent + SyncCall { - #[derive(CandidType)] - struct In<'a> { - address: &'a str, - network: BitcoinNetwork, - min_confirmations: Option, - } - self.query(MgmtMethod::BitcoinGetBalanceQuery.as_ref()) - .with_arg(In { - address, - network, - min_confirmations, - }) - .with_effective_canister_id(Principal::management_canister()) - .build() - } - - /// Fetch the list of [UTXOs](https://en.wikipedia.org/wiki/Unspent_transaction_output) for a Bitcoin address, - /// filtering by number of confirmations. Most applications should require 6 confirmations. - /// - /// This method is paginated. If not all the results can be returned, then `next_page` will be set to `Some`, - /// and its value can be passed to this method to get the next page. - pub fn bitcoin_get_utxos_query( - &self, - address: &str, - network: BitcoinNetwork, - filter: Option, - ) -> impl 'agent + SyncCall { - #[derive(CandidType)] - struct In<'a> { - address: &'a str, - network: BitcoinNetwork, - filter: Option, - } - self.query(MgmtMethod::BitcoinGetUtxosQuery.as_ref()) - .with_arg(In { - address, - network, - filter, - }) - .with_effective_canister_id(Principal::management_canister()) - .build() - } } diff --git a/icx/src/main.rs b/icx/src/main.rs index 824aea7c..c624c8ff 100644 --- a/icx/src/main.rs +++ b/icx/src/main.rs @@ -323,9 +323,6 @@ pub fn get_effective_canister_id( .context("Argument is not valid for InstallChunkedCode")?; Ok(in_args.target_canister) } - MgmtMethod::BitcoinGetBalanceQuery | MgmtMethod::BitcoinGetUtxosQuery => { - Ok(Principal::management_canister()) - } MgmtMethod::BitcoinGetBalance | MgmtMethod::BitcoinGetUtxos | MgmtMethod::BitcoinSendTransaction