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        #[derive(CandidType)]
78        struct In<'a> {
79            address: &'a str,
80            network: BitcoinNetwork,
81            min_confirmations: Option<u32>,
82        }
83        self.update("bitcoin_get_balance")
84            .with_arg(GetBalance {
85                address,
86                network: self.network,
87                min_confirmations,
88            })
89            .build()
90    }
91
92    /// Gets the BTC balance (in satoshis) of a particular Bitcoin address, filtering by number of confirmations.
93    /// Most applications should require 6 confirmations.
94    pub fn get_balance_query(
95        &self,
96        address: &str,
97        min_confirmations: Option<u32>,
98    ) -> impl 'agent + SyncCall<Value = (u64,)> {
99        self.query("bitcoin_get_balance_query")
100            .with_arg(GetBalance {
101                address,
102                network: self.network,
103                min_confirmations,
104            })
105            .build()
106    }
107
108    /// Fetch the list of [UTXOs](https://en.wikipedia.org/wiki/Unspent_transaction_output) for a Bitcoin address,
109    /// filtering by number of confirmations. Most applications should require 6 confirmations.
110    ///
111    /// This method is paginated. If not all the results can be returned, then `next_page` will be set to `Some`,
112    /// and its value can be passed to this method to get the next page.
113    pub fn get_utxos(
114        &self,
115        address: &str,
116        filter: Option<UtxosFilter>,
117    ) -> impl 'agent + AsyncCall<Value = (GetUtxosResponse,)> {
118        self.update("bitcoin_get_utxos")
119            .with_arg(GetUtxos {
120                address,
121                network: self.network,
122                filter,
123            })
124            .build()
125    }
126
127    /// Fetch the list of [UTXOs](https://en.wikipedia.org/wiki/Unspent_transaction_output) for a Bitcoin address,
128    /// filtering by number of confirmations. Most applications should require 6 confirmations.
129    ///
130    /// This method is paginated. If not all the results can be returned, then `next_page` will be set to `Some`,
131    /// and its value can be passed to this method to get the next page.
132    pub fn get_utxos_query(
133        &self,
134        address: &str,
135        filter: Option<UtxosFilter>,
136    ) -> impl 'agent + SyncCall<Value = (GetUtxosResponse,)> {
137        self.query("bitcoin_get_utxos_query")
138            .with_arg(GetUtxos {
139                address,
140                network: self.network,
141                filter,
142            })
143            .build()
144    }
145
146    /// Gets the transaction fee percentiles for the last 10,000 transactions. In the returned vector, `v[i]` is the `i`th percentile fee,
147    /// measured in millisatoshis/vbyte, and `v[0]` is the smallest fee.
148    pub fn get_current_fee_percentiles(&self) -> impl 'agent + AsyncCall<Value = (Vec<u64>,)> {
149        #[derive(CandidType)]
150        struct In {
151            network: BitcoinNetwork,
152        }
153        self.update("bitcoin_get_current_fee_percentiles")
154            .with_arg(In {
155                network: self.network,
156            })
157            .build()
158    }
159    /// 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
160    /// the chain was queried.
161    pub fn get_block_headers(
162        &self,
163        start_height: u32,
164        end_height: Option<u32>,
165    ) -> impl 'agent + AsyncCall<Value = (GetBlockHeadersResponse,)> {
166        #[derive(CandidType)]
167        struct In {
168            start_height: u32,
169            end_height: Option<u32>,
170        }
171        self.update("bitcoin_get_block_headers")
172            .with_arg(In {
173                start_height,
174                end_height,
175            })
176            .build()
177    }
178    /// Submits a new Bitcoin transaction. No guarantees are made about the outcome.
179    pub fn send_transaction(&self, transaction: Vec<u8>) -> impl 'agent + AsyncCall<Value = ()> {
180        #[derive(CandidType, Deserialize)]
181        struct In {
182            network: BitcoinNetwork,
183            #[serde(with = "serde_bytes")]
184            transaction: Vec<u8>,
185        }
186        self.update("bitcoin_send_transaction")
187            .with_arg(In {
188                network: self.network,
189                transaction,
190            })
191            .build()
192    }
193}
194
195#[derive(Debug, CandidType)]
196struct GetBalance<'a> {
197    address: &'a str,
198    network: BitcoinNetwork,
199    min_confirmations: Option<u32>,
200}
201
202#[derive(Debug, CandidType)]
203struct GetUtxos<'a> {
204    address: &'a str,
205    network: BitcoinNetwork,
206    filter: Option<UtxosFilter>,
207}
208
209/// The Bitcoin network that a Bitcoin transaction is placed on.
210#[derive(Clone, Copy, Debug, CandidType, Deserialize, PartialEq, Eq)]
211pub enum BitcoinNetwork {
212    /// The BTC network.
213    #[serde(rename = "mainnet")]
214    Mainnet,
215    /// The TESTBTC network.
216    #[serde(rename = "testnet")]
217    Testnet,
218    /// The REGTEST network.
219    ///
220    /// This is only available when developing with local replica.
221    #[serde(rename = "regtest")]
222    Regtest,
223}
224
225/// Defines how to filter results from [`BitcoinCanister::get_utxos_query`].
226#[derive(Debug, Clone, CandidType, Deserialize)]
227pub enum UtxosFilter {
228    /// Filter by the minimum number of UTXO confirmations. Most applications should set this to 6.
229    #[serde(rename = "min_confirmations")]
230    MinConfirmations(u32),
231    /// When paginating results, use this page. Provided by [`GetUtxosResponse.next_page`](GetUtxosResponse).
232    #[serde(rename = "page")]
233    Page(#[serde(with = "serde_bytes")] Vec<u8>),
234}
235
236/// Unique output descriptor of a Bitcoin transaction.
237#[derive(Debug, Clone, CandidType, Deserialize)]
238pub struct UtxoOutpoint {
239    /// The ID of the transaction. Not necessarily unique on its own.
240    #[serde(with = "serde_bytes")]
241    pub txid: Vec<u8>,
242    /// The index of the outpoint within the transaction.
243    pub vout: u32,
244}
245
246/// A Bitcoin [`UTXO`](https://en.wikipedia.org/wiki/Unspent_transaction_output), produced by a transaction.
247#[derive(Debug, Clone, CandidType, Deserialize)]
248pub struct Utxo {
249    /// The transaction outpoint that produced this UTXO.
250    pub outpoint: UtxoOutpoint,
251    /// The BTC quantity, in satoshis.
252    pub value: u64,
253    /// The block index this transaction was placed at.
254    pub height: u32,
255}
256
257/// Response type for the [`BitcoinCanister::get_utxos_query`] function.
258#[derive(Debug, Clone, CandidType, Deserialize)]
259pub struct GetUtxosResponse {
260    /// A list of UTXOs available for the specified address.
261    pub utxos: Vec<Utxo>,
262    /// The hash of the tip.
263    #[serde(with = "serde_bytes")]
264    pub tip_block_hash: Vec<u8>,
265    /// The block index of the tip of the chain known to the IC.
266    pub tip_height: u32,
267    /// If `Some`, then `utxos` does not contain the entire results of the query.
268    /// Call `bitcoin_get_utxos_query` again using `UtxosFilter::Page` for the next page of results.
269    pub next_page: Option<Vec<u8>>,
270}
271
272/// Response type for the [`BitcoinCanister::get_block_headers`] function.
273#[derive(Debug, Clone, CandidType, Deserialize)]
274pub struct GetBlockHeadersResponse {
275    /// The tip of the chain, current to when the headers were fetched.
276    pub tip_height: u32,
277    /// The headers of the requested block range.
278    pub block_headers: Vec<Vec<u8>>,
279}