ic_btc_interface/
lib.rs

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