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    MinConfirmationsTooLarge { given: u32, max: u32 },
353    UnknownTipBlockHash { tip_block_hash: BlockHash },
354    MalformedPage { err: String },
355}
356
357/// A request for getting the block headers from a given height.
358#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
359pub struct GetBlockHeadersRequest {
360    pub start_height: Height,
361    pub end_height: Option<Height>,
362    pub network: NetworkInRequest,
363}
364
365/// The response returned for a request for getting the block headers from a given height.
366#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
367pub struct GetBlockHeadersResponse {
368    pub tip_height: Height,
369    pub block_headers: Vec<BlockHeader>,
370}
371
372/// Errors when processing a `get_block_headers` request.
373#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
374pub enum GetBlockHeadersError {
375    StartHeightDoesNotExist {
376        requested: Height,
377        chain_height: Height,
378    },
379    EndHeightDoesNotExist {
380        requested: Height,
381        chain_height: Height,
382    },
383    StartHeightLargerThanEndHeight {
384        start_height: Height,
385        end_height: Height,
386    },
387}
388
389impl fmt::Display for GetBlockHeadersError {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        match self {
392            Self::StartHeightDoesNotExist {
393                requested,
394                chain_height,
395            } => {
396                write!(
397                    f,
398                    "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
399                    requested, chain_height
400                )
401            }
402            Self::EndHeightDoesNotExist {
403                requested,
404                chain_height,
405            } => {
406                write!(
407                    f,
408                    "The requested start_height is larger than the height of the chain. Requested: {}, height of chain: {}",
409                    requested, chain_height
410                )
411            }
412            Self::StartHeightLargerThanEndHeight {
413                start_height,
414                end_height,
415            } => {
416                write!(
417                    f,
418                    "The requested start_height is larger than the requested end_height. start_height: {}, end_height: {}", start_height, end_height)
419            }
420        }
421    }
422}
423
424/// A request for getting the current fee percentiles.
425#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
426pub struct GetCurrentFeePercentilesRequest {
427    pub network: NetworkInRequest,
428}
429
430impl fmt::Display for GetUtxosError {
431    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432        match self {
433            Self::MalformedAddress => {
434                write!(f, "Malformed address.")
435            }
436            Self::MinConfirmationsTooLarge { given, max } => {
437                write!(
438                    f,
439                    "The requested min_confirmations is too large. Given: {}, max supported: {}",
440                    given, max
441                )
442            }
443            Self::UnknownTipBlockHash { tip_block_hash } => {
444                write!(
445                    f,
446                    "The provided tip block hash {:?} is unknown.",
447                    tip_block_hash
448                )
449            }
450            Self::MalformedPage { err } => {
451                write!(f, "The provided page is malformed {}", err)
452            }
453        }
454    }
455}
456
457#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
458pub struct GetBalanceRequest {
459    pub address: Address,
460    pub network: NetworkInRequest,
461    pub min_confirmations: Option<u32>,
462}
463
464#[derive(CandidType, Debug, Deserialize, PartialEq, Eq, Clone)]
465pub enum GetBalanceError {
466    MalformedAddress,
467    MinConfirmationsTooLarge { given: u32, max: u32 },
468}
469
470impl fmt::Display for GetBalanceError {
471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472        match self {
473            Self::MalformedAddress => {
474                write!(f, "Malformed address.")
475            }
476            Self::MinConfirmationsTooLarge { given, max } => {
477                write!(
478                    f,
479                    "The requested min_confirmations is too large. Given: {}, max supported: {}",
480                    given, max
481                )
482            }
483        }
484    }
485}
486
487#[derive(CandidType, Debug, Deserialize, PartialEq, Eq)]
488pub struct SendTransactionRequest {
489    #[serde(with = "serde_bytes")]
490    pub transaction: Vec<u8>,
491    pub network: NetworkInRequest,
492}
493
494#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq)]
495pub enum SendTransactionError {
496    /// Can't deserialize transaction.
497    MalformedTransaction,
498    /// Enqueueing a request failed due to full queue to the Bitcoin adapter.
499    QueueFull,
500}
501
502impl fmt::Display for SendTransactionError {
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        match self {
505            Self::MalformedTransaction => {
506                write!(f, "Can't deserialize transaction because it's malformed.")
507            }
508            Self::QueueFull => {
509                write!(
510                    f,
511                    "Request can not be enqueued because the queue has reached its capacity. Please retry later."
512                )
513            }
514        }
515    }
516}
517
518/// A request to update the canister's config.
519#[derive(CandidType, Deserialize, Default, Serialize)]
520pub struct SetConfigRequest {
521    pub stability_threshold: Option<u128>,
522
523    /// Whether or not to enable/disable syncing of blocks from the network.
524    pub syncing: Option<Flag>,
525
526    /// The fees to charge for the various endpoints.
527    pub fees: Option<Fees>,
528
529    /// Whether or not to enable/disable the bitcoin apis.
530    pub api_access: Option<Flag>,
531
532    /// Whether or not to enable/disable the bitcoin apis if not fully synced.
533    pub disable_api_if_not_fully_synced: Option<Flag>,
534
535    /// The principal of the watchdog canister.
536    /// The watchdog canister has the authority to disable the Bitcoin canister's API
537    /// if it suspects that there is a problem.
538    pub watchdog_canister: Option<Option<Principal>>,
539
540    /// If enabled, fee percentiles are only computed when requested.
541    /// Otherwise, they are computed whenever we receive a new block.
542    pub lazily_evaluate_fee_percentiles: Option<Flag>,
543
544    /// If enabled, continuously burns all cycles in the canister's balance
545    /// to count towards the IC's burn rate.
546    pub burn_cycles: Option<Flag>,
547}
548
549#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Copy, Clone, Debug, Default)]
550pub enum Flag {
551    #[serde(rename = "enabled")]
552    #[default]
553    Enabled,
554    #[serde(rename = "disabled")]
555    Disabled,
556}
557
558/// The config used to initialize the canister.
559///
560/// This struct is equivalent to `Config`, except that all its fields are optional.
561/// Fields that are not specified here are loaded with their default value. See
562/// `Config::default()`.
563#[derive(CandidType, Deserialize, Debug, Default)]
564pub struct InitConfig {
565    pub stability_threshold: Option<u128>,
566    pub network: Option<Network>,
567    pub blocks_source: Option<Principal>,
568    pub syncing: Option<Flag>,
569    pub fees: Option<Fees>,
570    pub api_access: Option<Flag>,
571    pub disable_api_if_not_fully_synced: Option<Flag>,
572    pub watchdog_canister: Option<Option<Principal>>,
573    pub burn_cycles: Option<Flag>,
574    pub lazily_evaluate_fee_percentiles: Option<Flag>,
575}
576
577/// The config of the canister.
578#[derive(CandidType, Deserialize, Debug)]
579pub struct Config {
580    pub stability_threshold: u128,
581    pub network: Network,
582
583    /// The principal from which blocks are retrieved.
584    ///
585    /// Setting this source to the management canister means that the blocks will be
586    /// fetched directly from the replica, and that's what is used in production.
587    pub blocks_source: Principal,
588
589    pub syncing: Flag,
590
591    pub fees: Fees,
592
593    /// Flag to control access to the apis provided by the canister.
594    pub api_access: Flag,
595
596    /// Flag to determine if the API should be automatically disabled if
597    /// the canister isn't fully synced.
598    pub disable_api_if_not_fully_synced: Flag,
599
600    /// The principal of the watchdog canister.
601    /// The watchdog canister has the authority to disable the Bitcoin canister's API
602    /// if it suspects that there is a problem.
603    pub watchdog_canister: Option<Principal>,
604
605    /// If enabled, continuously burns all cycles in its balance
606    /// (to count towards the IC's burn rate).
607    pub burn_cycles: Flag,
608
609    /// If enabled, fee percentiles are only computed when requested.
610    /// Otherwise, they are computed whenever we receive a new block.
611    pub lazily_evaluate_fee_percentiles: Flag,
612}
613
614impl From<InitConfig> for Config {
615    fn from(init_config: InitConfig) -> Self {
616        let mut config = Config::default();
617
618        if let Some(stability_threshold) = init_config.stability_threshold {
619            config.stability_threshold = stability_threshold;
620        }
621
622        if let Some(network) = init_config.network {
623            config.network = network;
624        }
625
626        if let Some(blocks_source) = init_config.blocks_source {
627            config.blocks_source = blocks_source;
628        }
629
630        if let Some(syncing) = init_config.syncing {
631            config.syncing = syncing;
632        }
633
634        let fees_explicitly_set = init_config.fees.is_some();
635        if let Some(fees) = init_config.fees {
636            config.fees = fees;
637        }
638
639        if let Some(api_access) = init_config.api_access {
640            config.api_access = api_access;
641        }
642
643        if let Some(disable_api_if_not_fully_synced) = init_config.disable_api_if_not_fully_synced {
644            config.disable_api_if_not_fully_synced = disable_api_if_not_fully_synced;
645        }
646
647        if let Some(watchdog_canister) = init_config.watchdog_canister {
648            config.watchdog_canister = watchdog_canister;
649        }
650
651        if let Some(burn_cycles) = init_config.burn_cycles {
652            config.burn_cycles = burn_cycles;
653        }
654
655        if let Some(lazily_evaluate_fee_percentiles) = init_config.lazily_evaluate_fee_percentiles {
656            config.lazily_evaluate_fee_percentiles = lazily_evaluate_fee_percentiles;
657        }
658
659        // Config post-processing.
660        if !fees_explicitly_set {
661            config.fees = match config.network {
662                Network::Mainnet => Fees::mainnet(),
663                Network::Testnet => Fees::testnet(),
664                Network::Regtest => config.fees, // Keep unchanged for regtest.
665            };
666        }
667
668        config
669    }
670}
671
672impl Default for Config {
673    fn default() -> Self {
674        Self {
675            stability_threshold: DEFAULT_STABILITY_THRESHOLD,
676            network: Network::Regtest,
677            blocks_source: Principal::management_canister(),
678            syncing: Flag::Enabled,
679            fees: Fees::default(),
680            api_access: Flag::Enabled,
681            disable_api_if_not_fully_synced: Flag::Enabled,
682            watchdog_canister: None,
683            burn_cycles: Flag::Disabled,
684            lazily_evaluate_fee_percentiles: Flag::Disabled,
685        }
686    }
687}
688
689#[derive(CandidType, Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)]
690pub struct Fees {
691    /// The base fee to charge for all `get_utxos` requests.
692    pub get_utxos_base: u128,
693
694    /// The number of cycles to charge per 10 instructions.
695    pub get_utxos_cycles_per_ten_instructions: u128,
696
697    /// The maximum amount of cycles that can be charged in a `get_utxos` request.
698    /// A request must send at least this amount for it to be accepted.
699    pub get_utxos_maximum: u128,
700
701    /// The flat fee to charge for a `get_balance` request.
702    pub get_balance: u128,
703
704    /// The maximum amount of cycles that can be charged in a `get_balance` request.
705    /// A request must send at least this amount for it to be accepted.
706    pub get_balance_maximum: u128,
707
708    /// The flat fee to charge for a `get_current_fee_percentiles` request.
709    pub get_current_fee_percentiles: u128,
710
711    /// The maximum amount of cycles that can be charged in a `get_current_fee_percentiles` request.
712    /// A request must send at least this amount for it to be accepted.
713    pub get_current_fee_percentiles_maximum: u128,
714
715    /// The base fee to charge for all `send_transaction` requests.
716    pub send_transaction_base: u128,
717
718    /// The number of cycles to charge for each byte in the transaction.
719    pub send_transaction_per_byte: u128,
720
721    #[serde(default)]
722    /// The base fee to charge for all `get_block_headers` requests.
723    pub get_block_headers_base: u128,
724
725    #[serde(default)]
726    /// The number of cycles to charge per 10 instructions.
727    pub get_block_headers_cycles_per_ten_instructions: u128,
728
729    #[serde(default)]
730    /// The maximum amount of cycles that can be charged in a `get_block_headers` request.
731    /// A request must send at least this amount for it to be accepted.
732    pub get_block_headers_maximum: u128,
733}
734
735impl Fees {
736    pub fn testnet() -> Self {
737        // https://internetcomputer.org/docs/references/bitcoin-how-it-works#bitcoin-testnet
738        Self {
739            get_utxos_base: 20_000_000,
740            get_utxos_cycles_per_ten_instructions: 4,
741            get_utxos_maximum: 4_000_000_000,
742
743            get_current_fee_percentiles: 4_000_000,
744            get_current_fee_percentiles_maximum: 40_000_000,
745
746            get_balance: 4_000_000,
747            get_balance_maximum: 40_000_000,
748
749            send_transaction_base: 2_000_000_000,
750            send_transaction_per_byte: 8_000_000,
751
752            get_block_headers_base: 20_000_000,
753            get_block_headers_cycles_per_ten_instructions: 4,
754            get_block_headers_maximum: 4_000_000_000,
755        }
756    }
757
758    pub fn mainnet() -> Self {
759        // https://internetcomputer.org/docs/references/bitcoin-how-it-works#bitcoin-mainnet
760        Self {
761            get_utxos_base: 50_000_000,
762            get_utxos_cycles_per_ten_instructions: 10,
763            get_utxos_maximum: 10_000_000_000,
764
765            get_current_fee_percentiles: 10_000_000,
766            get_current_fee_percentiles_maximum: 100_000_000,
767
768            get_balance: 10_000_000,
769            get_balance_maximum: 100_000_000,
770
771            send_transaction_base: 5_000_000_000,
772            send_transaction_per_byte: 20_000_000,
773
774            get_block_headers_base: 50_000_000,
775            get_block_headers_cycles_per_ten_instructions: 10,
776            get_block_headers_maximum: 10_000_000_000,
777        }
778    }
779}
780
781#[cfg(test)]
782mod test {
783    use super::*;
784
785    #[test]
786    fn test_config_debug_formatter_is_enabled() {
787        // Verify that debug formatter for Config is enabled.
788        // This might be important for logging and debugging purposes.
789        assert!(
790            !format!("{:?}", Config::default()).is_empty(),
791            "Config should be printable using debug formatter {{:?}}."
792        );
793    }
794
795    #[test]
796    fn can_extract_bytes_from_txid() {
797        let tx_id = Txid([1; 32]);
798        let tx: [u8; 32] = tx_id.into();
799        assert_eq!(tx, [1; 32]);
800    }
801}