Skip to main content

ic_doge_interface/
lib.rs

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