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