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