Skip to main content

ic_btc_interface/
lib.rs

1//! Types used in the interface of the Bitcoin Canister.
2
3use candid::{CandidType, Deserialize, Principal};
4use datasize::DataSize;
5use serde::Serialize;
6use serde_bytes::ByteBuf;
7use std::fmt;
8use std::str::FromStr;
9
10pub type Address = String;
11pub type Satoshi = u64;
12pub type MillisatoshiPerByte = u64;
13pub type BlockHash = Vec<u8>;
14pub type Height = u32;
15pub type Page = ByteBuf;
16pub type BlockHeader = Vec<u8>;
17
18/// Default stability threshold for the Bitcoin canister.
19/// Must not be zero — a value of 0 can make the canister follow wrong branches,
20/// get stuck, and require a manual reset.
21const DEFAULT_STABILITY_THRESHOLD: u128 = 144; // ~24 hours at 10 min per block
22
23#[derive(CandidType, Clone, Copy, Deserialize, Debug, Eq, PartialEq, Serialize, Hash, DataSize)]
24pub enum Network {
25    /// Bitcoin Mainnet.
26    #[serde(rename = "mainnet")]
27    Mainnet,
28
29    /// Bitcoin Testnet4.
30    #[serde(rename = "testnet")]
31    Testnet,
32
33    /// Bitcoin Regtest.
34    #[serde(rename = "regtest")]
35    Regtest,
36}
37
38impl fmt::Display for Network {
39    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40        match self {
41            Self::Mainnet => write!(f, "mainnet"),
42            Self::Testnet => write!(f, "testnet"),
43            Self::Regtest => write!(f, "regtest"),
44        }
45    }
46}
47
48impl FromStr for Network {
49    type Err = String;
50
51    fn from_str(s: &str) -> Result<Self, Self::Err> {
52        match s {
53            "mainnet" => Ok(Network::Mainnet),
54            "testnet" => Ok(Network::Testnet),
55            "regtest" => Ok(Network::Regtest),
56            _ => Err("Bad network".to_string()),
57        }
58    }
59}
60
61impl From<Network> for NetworkInRequest {
62    fn from(network: Network) -> Self {
63        match network {
64            Network::Mainnet => Self::Mainnet,
65            Network::Testnet => Self::Testnet,
66            Network::Regtest => Self::Regtest,
67        }
68    }
69}
70
71impl From<NetworkInRequest> for Network {
72    fn from(network: NetworkInRequest) -> Self {
73        match network {
74            NetworkInRequest::Mainnet => Self::Mainnet,
75            NetworkInRequest::mainnet => Self::Mainnet,
76            NetworkInRequest::Testnet => Self::Testnet,
77            NetworkInRequest::testnet => Self::Testnet,
78            NetworkInRequest::Regtest => Self::Regtest,
79            NetworkInRequest::regtest => Self::Regtest,
80        }
81    }
82}
83
84/// A network enum that allows both upper and lowercase variants.
85/// Supporting both variants allows us to be compatible with the spec (lowercase)
86/// while not breaking current dapps that are using uppercase variants.
87#[derive(CandidType, Clone, Copy, Deserialize, Debug, Eq, PartialEq, Serialize, Hash)]
88pub enum NetworkInRequest {
89    /// Bitcoin Mainnet.
90    Mainnet,
91    /// Bitcoin Mainnet.
92    #[allow(non_camel_case_types)]
93    mainnet,
94
95    /// Bitcoin Testnet4.
96    Testnet,
97    /// Bitcoin Testnet4.
98    #[allow(non_camel_case_types)]
99    testnet,
100
101    /// Bitcoin Regtest.
102    Regtest,
103    /// Bitcoin Regtest.
104    #[allow(non_camel_case_types)]
105    regtest,
106}
107
108impl fmt::Display for NetworkInRequest {
109    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
110        match self {
111            Self::Mainnet => write!(f, "mainnet"),
112            Self::Testnet => write!(f, "testnet"),
113            Self::Regtest => write!(f, "regtest"),
114            Self::mainnet => write!(f, "mainnet"),
115            Self::testnet => write!(f, "testnet"),
116            Self::regtest => write!(f, "regtest"),
117        }
118    }
119}
120
121#[derive(CandidType, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
122pub struct Txid([u8; 32]);
123
124impl AsRef<[u8]> for Txid {
125    fn as_ref(&self) -> &[u8] {
126        &self.0
127    }
128}
129
130impl From<Txid> for [u8; 32] {
131    fn from(txid: Txid) -> Self {
132        txid.0
133    }
134}
135
136impl serde::Serialize for Txid {
137    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138    where
139        S: serde::ser::Serializer,
140    {
141        serializer.serialize_bytes(&self.0)
142    }
143}
144
145impl<'de> serde::de::Deserialize<'de> for Txid {
146    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147    where
148        D: serde::de::Deserializer<'de>,
149    {
150        struct TxidVisitor;
151
152        impl<'de> serde::de::Visitor<'de> for TxidVisitor {
153            type Value = Txid;
154
155            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
156                formatter.write_str("a 32-byte array")
157            }
158
159            fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
160            where
161                E: serde::de::Error,
162            {
163                match TryInto::<[u8; 32]>::try_into(value) {
164                    Ok(txid) => Ok(Txid(txid)),
165                    Err(_) => Err(E::invalid_length(value.len(), &self)),
166                }
167            }
168
169            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
170            where
171                A: serde::de::SeqAccess<'de>,
172            {
173                use serde::de::Error;
174                if let Some(size_hint) = seq.size_hint() {
175                    if size_hint != 32 {
176                        return Err(A::Error::invalid_length(size_hint, &self));
177                    }
178                }
179                let mut bytes = [0u8; 32];
180                let mut i = 0;
181                while let Some(byte) = seq.next_element()? {
182                    if i == 32 {
183                        return Err(A::Error::invalid_length(i + 1, &self));
184                    }
185
186                    bytes[i] = byte;
187                    i += 1;
188                }
189                if i != 32 {
190                    return Err(A::Error::invalid_length(i, &self));
191                }
192                Ok(Txid(bytes))
193            }
194        }
195
196        deserializer.deserialize_bytes(TxidVisitor)
197    }
198}
199
200impl fmt::Display for Txid {
201    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
202        // In Bitcoin, you display hash bytes in reverse order.
203        //
204        // > Due to historical accident, the tx and block hashes that bitcoin core
205        // > uses are byte-reversed. I’m not entirely sure why. Maybe something
206        // > like using openssl bignum to store hashes or something like that,
207        // > then printing them as a number.
208        // > -- Wladimir van der Laan
209        //
210        // Source: https://learnmeabitcoin.com/technical/txid
211        for b in self.0.iter().rev() {
212            write!(fmt, "{:02x}", *b)?
213        }
214        Ok(())
215    }
216}
217
218impl From<[u8; 32]> for Txid {
219    fn from(bytes: [u8; 32]) -> Self {
220        Self(bytes)
221    }
222}
223
224impl TryFrom<&'_ [u8]> for Txid {
225    type Error = core::array::TryFromSliceError;
226    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
227        let txid: [u8; 32] = bytes.try_into()?;
228        Ok(Txid(txid))
229    }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum TxidFromStrError {
234    InvalidChar(u8),
235    InvalidLength { expected: usize, actual: usize },
236}
237
238impl fmt::Display for TxidFromStrError {
239    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
240        match self {
241            Self::InvalidChar(c) => write!(f, "char {c} is not a valid hex"),
242            Self::InvalidLength { expected, actual } => write!(
243                f,
244                "Bitcoin transaction id must be precisely {expected} characters, got {actual}"
245            ),
246        }
247    }
248}
249
250impl FromStr for Txid {
251    type Err = TxidFromStrError;
252
253    fn from_str(s: &str) -> Result<Self, Self::Err> {
254        fn decode_hex_char(c: u8) -> Result<u8, TxidFromStrError> {
255            match c {
256                b'A'..=b'F' => Ok(c - b'A' + 10),
257                b'a'..=b'f' => Ok(c - b'a' + 10),
258                b'0'..=b'9' => Ok(c - b'0'),
259                _ => Err(TxidFromStrError::InvalidChar(c)),
260            }
261        }
262        if s.len() != 64 {
263            return Err(TxidFromStrError::InvalidLength {
264                expected: 64,
265                actual: s.len(),
266            });
267        }
268        let mut bytes = [0u8; 32];
269        let chars = s.as_bytes();
270        for i in 0..32 {
271            bytes[31 - i] =
272                (decode_hex_char(chars[2 * i])? << 4) | decode_hex_char(chars[2 * i + 1])?;
273        }
274        Ok(Self(bytes))
275    }
276}
277
278/// A reference to a transaction output.
279#[derive(
280    CandidType, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, PartialOrd, Ord,
281)]
282pub struct OutPoint {
283    /// A cryptographic hash of the transaction.
284    /// A transaction can output multiple UTXOs.
285    pub txid: Txid,
286    /// The index of the output within the transaction.
287    pub vout: u32,
288}
289
290/// An unspent transaction output.
291#[derive(
292    CandidType, Debug, Deserialize, Ord, PartialOrd, PartialEq, Serialize, Clone, Hash, Eq,
293)]
294pub struct Utxo {
295    pub outpoint: OutPoint,
296    pub value: Satoshi,
297    pub height: Height,
298}
299
300/// A filter used when requesting UTXOs.
301#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
302pub enum UtxosFilter {
303    MinConfirmations(u32),
304    Page(Page),
305}
306
307impl From<UtxosFilterInRequest> for UtxosFilter {
308    fn from(filter: UtxosFilterInRequest) -> Self {
309        match filter {
310            UtxosFilterInRequest::MinConfirmations(x) => Self::MinConfirmations(x),
311            UtxosFilterInRequest::min_confirmations(x) => Self::MinConfirmations(x),
312            UtxosFilterInRequest::Page(p) => Self::Page(p),
313            UtxosFilterInRequest::page(p) => Self::Page(p),
314        }
315    }
316}
317
318/// A UtxosFilter enum that allows both upper and lowercase variants.
319/// Supporting both variants allows us to be compatible with the spec (lowercase)
320/// while not breaking current dapps that are using uppercase variants.
321#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
322pub enum UtxosFilterInRequest {
323    MinConfirmations(u32),
324    #[allow(non_camel_case_types)]
325    min_confirmations(u32),
326    Page(Page),
327    #[allow(non_camel_case_types)]
328    page(Page),
329}
330
331/// A request for getting the UTXOs for a given address.
332#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
333pub struct GetUtxosRequest {
334    pub address: Address,
335    pub network: NetworkInRequest,
336    pub filter: Option<UtxosFilterInRequest>,
337}
338
339/// The response returned for a request to get the UTXOs of a given address.
340#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
341pub struct GetUtxosResponse {
342    pub utxos: Vec<Utxo>,
343    pub tip_block_hash: BlockHash,
344    pub tip_height: Height,
345    pub next_page: Option<Page>,
346}
347
348/// Errors when processing a `get_utxos` request.
349#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
350pub enum GetUtxosError {
351    MalformedAddress,
352    AddressForWrongNetwork { expected: Network },
353    MinConfirmationsTooLarge { given: u32, max: u32 },
354    UnknownTipBlockHash { tip_block_hash: BlockHash },
355    MalformedPage { err: String },
356}
357
358/// A request for getting the block headers from a given height.
359#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
360pub struct GetBlockHeadersRequest {
361    pub start_height: Height,
362    pub end_height: Option<Height>,
363    pub network: NetworkInRequest,
364}
365
366/// The response returned for a request for getting the block headers from a given height.
367#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
368pub struct GetBlockHeadersResponse {
369    pub tip_height: Height,
370    pub block_headers: Vec<BlockHeader>,
371}
372
373/// Errors when processing a `get_block_headers` request.
374#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
375pub enum GetBlockHeadersError {
376    StartHeightDoesNotExist {
377        requested: Height,
378        chain_height: Height,
379    },
380    EndHeightDoesNotExist {
381        requested: Height,
382        chain_height: Height,
383    },
384    StartHeightLargerThanEndHeight {
385        start_height: Height,
386        end_height: Height,
387    },
388}
389
390impl fmt::Display for GetBlockHeadersError {
391    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392        match self {
393            Self::StartHeightDoesNotExist {
394                requested,
395                chain_height,
396            } => {
397                write!(
398                    f,
399                    "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
400                    requested, chain_height
401                )
402            }
403            Self::EndHeightDoesNotExist {
404                requested,
405                chain_height,
406            } => {
407                write!(
408                    f,
409                    "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
410                    requested, chain_height
411                )
412            }
413            Self::StartHeightLargerThanEndHeight {
414                start_height,
415                end_height,
416            } => {
417                write!(
418                    f,
419                    "The requested start_height is larger than the requested end_height. start_height: {}, end_height: {}", start_height, end_height)
420            }
421        }
422    }
423}
424
425/// A request for getting the current fee percentiles.
426#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
427pub struct GetCurrentFeePercentilesRequest {
428    pub network: NetworkInRequest,
429}
430
431impl fmt::Display for GetUtxosError {
432    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433        match self {
434            Self::MalformedAddress => {
435                write!(f, "Malformed address.")
436            }
437            Self::MinConfirmationsTooLarge { given, max } => {
438                write!(
439                    f,
440                    "The requested min_confirmations is too large. Given: {}, max supported: {}",
441                    given, max
442                )
443            }
444            Self::UnknownTipBlockHash { tip_block_hash } => {
445                write!(
446                    f,
447                    "The provided tip block hash {:?} is unknown.",
448                    tip_block_hash
449                )
450            }
451            Self::MalformedPage { err } => {
452                write!(f, "The provided page is malformed {}", err)
453            }
454            Self::AddressForWrongNetwork { expected } => {
455                write!(
456                    f,
457                    "Address does not belong to the expected network: {}",
458                    expected
459                )
460            }
461        }
462    }
463}
464
465#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
466pub struct GetBalanceRequest {
467    pub address: Address,
468    pub network: NetworkInRequest,
469    pub min_confirmations: Option<u32>,
470}
471
472#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
473pub enum GetBalanceError {
474    MalformedAddress,
475    AddressForWrongNetwork { expected: Network },
476    MinConfirmationsTooLarge { given: u32, max: u32 },
477}
478
479impl fmt::Display for GetBalanceError {
480    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
481        match self {
482            Self::MalformedAddress => {
483                write!(f, "Malformed address.")
484            }
485            Self::MinConfirmationsTooLarge { given, max } => {
486                write!(
487                    f,
488                    "The requested min_confirmations is too large. Given: {}, max supported: {}",
489                    given, max
490                )
491            }
492            Self::AddressForWrongNetwork { expected } => {
493                write!(
494                    f,
495                    "Address does not belong to the expected network: {}",
496                    expected
497                )
498            }
499        }
500    }
501}
502
503#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
504pub struct SendTransactionRequest {
505    #[serde(with = "serde_bytes")]
506    pub transaction: Vec<u8>,
507    pub network: NetworkInRequest,
508}
509
510#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq)]
511pub enum SendTransactionError {
512    /// Can't deserialize transaction.
513    MalformedTransaction,
514    /// Enqueueing a request failed due to full queue to the Bitcoin adapter.
515    QueueFull,
516}
517
518impl fmt::Display for SendTransactionError {
519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520        match self {
521            Self::MalformedTransaction => {
522                write!(f, "Can't deserialize transaction because it's malformed.")
523            }
524            Self::QueueFull => {
525                write!(
526                    f,
527                    "Request can not be enqueued because the queue has reached its capacity. Please retry later."
528                )
529            }
530        }
531    }
532}
533
534/// A request to update the canister's config.
535#[derive(CandidType, Deserialize, Default, Serialize)]
536pub struct SetConfigRequest {
537    pub stability_threshold: Option<u128>,
538
539    /// Whether or not to enable/disable syncing of blocks from the network.
540    pub syncing: Option<Flag>,
541
542    /// The fees to charge for the various endpoints.
543    pub fees: Option<Fees>,
544
545    /// Whether or not to enable/disable the bitcoin apis.
546    pub api_access: Option<Flag>,
547
548    /// Whether or not to enable/disable the bitcoin apis if not fully synced.
549    pub disable_api_if_not_fully_synced: Option<Flag>,
550
551    /// The principal of the watchdog canister.
552    /// The watchdog canister has the authority to disable the Bitcoin canister's API
553    /// if it suspects that there is a problem.
554    pub watchdog_canister: Option<Option<Principal>>,
555
556    /// If enabled, fee percentiles are only computed when requested.
557    /// Otherwise, they are computed whenever we receive a new block.
558    pub lazily_evaluate_fee_percentiles: Option<Flag>,
559
560    /// If enabled, continuously burns all cycles in the canister's balance
561    /// to count towards the IC's burn rate.
562    pub burn_cycles: Option<Flag>,
563}
564
565#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Copy, Clone, Debug, Default)]
566pub enum Flag {
567    #[serde(rename = "enabled")]
568    #[default]
569    Enabled,
570    #[serde(rename = "disabled")]
571    Disabled,
572}
573
574#[derive(CandidType, Deserialize)]
575pub enum CanisterArg {
576    #[serde(rename = "init")]
577    Init(InitConfig),
578    #[serde(rename = "upgrade")]
579    Upgrade(Option<SetConfigRequest>),
580}
581
582/// The config used to initialize the canister.
583///
584/// This struct is equivalent to `Config`, except that all its fields are optional.
585/// Fields that are not specified here are loaded with their default value. See
586/// `Config::default()`.
587#[derive(CandidType, Deserialize, Debug, Default)]
588pub struct InitConfig {
589    pub stability_threshold: Option<u128>,
590    pub network: Option<Network>,
591    pub blocks_source: Option<Principal>,
592    pub syncing: Option<Flag>,
593    pub fees: Option<Fees>,
594    pub api_access: Option<Flag>,
595    pub disable_api_if_not_fully_synced: Option<Flag>,
596    pub watchdog_canister: Option<Option<Principal>>,
597    pub burn_cycles: Option<Flag>,
598    pub lazily_evaluate_fee_percentiles: Option<Flag>,
599}
600
601/// The config of the canister.
602#[derive(CandidType, Deserialize, Debug)]
603pub struct Config {
604    pub stability_threshold: u128,
605    pub network: Network,
606
607    /// The principal from which blocks are retrieved.
608    ///
609    /// Setting this source to the management canister means that the blocks will be
610    /// fetched directly from the replica, and that's what is used in production.
611    pub blocks_source: Principal,
612
613    pub syncing: Flag,
614
615    pub fees: Fees,
616
617    /// Flag to control access to the apis provided by the canister.
618    pub api_access: Flag,
619
620    /// Flag to determine if the API should be automatically disabled if
621    /// the canister isn't fully synced.
622    pub disable_api_if_not_fully_synced: Flag,
623
624    /// The principal of the watchdog canister.
625    /// The watchdog canister has the authority to disable the Bitcoin canister's API
626    /// if it suspects that there is a problem.
627    pub watchdog_canister: Option<Principal>,
628
629    /// If enabled, continuously burns all cycles in its balance
630    /// (to count towards the IC's burn rate).
631    pub burn_cycles: Flag,
632
633    /// If enabled, fee percentiles are only computed when requested.
634    /// Otherwise, they are computed whenever we receive a new block.
635    pub lazily_evaluate_fee_percentiles: Flag,
636}
637
638impl From<InitConfig> for Config {
639    fn from(init_config: InitConfig) -> Self {
640        let mut config = Config::default();
641
642        if let Some(stability_threshold) = init_config.stability_threshold {
643            config.stability_threshold = stability_threshold;
644        }
645
646        if let Some(network) = init_config.network {
647            config.network = network;
648        }
649
650        if let Some(blocks_source) = init_config.blocks_source {
651            config.blocks_source = blocks_source;
652        }
653
654        if let Some(syncing) = init_config.syncing {
655            config.syncing = syncing;
656        }
657
658        let fees_explicitly_set = init_config.fees.is_some();
659        if let Some(fees) = init_config.fees {
660            config.fees = fees;
661        }
662
663        if let Some(api_access) = init_config.api_access {
664            config.api_access = api_access;
665        }
666
667        if let Some(disable_api_if_not_fully_synced) = init_config.disable_api_if_not_fully_synced {
668            config.disable_api_if_not_fully_synced = disable_api_if_not_fully_synced;
669        }
670
671        if let Some(watchdog_canister) = init_config.watchdog_canister {
672            config.watchdog_canister = watchdog_canister;
673        }
674
675        if let Some(burn_cycles) = init_config.burn_cycles {
676            config.burn_cycles = burn_cycles;
677        }
678
679        if let Some(lazily_evaluate_fee_percentiles) = init_config.lazily_evaluate_fee_percentiles {
680            config.lazily_evaluate_fee_percentiles = lazily_evaluate_fee_percentiles;
681        }
682
683        // Config post-processing.
684        if !fees_explicitly_set {
685            config.fees = match config.network {
686                Network::Mainnet => Fees::mainnet(),
687                Network::Testnet => Fees::testnet(),
688                Network::Regtest => config.fees, // Keep unchanged for regtest.
689            };
690        }
691
692        config
693    }
694}
695
696impl Default for Config {
697    fn default() -> Self {
698        Self {
699            stability_threshold: DEFAULT_STABILITY_THRESHOLD,
700            network: Network::Regtest,
701            blocks_source: Principal::management_canister(),
702            syncing: Flag::Enabled,
703            fees: Fees::default(),
704            api_access: Flag::Enabled,
705            disable_api_if_not_fully_synced: Flag::Enabled,
706            watchdog_canister: None,
707            burn_cycles: Flag::Disabled,
708            lazily_evaluate_fee_percentiles: Flag::Disabled,
709        }
710    }
711}
712
713#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)]
714pub struct Fees {
715    /// The base fee to charge for all `get_utxos` requests.
716    pub get_utxos_base: u128,
717
718    /// The number of cycles to charge per 10 instructions.
719    pub get_utxos_cycles_per_ten_instructions: u128,
720
721    /// The maximum amount of cycles that can be charged in a `get_utxos` request.
722    /// A request must send at least this amount for it to be accepted.
723    pub get_utxos_maximum: u128,
724
725    /// The flat fee to charge for a `get_balance` request.
726    pub get_balance: u128,
727
728    /// The maximum amount of cycles that can be charged in a `get_balance` request.
729    /// A request must send at least this amount for it to be accepted.
730    pub get_balance_maximum: u128,
731
732    /// The flat fee to charge for a `get_current_fee_percentiles` request.
733    pub get_current_fee_percentiles: u128,
734
735    /// The maximum amount of cycles that can be charged in a `get_current_fee_percentiles` request.
736    /// A request must send at least this amount for it to be accepted.
737    pub get_current_fee_percentiles_maximum: u128,
738
739    /// The base fee to charge for all `send_transaction` requests.
740    pub send_transaction_base: u128,
741
742    /// The number of cycles to charge for each byte in the transaction.
743    pub send_transaction_per_byte: u128,
744
745    #[serde(default)]
746    /// The base fee to charge for all `get_block_headers` requests.
747    pub get_block_headers_base: u128,
748
749    #[serde(default)]
750    /// The number of cycles to charge per 10 instructions.
751    pub get_block_headers_cycles_per_ten_instructions: u128,
752
753    #[serde(default)]
754    /// The maximum amount of cycles that can be charged in a `get_block_headers` request.
755    /// A request must send at least this amount for it to be accepted.
756    pub get_block_headers_maximum: u128,
757}
758
759impl Fees {
760    pub fn testnet() -> Self {
761        // https://internetcomputer.org/docs/references/bitcoin-how-it-works#bitcoin-testnet
762        Self {
763            get_utxos_base: 20_000_000,
764            get_utxos_cycles_per_ten_instructions: 4,
765            get_utxos_maximum: 4_000_000_000,
766
767            get_current_fee_percentiles: 4_000_000,
768            get_current_fee_percentiles_maximum: 40_000_000,
769
770            get_balance: 4_000_000,
771            get_balance_maximum: 40_000_000,
772
773            send_transaction_base: 2_000_000_000,
774            send_transaction_per_byte: 8_000_000,
775
776            get_block_headers_base: 20_000_000,
777            get_block_headers_cycles_per_ten_instructions: 4,
778            get_block_headers_maximum: 4_000_000_000,
779        }
780    }
781
782    pub fn mainnet() -> Self {
783        // https://internetcomputer.org/docs/references/bitcoin-how-it-works#bitcoin-mainnet
784        Self {
785            get_utxos_base: 50_000_000,
786            get_utxos_cycles_per_ten_instructions: 10,
787            get_utxos_maximum: 10_000_000_000,
788
789            get_current_fee_percentiles: 10_000_000,
790            get_current_fee_percentiles_maximum: 100_000_000,
791
792            get_balance: 10_000_000,
793            get_balance_maximum: 100_000_000,
794
795            send_transaction_base: 5_000_000_000,
796            send_transaction_per_byte: 20_000_000,
797
798            get_block_headers_base: 50_000_000,
799            get_block_headers_cycles_per_ten_instructions: 10,
800            get_block_headers_maximum: 10_000_000_000,
801        }
802    }
803}
804
805/// Information about the blockchain as seen by the canister.
806///
807/// Returns information about the main chain tip. The main chain is the
808/// canister's best guess at what the Bitcoin network considers the canonical chain.
809/// It is defined as the chain with the most accumulated proof-of-work (difficulty),
810/// following Bitcoin's consensus rule. When accumulated difficulties are tied,
811/// the longest branch (by block count) wins. If branches still tie on both
812/// criteria, the chain ends at the fork point (contested tip).
813#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
814pub struct BlockchainInfo {
815    /// The height of the main chain tip.
816    pub height: Height,
817    /// The hash of the tip block.
818    pub block_hash: BlockHash,
819    /// Unix timestamp of the tip block (seconds since epoch).
820    pub timestamp: u32,
821    /// Difficulty of the tip block.
822    pub difficulty: u128,
823    /// Total number of UTXOs up to the main chain tip (stable + unstable main chain blocks).
824    pub utxos_length: u64,
825}
826
827#[cfg(test)]
828mod test {
829    use super::*;
830
831    #[test]
832    fn test_config_debug_formatter_is_enabled() {
833        // Verify that debug formatter for Config is enabled.
834        // This might be important for logging and debugging purposes.
835        assert!(
836            !format!("{:?}", Config::default()).is_empty(),
837            "Config should be printable using debug formatter {{:?}}."
838        );
839    }
840
841    #[test]
842    fn can_extract_bytes_from_txid() {
843        let tx_id = Txid([1; 32]);
844        let tx: [u8; 32] = tx_id.into();
845        assert_eq!(tx, [1; 32]);
846    }
847}