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
41use std::fmt::{self, Debug};
42use std::io::Error;
43use std::str::FromStr;
44
45pub use amount::*;
46/// Mostly re-exported for [`Decodable`] macros.
47pub use anyhow;
48use bitcoin::address::NetworkUnchecked;
49pub use bitcoin::hashes::Hash as BitcoinHash;
50use bitcoin::{Address, Network};
51use envs::BitcoinRpcConfig;
52use lightning::util::ser::Writeable;
53use lightning_types::features::Bolt11InvoiceFeatures;
54pub use macro_rules_attribute::apply;
55pub use peer_id::*;
56use serde::{Deserialize, Serialize};
57use thiserror::Error;
58pub use tiered::Tiered;
59pub use tiered_multi::*;
60use util::SafeUrl;
61pub use {bitcoin, hex, secp256k1};
62
63use crate::encoding::{Decodable, DecodeError, Encodable};
64use crate::module::registry::ModuleDecoderRegistry;
65
66/// Admin (guardian) client types
67pub mod admin_client;
68/// Bitcoin amount types
69mod amount;
70/// Federation-stored client backups
71pub mod backup;
72/// Legacy serde encoding for `bls12_381`
73pub mod bls12_381_serde;
74/// Federation configuration
75pub mod config;
76/// Fundamental types
77pub mod core;
78/// Database handling
79pub mod db;
80/// Consensus encoding
81pub mod encoding;
82pub mod endpoint_constants;
83/// Common environment variables
84pub mod envs;
85pub mod epoch;
86/// Formatting helpers
87pub mod fmt_utils;
88/// Federation invite code
89pub mod invite_code;
90pub mod iroh_prod;
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/// Amount of bitcoin to send, or `All` to send all available funds
146#[derive(Debug, Eq, PartialEq, Copy, Hash, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum BitcoinAmountOrAll {
149    All,
150    #[serde(untagged)]
151    Amount(#[serde(with = "bitcoin::amount::serde::as_sat")] bitcoin::Amount),
152}
153
154impl std::fmt::Display for BitcoinAmountOrAll {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            Self::All => write!(f, "all"),
158            Self::Amount(amount) => write!(f, "{amount}"),
159        }
160    }
161}
162
163impl FromStr for BitcoinAmountOrAll {
164    type Err = anyhow::Error;
165
166    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
167        if s == "all" {
168            Ok(Self::All)
169        } else {
170            let amount = Amount::from_str(s)?;
171            Ok(Self::Amount(amount.try_into()?))
172        }
173    }
174}
175
176/// `InPoint` represents a globally unique input in a transaction
177///
178/// Hence, a transaction ID and the input index is required.
179#[derive(
180    Debug,
181    Clone,
182    Copy,
183    Eq,
184    PartialEq,
185    PartialOrd,
186    Ord,
187    Hash,
188    Deserialize,
189    Serialize,
190    Encodable,
191    Decodable,
192)]
193pub struct InPoint {
194    /// The referenced transaction ID
195    pub txid: TransactionId,
196    /// As a transaction may have multiple inputs, this refers to the index of
197    /// the input in a transaction
198    pub in_idx: u64,
199}
200
201impl std::fmt::Display for InPoint {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        write!(f, "{}:{}", self.txid, self.in_idx)
204    }
205}
206
207/// `OutPoint` represents a globally unique output in a transaction
208///
209/// Hence, a transaction ID and the output index is required.
210#[derive(
211    Debug,
212    Clone,
213    Copy,
214    Eq,
215    PartialEq,
216    PartialOrd,
217    Ord,
218    Hash,
219    Deserialize,
220    Serialize,
221    Encodable,
222    Decodable,
223)]
224pub struct OutPoint {
225    /// The referenced transaction ID
226    pub txid: TransactionId,
227    /// As a transaction may have multiple outputs, this refers to the index of
228    /// the output in a transaction
229    pub out_idx: u64,
230}
231
232impl std::fmt::Display for OutPoint {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        write!(f, "{}:{}", self.txid, self.out_idx)
235    }
236}
237
238impl Encodable for TransactionId {
239    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), Error> {
240        let bytes = &self[..];
241        writer.write_all(bytes)?;
242        Ok(())
243    }
244}
245
246impl Decodable for TransactionId {
247    fn consensus_decode_partial<D: std::io::Read>(
248        d: &mut D,
249        _modules: &ModuleDecoderRegistry,
250    ) -> Result<Self, DecodeError> {
251        let mut bytes = [0u8; 32];
252        d.read_exact(&mut bytes).map_err(DecodeError::from_err)?;
253        Ok(Self::from_byte_array(bytes))
254    }
255}
256
257#[derive(
258    Copy,
259    Clone,
260    Debug,
261    PartialEq,
262    Ord,
263    PartialOrd,
264    Eq,
265    Hash,
266    Serialize,
267    Deserialize,
268    Encodable,
269    Decodable,
270)]
271pub struct Feerate {
272    pub sats_per_kvb: u64,
273}
274
275impl fmt::Display for Feerate {
276    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277        f.write_fmt(format_args!("{}sat/kvb", self.sats_per_kvb))
278    }
279}
280
281impl Feerate {
282    pub fn calculate_fee(&self, weight: u64) -> bitcoin::Amount {
283        let sats = weight_to_vbytes(weight) * self.sats_per_kvb / 1000;
284        bitcoin::Amount::from_sat(sats)
285    }
286}
287
288const WITNESS_SCALE_FACTOR: u64 = bitcoin::constants::WITNESS_SCALE_FACTOR as u64;
289
290/// Converts weight to virtual bytes, defined in [BIP-141] as weight / 4
291/// (rounded up to the next integer).
292///
293/// [BIP-141]: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
294pub fn weight_to_vbytes(weight: u64) -> u64 {
295    weight.div_ceil(WITNESS_SCALE_FACTOR)
296}
297
298#[derive(Debug, Error)]
299pub enum CoreError {
300    #[error("Mismatching outcome variant: expected {0}, got {1}")]
301    MismatchingVariant(&'static str, &'static str),
302}
303
304// Encode features for a bolt11 invoice without encoding the length.
305// This functionality was available in `lightning` v0.0.123, but has since been
306// removed. See the original code here:
307// https://docs.rs/lightning/0.0.123/src/lightning/ln/features.rs.html#745-750
308// https://docs.rs/lightning/0.0.123/src/lightning/ln/features.rs.html#1008-1012
309pub fn encode_bolt11_invoice_features_without_length(features: &Bolt11InvoiceFeatures) -> Vec<u8> {
310    let mut feature_bytes = vec![];
311    for f in features.le_flags().iter().rev() {
312        f.write(&mut feature_bytes)
313            .expect("Writing to byte vec can't fail");
314    }
315    feature_bytes
316}
317
318/// Outputs hex into an object implementing `fmt::Write`.
319///
320/// Vendored from `bitcoin_hashes` v0.11.0:
321/// <https://docs.rs/bitcoin_hashes/0.11.0/src/bitcoin_hashes/hex.rs.html#173-189>
322pub fn format_hex(data: &[u8], f: &mut std::fmt::Formatter) -> std::fmt::Result {
323    let prec = f.precision().unwrap_or(2 * data.len());
324    let width = f.width().unwrap_or(2 * data.len());
325    for _ in (2 * data.len())..width {
326        f.write_str("0")?;
327    }
328    for ch in data.iter().take(prec / 2) {
329        write!(f, "{:02x}", *ch)?;
330    }
331    if prec < 2 * data.len() && prec % 2 == 1 {
332        write!(f, "{:x}", data[prec / 2] / 16)?;
333    }
334    Ok(())
335}
336
337/// Gets the (approximate) network from a bitcoin address.
338///
339/// This function mimics how `Address.network` is calculated in bitcoin v0.30.
340/// However, that field was removed in more recent versions in part because it
341/// can only distinguish between `Bitcoin`, `Testnet` and `Regtest`.
342///
343/// As of bitcoin v0.32.4, `Address::is_valid_for_network()` performs equality
344/// checks using `NetworkKind` and `KnownHrp`, which only distinguish between
345/// `Bitcoin`, `Testnet` and `Regtest`.
346/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/address/mod.rs.html#709-716>
347/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/network.rs.html#51-58>
348/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/address/mod.rs.html#200-209>
349pub fn get_network_for_address(address: &Address<NetworkUnchecked>) -> Network {
350    if address.is_valid_for_network(Network::Bitcoin) {
351        Network::Bitcoin
352    } else if address.is_valid_for_network(Network::Testnet) {
353        Network::Testnet
354    } else if address.is_valid_for_network(Network::Regtest) {
355        Network::Regtest
356    } else {
357        panic!("Address is not valid for any network");
358    }
359}
360
361/// Returns the default esplora server according to the network
362pub fn default_esplora_server(network: Network, port: Option<String>) -> BitcoinRpcConfig {
363    BitcoinRpcConfig {
364        kind: "esplora".to_string(),
365        url: match network {
366            Network::Bitcoin => SafeUrl::parse("https://mempool.space/api/"),
367            Network::Testnet => SafeUrl::parse("https://mempool.space/testnet/api/"),
368            Network::Testnet4 => SafeUrl::parse("https://mempool.space/testnet4/api/"),
369            Network::Signet => SafeUrl::parse("https://mutinynet.com/api/"),
370            Network::Regtest => SafeUrl::parse(&format!(
371                "http://127.0.0.1:{}/",
372                port.unwrap_or_else(|| String::from("50002"))
373            )),
374            _ => panic!("Failed to parse default esplora server"),
375        }
376        .expect("Failed to parse default esplora server"),
377    }
378}
379
380#[cfg(test)]
381mod tests;