ic_utils/interfaces/
bitcoin_canister.rs

1//! The canister interface for the [Bitcoin canister](https://github.com/dfinity/bitcoin-canister).
2
3use std::ops::Deref;
4
5use candid::{CandidType, Principal};
6use ic_agent::{Agent, AgentError};
7use serde::Deserialize;
8
9use crate::{
10    call::{AsyncCall, SyncCall},
11    Canister,
12};
13
14/// The canister interface for the IC [Bitcoin canister](https://github.com/dfinity/bitcoin-canister).
15#[derive(Debug)]
16pub struct BitcoinCanister<'agent> {
17    canister: Canister<'agent>,
18    network: BitcoinNetwork,
19}
20
21impl<'agent> Deref for BitcoinCanister<'agent> {
22    type Target = Canister<'agent>;
23    fn deref(&self) -> &Self::Target {
24        &self.canister
25    }
26}
27const MAINNET_ID: Principal =
28    Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, 0xa0, 0x00, 0x04, 0x01, 0x01]);
29const TESTNET_ID: Principal =
30    Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x01, 0xa0, 0x00, 0x01, 0x01, 0x01]);
31
32impl<'agent> BitcoinCanister<'agent> {
33    /// Create a `BitcoinCanister` interface from an existing canister object.
34    pub fn from_canister(canister: Canister<'agent>, network: BitcoinNetwork) -> Self {
35        Self { canister, network }
36    }
37    /// Create a `BitcoinCanister` interface pointing to the specified canister ID.
38    pub fn create(agent: &'agent Agent, canister_id: Principal, network: BitcoinNetwork) -> Self {
39        Self::from_canister(
40            Canister::builder()
41                .with_agent(agent)
42                .with_canister_id(canister_id)
43                .build()
44                .expect("all required fields should be set"),
45            network,
46        )
47    }
48    /// Create a `BitcoinCanister` interface for the Bitcoin mainnet canister on the IC mainnet.
49    pub fn mainnet(agent: &'agent Agent) -> Self {
50        Self::for_network(agent, BitcoinNetwork::Mainnet).expect("valid network")
51    }
52    /// Create a `BitcoinCanister` interface for the Bitcoin testnet canister on the IC mainnet.
53    pub fn testnet(agent: &'agent Agent) -> Self {
54        Self::for_network(agent, BitcoinNetwork::Testnet).expect("valid network")
55    }
56    /// Create a `BitcoinCanister` interface for the specified Bitcoin network on the IC mainnet. Errors if `Regtest` is specified.
57    pub fn for_network(agent: &'agent Agent, network: BitcoinNetwork) -> Result<Self, AgentError> {
58        let canister_id = match network {
59            BitcoinNetwork::Mainnet => MAINNET_ID,
60            BitcoinNetwork::Testnet => TESTNET_ID,
61            BitcoinNetwork::Regtest => {
62                return Err(AgentError::MessageError(
63                    "No applicable canister ID for regtest".to_string(),
64                ))
65            }
66        };
67        Ok(Self::create(agent, canister_id, network))
68    }
69
70    /// Gets the BTC balance (in satoshis) of a particular Bitcoin address, filtering by number of confirmations.
71    /// Most applications should require 6 confirmations.
72    pub fn get_balance(
73        &self,
74        address: &str,
75        min_confirmations: Option<u32>,
76    ) -> impl 'agent + AsyncCall<Value = (u64,)> {
77        self.update("bitcoin_get_balance")
78            .with_arg(GetBalance {
79                address,
80                network: self.network,
81                min_confirmations,
82            })
83            .build()
84    }
85
86    /// Gets the BTC balance (in satoshis) of a particular Bitcoin address, filtering by number of confirmations.
87    /// Most applications should require 6 confirmations.
88    pub fn get_balance_query(
89        &self,
90        address: &str,
91        min_confirmations: Option<u32>,
92    ) -> impl 'agent + SyncCall<Value = (u64,)> {
93        self.query("bitcoin_get_balance_query")
94            .with_arg(GetBalance {
95                address,
96                network: self.network,
97                min_confirmations,
98            })
99            .build()
100    }
101
102    /// Fetch the list of [UTXOs](https://en.wikipedia.org/wiki/Unspent_transaction_output) for a Bitcoin address,
103    /// filtering by number of confirmations. Most applications should require 6 confirmations.
104    ///
105    /// This method is paginated. If not all the results can be returned, then `next_page` will be set to `Some`,
106    /// and its value can be passed to this method to get the next page.
107    pub fn get_utxos(
108        &self,
109        address: &str,
110        filter: Option<UtxosFilter>,
111    ) -> impl 'agent + AsyncCall<Value = (GetUtxosResponse,)> {
112        self.update("bitcoin_get_utxos")
113            .with_arg(GetUtxos {
114                address,
115                network: self.network,
116                filter,
117            })
118            .build()
119    }
120
121    /// Fetch the list of [UTXOs](https://en.wikipedia.org/wiki/Unspent_transaction_output) for a Bitcoin address,
122    /// filtering by number of confirmations. Most applications should require 6 confirmations.
123    ///
124    /// This method is paginated. If not all the results can be returned, then `next_page` will be set to `Some`,
125    /// and its value can be passed to this method to get the next page.
126    pub fn get_utxos_query(
127        &self,
128        address: &str,
129        filter: Option<UtxosFilter>,
130    ) -> impl 'agent + SyncCall<Value = (GetUtxosResponse,)> {
131        self.query("bitcoin_get_utxos_query")
132            .with_arg(GetUtxos {
133                address,
134                network: self.network,
135                filter,
136            })
137            .build()
138    }
139
140    /// Gets the transaction fee percentiles for the last 10,000 transactions. In the returned vector, `v[i]` is the `i`th percentile fee,
141    /// measured in millisatoshis/vbyte, and `v[0]` is the smallest fee.
142    pub fn get_current_fee_percentiles(&self) -> impl 'agent + AsyncCall<Value = (Vec<u64>,)> {
143        #[derive(CandidType)]
144        struct In {
145            network: BitcoinNetwork,
146        }
147        self.update("bitcoin_get_current_fee_percentiles")
148            .with_arg(In {
149                network: self.network,
150            })
151            .build()
152    }
153    /// 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
154    /// the chain was queried.
155    pub fn get_block_headers(
156        &self,
157        start_height: u32,
158        end_height: Option<u32>,
159    ) -> impl 'agent + AsyncCall<Value = (GetBlockHeadersResponse,)> {
160        #[derive(CandidType)]
161        struct In {
162            start_height: u32,
163            end_height: Option<u32>,
164        }
165        self.update("bitcoin_get_block_headers")
166            .with_arg(In {
167                start_height,
168                end_height,
169            })
170            .build()
171    }
172    /// Submits a new Bitcoin transaction. No guarantees are made about the outcome.
173    pub fn send_transaction(&self, transaction: Vec<u8>) -> impl 'agent + AsyncCall<Value = ()> {
174        #[derive(CandidType, Deserialize)]
175        struct In {
176            network: BitcoinNetwork,
177            #[serde(with = "serde_bytes")]
178            transaction: Vec<u8>,
179        }
180        self.update("bitcoin_send_transaction")
181            .with_arg(In {
182                network: self.network,
183                transaction,
184            })
185            .build()
186    }
187}
188
189#[derive(Debug, CandidType)]
190struct GetBalance<'a> {
191    address: &'a str,
192    network: BitcoinNetwork,
193    min_confirmations: Option<u32>,
194}
195
196#[derive(Debug, CandidType)]
197struct GetUtxos<'a> {
198    address: &'a str,
199    network: BitcoinNetwork,
200    filter: Option<UtxosFilter>,
201}
202
203/// The Bitcoin network that a Bitcoin transaction is placed on.
204#[derive(Clone, Copy, Debug, CandidType, Deserialize, PartialEq, Eq)]
205pub enum BitcoinNetwork {
206    /// The BTC network.
207    #[serde(rename = "mainnet")]
208    Mainnet,
209    /// The TESTBTC network.
210    #[serde(rename = "testnet")]
211    Testnet,
212    /// The REGTEST network.
213    ///
214    /// This is only available when developing with local replica.
215    #[serde(rename = "regtest")]
216    Regtest,
217}
218
219/// Defines how to filter results from [`BitcoinCanister::get_utxos_query`].
220#[derive(Debug, Clone, CandidType, Deserialize)]
221pub enum UtxosFilter {
222    /// Filter by the minimum number of UTXO confirmations. Most applications should set this to 6.
223    #[serde(rename = "min_confirmations")]
224    MinConfirmations(u32),
225    /// When paginating results, use this page. Provided by [`GetUtxosResponse.next_page`](GetUtxosResponse).
226    #[serde(rename = "page")]
227    Page(#[serde(with = "serde_bytes")] Vec<u8>),
228}
229
230/// Unique output descriptor of a Bitcoin transaction.
231#[derive(Debug, Clone, CandidType, Deserialize)]
232pub struct UtxoOutpoint {
233    /// The ID of the transaction. Not necessarily unique on its own.
234    #[serde(with = "serde_bytes")]
235    pub txid: Vec<u8>,
236    /// The index of the outpoint within the transaction.
237    pub vout: u32,
238}
239
240/// A Bitcoin [`UTXO`](https://en.wikipedia.org/wiki/Unspent_transaction_output), produced by a transaction.
241#[derive(Debug, Clone, CandidType, Deserialize)]
242pub struct Utxo {
243    /// The transaction outpoint that produced this UTXO.
244    pub outpoint: UtxoOutpoint,
245    /// The BTC quantity, in satoshis.
246    pub value: u64,
247    /// The block index this transaction was placed at.
248    pub height: u32,
249}
250
251/// Response type for the [`BitcoinCanister::get_utxos_query`] function.
252#[derive(Debug, Clone, CandidType, Deserialize)]
253pub struct GetUtxosResponse {
254    /// A list of UTXOs available for the specified address.
255    pub utxos: Vec<Utxo>,
256    /// The hash of the tip.
257    #[serde(with = "serde_bytes")]
258    pub tip_block_hash: Vec<u8>,
259    /// The block index of the tip of the chain known to the IC.
260    pub tip_height: u32,
261    /// If `Some`, then `utxos` does not contain the entire results of the query.
262    /// Call `bitcoin_get_utxos_query` again using `UtxosFilter::Page` for the next page of results.
263    pub next_page: Option<Vec<u8>>,
264}
265
266/// Response type for the [`BitcoinCanister::get_block_headers`] function.
267#[derive(Debug, Clone, CandidType, Deserialize)]
268pub struct GetBlockHeadersResponse {
269    /// The tip of the chain, current to when the headers were fetched.
270    pub tip_height: u32,
271    /// The headers of the requested block range.
272    pub block_headers: Vec<Vec<u8>>,
273}