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