Skip to main content

fedimint_core/
lib.rs

1#![deny(clippy::pedantic, clippy::nursery)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_possible_wrap)]
4#![allow(clippy::cast_precision_loss)]
5#![allow(clippy::cast_sign_loss)]
6#![allow(clippy::cognitive_complexity)]
7#![allow(clippy::doc_markdown)]
8#![allow(clippy::future_not_send)]
9#![allow(clippy::missing_const_for_fn)]
10#![allow(clippy::missing_errors_doc)]
11#![allow(clippy::missing_panics_doc)]
12#![allow(clippy::module_name_repetitions)]
13#![allow(clippy::must_use_candidate)]
14#![allow(clippy::needless_lifetimes)]
15#![allow(clippy::redundant_pub_crate)]
16#![allow(clippy::return_self_not_must_use)]
17#![allow(clippy::similar_names)]
18#![allow(clippy::transmute_ptr_to_ptr)]
19#![allow(clippy::unsafe_derive_deserialize)]
20
21//! Fedimint Core library
22//!
23//! `fedimint-core` contains commonly used types, utilities and primitives,
24//! shared between both client and server code.
25//!
26//! Things that are server-side only typically live in `fedimint-server`, and
27//! client-side only in `fedimint-client`.
28//!
29//! ### Wasm support
30//!
31//! All code in `fedimint-core` needs to compile on Wasm, and `fedimint-core`
32//! includes helpers and wrappers around non-wasm-safe utitlies.
33//!
34//! In particular:
35//!
36//! * [`fedimint_core::task`] for task spawning and control
37//! * [`fedimint_core::time`] for time-related operations
38
39extern crate self as fedimint_core;
40
41#[cfg(feature = "uniffi")]
42uniffi::setup_scaffolding!();
43
44use std::fmt::{self, Debug};
45use std::io::Error;
46use std::ops::{self, Range};
47use std::str::FromStr;
48
49pub use amount::*;
50/// Mostly re-exported for [`Decodable`] macros.
51pub use anyhow;
52use bitcoin::address::NetworkUnchecked;
53pub use bitcoin::hashes::Hash as BitcoinHash;
54use bitcoin::{Address, Network};
55use envs::BitcoinRpcConfig;
56use lightning::util::ser::Writeable;
57use lightning_types::features::Bolt11InvoiceFeatures;
58pub use macro_rules_attribute::apply;
59pub use peer_id::*;
60use serde::{Deserialize, Deserializer, Serialize, Serializer};
61use thiserror::Error;
62pub use tiered::Tiered;
63pub use tiered_multi::*;
64use util::SafeUrl;
65pub use {bitcoin, hex, secp256k1};
66
67use crate::encoding::{Decodable, DecodeError, Encodable};
68use crate::module::registry::ModuleDecoderRegistry;
69
70/// Admin (guardian) client types
71pub mod admin_client;
72/// Bitcoin amount types
73mod amount;
74/// Federation-stored client backups
75pub mod backup;
76/// Legacy serde encoding for `bls12_381`
77pub mod bls12_381_serde;
78/// Federation configuration
79pub mod config;
80/// Fundamental types
81pub mod core;
82/// Database handling
83pub mod db;
84/// Consensus encoding
85pub mod encoding;
86pub mod endpoint_constants;
87/// Common environment variables
88pub mod envs;
89pub mod epoch;
90/// Formatting helpers
91pub mod fmt_utils;
92/// Federation invite code
93pub mod invite_code;
94pub mod log;
95/// Common macros
96#[macro_use]
97pub mod macros;
98/// Base 32 encoding
99pub mod base32;
100/// Extendable module sysystem
101pub mod module;
102/// Peer networking
103pub mod net;
104/// `PeerId` type
105mod peer_id;
106/// Runtime (wasm32 vs native) differences handling
107pub mod runtime;
108/// Rustls support
109pub mod rustls;
110/// Peer setup code for setup ceremony
111pub mod setup_code;
112/// Task handling, including wasm safe logic
113pub mod task;
114/// Types handling per-denomination values
115pub mod tiered;
116/// Types handling multiple per-denomination values
117pub mod tiered_multi;
118/// Time handling, wasm safe functionality
119pub mod time;
120/// Timing helpers
121pub mod timing;
122/// Fedimint transaction (inpus + outputs + signature) types
123pub mod transaction;
124/// Peg-in txo proofs
125pub mod txoproof;
126/// General purpose utilities
127pub mod util;
128/// Version
129pub mod version;
130
131/// Atomic BFT unit containing consensus items
132pub mod session_outcome;
133
134// It's necessary to wrap `hash_newtype!` in a module because the generated code
135// references a module called "core", but we export a conflicting module in this
136// file.
137mod txid {
138    use bitcoin::hashes::hash_newtype;
139    use bitcoin::hashes::sha256::Hash as Sha256;
140
141    hash_newtype!(
142        /// A transaction id for peg-ins, peg-outs and reissuances
143        pub struct TransactionId(Sha256);
144    );
145}
146pub use txid::TransactionId;
147
148#[cfg(feature = "uniffi")]
149uniffi::custom_type!(TransactionId, String, {
150    lower: |txid| txid.to_string(),
151    try_lift: |s| TransactionId::from_str(&s).map_err(Into::into),
152});
153
154pub struct TransactionIdShortFmt<'a>(&'a TransactionId);
155pub struct TransactionIdFullFmt<'a>(&'a TransactionId);
156
157impl TransactionId {
158    pub fn fmt_short(&self) -> TransactionIdShortFmt<'_> {
159        TransactionIdShortFmt(self)
160    }
161
162    pub fn fmt_full(&self) -> TransactionIdFullFmt<'_> {
163        TransactionIdFullFmt(self)
164    }
165}
166
167impl std::fmt::Display for TransactionIdShortFmt<'_> {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        let bytes = &self.0[..];
170        format_hex(&bytes[..4], f)?;
171        f.write_str("_")?;
172        format_hex(&bytes[28..], f)
173    }
174}
175
176impl std::fmt::Display for TransactionIdFullFmt<'_> {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        let bytes = &self.0[..];
179        format_hex(bytes, f)
180    }
181}
182
183/// Bitcoin chain identifier
184///
185/// This is a newtype wrapper around [`bitcoin::BlockHash`] representing the
186/// block hash at height 1, which uniquely identifies a Bitcoin chain (mainnet,
187/// testnet, signet, regtest, or custom networks), unlike genesis block hash
188/// which is often the same for same types of networks (e.g. mutinynet vs
189/// signet4).
190///
191/// Using a distinct type instead of raw `BlockHash` provides type safety and
192/// makes the intent clearer when passing chain identifiers through APIs.
193#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Encodable, Decodable)]
194pub struct ChainId(pub bitcoin::BlockHash);
195
196#[cfg(feature = "uniffi")]
197uniffi::custom_type!(ChainId, String, {
198    lower: |c| c.to_string(),
199    try_lift: |s| ChainId::from_str(&s).map_err(Into::into),
200});
201
202impl ChainId {
203    /// Create a new `ChainId` from a `BlockHash`
204    pub fn new(block_hash: bitcoin::BlockHash) -> Self {
205        Self(block_hash)
206    }
207
208    /// Get the inner `BlockHash`
209    pub fn block_hash(&self) -> bitcoin::BlockHash {
210        self.0
211    }
212}
213
214impl From<bitcoin::BlockHash> for ChainId {
215    fn from(block_hash: bitcoin::BlockHash) -> Self {
216        Self(block_hash)
217    }
218}
219
220impl From<ChainId> for bitcoin::BlockHash {
221    fn from(chain_id: ChainId) -> Self {
222        chain_id.0
223    }
224}
225
226impl std::fmt::Display for ChainId {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        write!(f, "{}", self.0)
229    }
230}
231
232impl FromStr for ChainId {
233    type Err = bitcoin::hashes::hex::HexToArrayError;
234
235    fn from_str(s: &str) -> Result<Self, Self::Err> {
236        bitcoin::BlockHash::from_str(s).map(Self)
237    }
238}
239
240impl Serialize for ChainId {
241    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
242    where
243        S: Serializer,
244    {
245        self.0.serialize(serializer)
246    }
247}
248
249impl<'de> Deserialize<'de> for ChainId {
250    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
251    where
252        D: Deserializer<'de>,
253    {
254        bitcoin::BlockHash::deserialize(deserializer).map(Self)
255    }
256}
257
258/// Amount of bitcoin to send, or `All` to send all available funds
259#[derive(Debug, Eq, PartialEq, Copy, Hash, Clone)]
260pub enum BitcoinAmountOrAll {
261    All,
262    Amount(bitcoin::Amount),
263}
264
265impl std::fmt::Display for BitcoinAmountOrAll {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        match self {
268            Self::All => write!(f, "all"),
269            Self::Amount(amount) => write!(f, "{amount}"),
270        }
271    }
272}
273
274impl FromStr for BitcoinAmountOrAll {
275    type Err = ParseBitcoinAmountOrAllError;
276
277    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
278        if s.eq_ignore_ascii_case("all") {
279            Ok(Self::All)
280        } else {
281            let amount = Amount::from_str(s)?;
282            Ok(Self::Amount(amount.try_into()?))
283        }
284    }
285}
286
287/// Failure to parse a [`BitcoinAmountOrAll`] from its string form.
288#[derive(Debug, Error)]
289#[non_exhaustive]
290pub enum ParseBitcoinAmountOrAllError {
291    /// The string is neither `all` nor a valid amount.
292    #[error("Invalid amount: {0}")]
293    Amount(#[from] ParseAmountError),
294    /// The amount is valid but has sub-satoshi precision.
295    #[error("Amount cannot be expressed in satoshis: {0}")]
296    Precision(#[from] AmountConversionError),
297}
298
299// Custom serde to handle both "all" and numbers/strings
300impl<'de> Deserialize<'de> for BitcoinAmountOrAll {
301    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
302    where
303        D: Deserializer<'de>,
304    {
305        use serde::de::Error;
306
307        struct Visitor;
308
309        impl serde::de::Visitor<'_> for Visitor {
310            type Value = BitcoinAmountOrAll;
311
312            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
313                write!(f, "a bitcoin amount as number or 'all'")
314            }
315
316            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
317            where
318                E: Error,
319            {
320                if v.eq_ignore_ascii_case("all") {
321                    Ok(BitcoinAmountOrAll::All)
322                } else {
323                    let sat: u64 = v.parse().map_err(E::custom)?;
324                    Ok(BitcoinAmountOrAll::Amount(bitcoin::Amount::from_sat(sat)))
325                }
326            }
327
328            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
329            where
330                E: Error,
331            {
332                Ok(BitcoinAmountOrAll::Amount(bitcoin::Amount::from_sat(v)))
333            }
334
335            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
336            where
337                E: Error,
338            {
339                if v < 0 {
340                    return Err(E::custom("amount cannot be negative"));
341                }
342                Ok(BitcoinAmountOrAll::Amount(bitcoin::Amount::from_sat(
343                    v as u64,
344                )))
345            }
346        }
347
348        deserializer.deserialize_any(Visitor)
349    }
350}
351
352impl Serialize for BitcoinAmountOrAll {
353    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
354    where
355        S: Serializer,
356    {
357        match self {
358            Self::All => serializer.serialize_str("all"),
359            Self::Amount(a) => serializer.serialize_u64(a.to_sat()),
360        }
361    }
362}
363
364/// `InPoint` represents a globally unique input in a transaction
365///
366/// Hence, a transaction ID and the input index is required.
367#[derive(
368    Debug,
369    Clone,
370    Copy,
371    Eq,
372    PartialEq,
373    PartialOrd,
374    Ord,
375    Hash,
376    Deserialize,
377    Serialize,
378    Encodable,
379    Decodable,
380)]
381pub struct InPoint {
382    /// The referenced transaction ID
383    pub txid: TransactionId,
384    /// As a transaction may have multiple inputs, this refers to the index of
385    /// the input in a transaction
386    pub in_idx: u64,
387}
388
389impl std::fmt::Display for InPoint {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        write!(f, "{}:{}", self.txid, self.in_idx)
392    }
393}
394
395/// `OutPoint` represents a globally unique output in a transaction
396///
397/// Hence, a transaction ID and the output index is required.
398#[derive(
399    Debug,
400    Clone,
401    Copy,
402    Eq,
403    PartialEq,
404    PartialOrd,
405    Ord,
406    Hash,
407    Deserialize,
408    Serialize,
409    Encodable,
410    Decodable,
411)]
412#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
413pub struct OutPoint {
414    /// The referenced transaction ID
415    pub txid: TransactionId,
416    /// As a transaction may have multiple outputs, this refers to the index of
417    /// the output in a transaction
418    pub out_idx: u64,
419}
420
421impl std::fmt::Display for OutPoint {
422    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423        write!(f, "{}:{}", self.txid, self.out_idx)
424    }
425}
426
427/// A contiguous range of input/output indexes
428#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Encodable, Decodable)]
429pub struct IdxRange {
430    start: u64,
431    end: u64,
432}
433
434impl IdxRange {
435    pub fn new_single(start: u64) -> Option<Self> {
436        start.checked_add(1).map(|end| Self { start, end })
437    }
438
439    pub fn start(self) -> u64 {
440        self.start
441    }
442
443    pub fn count(self) -> usize {
444        self.into_iter().count()
445    }
446
447    /// Number of indexes in the range, or `None` if the range is descending or
448    /// does not fit into a `usize`.
449    ///
450    /// Ranges are deserialized verbatim from untrusted API requests, so a
451    /// caller validating one has to go through this rather than
452    /// [`Self::count`], which reports a descending range as empty and panics on
453    /// `usize` overflow.
454    pub fn checked_count(self) -> Option<usize> {
455        usize::try_from(self.end.checked_sub(self.start)?).ok()
456    }
457
458    pub fn from_inclusive(range: ops::RangeInclusive<u64>) -> Option<Self> {
459        range.end().checked_add(1).map(|end| Self {
460            start: *range.start(),
461            end,
462        })
463    }
464}
465
466impl From<Range<u64>> for IdxRange {
467    fn from(Range { start, end }: Range<u64>) -> Self {
468        Self { start, end }
469    }
470}
471
472impl IntoIterator for IdxRange {
473    type Item = u64;
474    type IntoIter = ops::Range<u64>;
475
476    fn into_iter(self) -> Self::IntoIter {
477        ops::Range {
478            start: self.start,
479            end: self.end,
480        }
481    }
482}
483
484/// Represents a range of output indices for a single transaction
485#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Encodable, Decodable)]
486pub struct OutPointRange {
487    pub txid: TransactionId,
488    idx_range: IdxRange,
489}
490
491impl OutPointRange {
492    pub fn new(txid: TransactionId, idx_range: IdxRange) -> Self {
493        Self { txid, idx_range }
494    }
495
496    pub fn new_single(txid: TransactionId, idx: u64) -> Option<Self> {
497        IdxRange::new_single(idx).map(|idx_range| Self { txid, idx_range })
498    }
499
500    pub fn start_idx(self) -> u64 {
501        self.idx_range.start()
502    }
503
504    pub fn out_idx_iter(self) -> impl Iterator<Item = u64> {
505        self.idx_range.into_iter()
506    }
507
508    pub fn count(self) -> usize {
509        self.idx_range.count()
510    }
511
512    /// See [`IdxRange::checked_count`].
513    pub fn checked_count(self) -> Option<usize> {
514        self.idx_range.checked_count()
515    }
516
517    pub fn start_out_point(self) -> OutPoint {
518        OutPoint {
519            txid: self.txid,
520            out_idx: self.idx_range.start(),
521        }
522    }
523
524    pub fn end_out_point(self) -> OutPoint {
525        OutPoint {
526            txid: self.txid,
527            out_idx: self.idx_range.end,
528        }
529    }
530
531    pub fn txid(&self) -> TransactionId {
532        self.txid
533    }
534}
535
536impl IntoIterator for OutPointRange {
537    type Item = OutPoint;
538    type IntoIter = OutPointRangeIter;
539
540    fn into_iter(self) -> Self::IntoIter {
541        OutPointRangeIter {
542            txid: self.txid,
543            inner: self.idx_range.into_iter(),
544        }
545    }
546}
547
548pub struct OutPointRangeIter {
549    txid: TransactionId,
550    inner: ops::Range<u64>,
551}
552
553impl Iterator for OutPointRangeIter {
554    type Item = OutPoint;
555
556    fn next(&mut self) -> Option<Self::Item> {
557        self.inner.next().map(|idx| OutPoint {
558            txid: self.txid,
559            out_idx: idx,
560        })
561    }
562}
563
564impl Encodable for TransactionId {
565    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), Error> {
566        let bytes = &self[..];
567        writer.write_all(bytes)?;
568        Ok(())
569    }
570}
571
572impl Decodable for TransactionId {
573    fn consensus_decode_partial<D: std::io::Read>(
574        d: &mut D,
575        _modules: &ModuleDecoderRegistry,
576    ) -> Result<Self, DecodeError> {
577        let mut bytes = [0u8; 32];
578        d.read_exact(&mut bytes).map_err(DecodeError::from_err)?;
579        Ok(Self::from_byte_array(bytes))
580    }
581}
582
583#[derive(
584    Copy,
585    Clone,
586    Debug,
587    PartialEq,
588    Ord,
589    PartialOrd,
590    Eq,
591    Hash,
592    Serialize,
593    Deserialize,
594    Encodable,
595    Decodable,
596)]
597pub struct Feerate {
598    pub sats_per_kvb: u64,
599}
600
601impl fmt::Display for Feerate {
602    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
603        f.write_fmt(format_args!("{}sat/kvb", self.sats_per_kvb))
604    }
605}
606
607impl Feerate {
608    pub fn calculate_fee(&self, weight: u64) -> bitcoin::Amount {
609        let sats = weight_to_vbytes(weight) * self.sats_per_kvb / 1000;
610        bitcoin::Amount::from_sat(sats)
611    }
612
613    /// Fee for a transaction of the given weight, reproducing the wrapping the
614    /// unchecked multiplication used to do in release profiles.
615    ///
616    /// Consensus behaviour must not depend on the build profile, and
617    /// [`Self::calculate_fee`]'s `*` panics with overflow checks on and wraps
618    /// without them. Sessions ordered before the fee computation was checked
619    /// have to replay as release binaries ran them, so the wrap is explicit.
620    pub fn wrapping_calculate_fee(&self, weight: u64) -> bitcoin::Amount {
621        let sats = weight_to_vbytes(weight).wrapping_mul(self.sats_per_kvb) / 1000;
622        bitcoin::Amount::from_sat(sats)
623    }
624
625    /// Fee for a transaction of the given weight, or `None` if the rate and
626    /// weight do not describe a fee that can exist on chain.
627    ///
628    /// [`Self::calculate_fee`] multiplies unchecked, which wraps to an
629    /// arbitrarily small fee in release profiles. Callers that decide whether
630    /// to accept a transaction must use this instead.
631    pub fn checked_calculate_fee(&self, weight: u64) -> Option<bitcoin::Amount> {
632        let sats = weight_to_vbytes(weight).checked_mul(self.sats_per_kvb)? / 1000;
633        (sats <= bitcoin::Amount::MAX_MONEY.to_sat()).then(|| bitcoin::Amount::from_sat(sats))
634    }
635}
636
637const WITNESS_SCALE_FACTOR: u64 = bitcoin::constants::WITNESS_SCALE_FACTOR as u64;
638
639/// Converts weight to virtual bytes, defined in [BIP-141] as weight / 4
640/// (rounded up to the next integer).
641///
642/// [BIP-141]: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
643pub fn weight_to_vbytes(weight: u64) -> u64 {
644    weight.div_ceil(WITNESS_SCALE_FACTOR)
645}
646
647#[derive(Debug, Error)]
648pub enum CoreError {
649    #[error("Mismatching outcome variant: expected {0}, got {1}")]
650    MismatchingVariant(&'static str, &'static str),
651}
652
653// Encode features for a bolt11 invoice without encoding the length.
654// This functionality was available in `lightning` v0.0.123, but has since been
655// removed. See the original code here:
656// https://docs.rs/lightning/0.0.123/src/lightning/ln/features.rs.html#745-750
657// https://docs.rs/lightning/0.0.123/src/lightning/ln/features.rs.html#1008-1012
658pub fn encode_bolt11_invoice_features_without_length(features: &Bolt11InvoiceFeatures) -> Vec<u8> {
659    let mut feature_bytes = vec![];
660    for f in features.le_flags().iter().rev() {
661        f.write(&mut feature_bytes)
662            .expect("Writing to byte vec can't fail");
663    }
664    feature_bytes
665}
666
667/// Outputs hex into an object implementing `fmt::Write`.
668///
669/// Vendored from `bitcoin_hashes` v0.11.0:
670/// <https://docs.rs/bitcoin_hashes/0.11.0/src/bitcoin_hashes/hex.rs.html#173-189>
671pub fn format_hex(data: &[u8], f: &mut std::fmt::Formatter) -> std::fmt::Result {
672    let prec = f.precision().unwrap_or(2 * data.len());
673    let width = f.width().unwrap_or(2 * data.len());
674    for _ in (2 * data.len())..width {
675        f.write_str("0")?;
676    }
677    for ch in data.iter().take(prec / 2) {
678        write!(f, "{:02x}", *ch)?;
679    }
680    if prec < 2 * data.len() && prec % 2 == 1 {
681        write!(f, "{:x}", data[prec / 2] / 16)?;
682    }
683    Ok(())
684}
685
686/// Gets the (approximate) network from a bitcoin address.
687///
688/// This function mimics how `Address.network` is calculated in bitcoin v0.30.
689/// However, that field was removed in more recent versions in part because it
690/// can only distinguish between `Bitcoin`, `Testnet` and `Regtest`.
691///
692/// As of bitcoin v0.32.4, `Address::is_valid_for_network()` performs equality
693/// checks using `NetworkKind` and `KnownHrp`, which only distinguish between
694/// `Bitcoin`, `Testnet` and `Regtest`.
695/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/address/mod.rs.html#709-716>
696/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/network.rs.html#51-58>
697/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/address/mod.rs.html#200-209>
698pub fn get_network_for_address(address: &Address<NetworkUnchecked>) -> Network {
699    if address.is_valid_for_network(Network::Bitcoin) {
700        Network::Bitcoin
701    } else if address.is_valid_for_network(Network::Testnet) {
702        Network::Testnet
703    } else if address.is_valid_for_network(Network::Regtest) {
704        Network::Regtest
705    } else {
706        panic!("Address is not valid for any network");
707    }
708}
709
710/// Returns the default esplora server according to the network
711pub fn default_esplora_server(network: Network, port: Option<String>) -> BitcoinRpcConfig {
712    BitcoinRpcConfig {
713        kind: "esplora".to_string(),
714        url: match network {
715            Network::Bitcoin => SafeUrl::parse("https://mempool.space/api/"),
716            Network::Testnet => SafeUrl::parse("https://mempool.space/testnet/api/"),
717            Network::Testnet4 => SafeUrl::parse("https://mempool.space/testnet4/api/"),
718            Network::Signet => SafeUrl::parse("https://mutinynet.com/api/"),
719            Network::Regtest => SafeUrl::parse(&format!(
720                "http://127.0.0.1:{}/",
721                port.unwrap_or_else(|| String::from("50002"))
722            )),
723        }
724        .expect("Failed to parse default esplora server"),
725    }
726}
727
728#[cfg(test)]
729mod tests;