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::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 lightning::util::ser::Writeable;
52use lightning_types::features::Bolt11InvoiceFeatures;
53pub use macro_rules_attribute::apply;
54pub use peer_id::*;
55use serde::{Deserialize, Serialize};
56use thiserror::Error;
57pub use tiered::Tiered;
58pub use tiered_multi::*;
59pub use {bitcoin, hex, secp256k1};
60
61use crate::encoding::{Decodable, DecodeError, Encodable};
62use crate::module::registry::ModuleDecoderRegistry;
63
64/// Admin (guardian) client types
65pub mod admin_client;
66/// Bitcoin amount types
67mod amount;
68/// Federation-stored client backups
69pub mod backup;
70/// Legacy serde encoding for `bls12_381`
71pub mod bls12_381_serde;
72/// Federation configuration
73pub mod config;
74/// Fundamental types
75pub mod core;
76/// Database handling
77pub mod db;
78/// Consensus encoding
79pub mod encoding;
80pub mod endpoint_constants;
81/// Common environment variables
82pub mod envs;
83pub mod epoch;
84/// Formatting helpers
85pub mod fmt_utils;
86/// Federation invite code
87pub mod invite_code;
88/// Common macros
89#[macro_use]
90pub mod macros;
91/// Extendable module sysystem
92pub mod module;
93/// Peer networking
94pub mod net;
95/// `PeerId` type
96mod peer_id;
97/// Runtime (wasm32 vs native) differences handling
98pub mod runtime;
99/// Task handling, including wasm safe logic
100pub mod task;
101/// Types handling per-denomination values
102pub mod tiered;
103/// Types handling multiple per-denomination values
104pub mod tiered_multi;
105/// Time handling, wasm safe functionality
106pub mod time;
107/// Timing helpers
108pub mod timing;
109/// Fedimint transaction (inpus + outputs + signature) types
110pub mod transaction;
111/// Peg-in txo proofs
112pub mod txoproof;
113/// General purpose utilities
114pub mod util;
115/// Version
116pub mod version;
117
118/// Atomic BFT unit containing consensus items
119pub mod session_outcome;
120
121// It's necessary to wrap `hash_newtype!` in a module because the generated code
122// references a module called "core", but we export a conflicting module in this
123// file.
124mod txid {
125    use bitcoin::hashes::hash_newtype;
126    use bitcoin::hashes::sha256::Hash as Sha256;
127
128    hash_newtype!(
129        /// A transaction id for peg-ins, peg-outs and reissuances
130        pub struct TransactionId(Sha256);
131    );
132}
133pub use txid::TransactionId;
134
135/// Amount of bitcoin to send, or `All` to send all available funds
136#[derive(Debug, Eq, PartialEq, Copy, Hash, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum BitcoinAmountOrAll {
139    All,
140    #[serde(untagged)]
141    Amount(#[serde(with = "bitcoin::amount::serde::as_sat")] bitcoin::Amount),
142}
143
144impl std::fmt::Display for BitcoinAmountOrAll {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        match self {
147            Self::All => write!(f, "all"),
148            Self::Amount(amount) => write!(f, "{amount}"),
149        }
150    }
151}
152
153impl FromStr for BitcoinAmountOrAll {
154    type Err = anyhow::Error;
155
156    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
157        if s == "all" {
158            Ok(Self::All)
159        } else {
160            let amount = Amount::from_str(s)?;
161            Ok(Self::Amount(amount.try_into()?))
162        }
163    }
164}
165
166/// `InPoint` represents a globally unique input in a transaction
167///
168/// Hence, a transaction ID and the input index is required.
169#[derive(
170    Debug,
171    Clone,
172    Copy,
173    Eq,
174    PartialEq,
175    PartialOrd,
176    Ord,
177    Hash,
178    Deserialize,
179    Serialize,
180    Encodable,
181    Decodable,
182)]
183pub struct InPoint {
184    /// The referenced transaction ID
185    pub txid: TransactionId,
186    /// As a transaction may have multiple inputs, this refers to the index of
187    /// the input in a transaction
188    pub in_idx: u64,
189}
190
191impl std::fmt::Display for InPoint {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        write!(f, "{}:{}", self.txid, self.in_idx)
194    }
195}
196
197/// `OutPoint` represents a globally unique output in a transaction
198///
199/// Hence, a transaction ID and the output index is required.
200#[derive(
201    Debug,
202    Clone,
203    Copy,
204    Eq,
205    PartialEq,
206    PartialOrd,
207    Ord,
208    Hash,
209    Deserialize,
210    Serialize,
211    Encodable,
212    Decodable,
213)]
214pub struct OutPoint {
215    /// The referenced transaction ID
216    pub txid: TransactionId,
217    /// As a transaction may have multiple outputs, this refers to the index of
218    /// the output in a transaction
219    pub out_idx: u64,
220}
221
222impl std::fmt::Display for OutPoint {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        write!(f, "{}:{}", self.txid, self.out_idx)
225    }
226}
227
228impl Encodable for TransactionId {
229    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<usize, Error> {
230        let bytes = &self[..];
231        writer.write_all(bytes)?;
232        Ok(bytes.len())
233    }
234}
235
236impl Decodable for TransactionId {
237    fn consensus_decode_partial<D: std::io::Read>(
238        d: &mut D,
239        _modules: &ModuleDecoderRegistry,
240    ) -> Result<Self, DecodeError> {
241        let mut bytes = [0u8; 32];
242        d.read_exact(&mut bytes).map_err(DecodeError::from_err)?;
243        Ok(Self::from_byte_array(bytes))
244    }
245}
246
247#[derive(
248    Copy,
249    Clone,
250    Debug,
251    PartialEq,
252    Ord,
253    PartialOrd,
254    Eq,
255    Hash,
256    Serialize,
257    Deserialize,
258    Encodable,
259    Decodable,
260)]
261pub struct Feerate {
262    pub sats_per_kvb: u64,
263}
264
265impl Feerate {
266    pub fn calculate_fee(&self, weight: u64) -> bitcoin::Amount {
267        let sats = weight_to_vbytes(weight) * self.sats_per_kvb / 1000;
268        bitcoin::Amount::from_sat(sats)
269    }
270}
271
272const WITNESS_SCALE_FACTOR: u64 = bitcoin::constants::WITNESS_SCALE_FACTOR as u64;
273
274/// Converts weight to virtual bytes, defined in [BIP-141] as weight / 4
275/// (rounded up to the next integer).
276///
277/// [BIP-141]: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#transaction-size-calculations
278pub fn weight_to_vbytes(weight: u64) -> u64 {
279    weight.div_ceil(WITNESS_SCALE_FACTOR)
280}
281
282#[derive(Debug, Error)]
283pub enum CoreError {
284    #[error("Mismatching outcome variant: expected {0}, got {1}")]
285    MismatchingVariant(&'static str, &'static str),
286}
287
288// Encode features for a bolt11 invoice without encoding the length.
289// This functionality was available in `lightning` v0.0.123, but has since been
290// removed. See the original code here:
291// https://docs.rs/lightning/0.0.123/src/lightning/ln/features.rs.html#745-750
292// https://docs.rs/lightning/0.0.123/src/lightning/ln/features.rs.html#1008-1012
293pub fn encode_bolt11_invoice_features_without_length(features: &Bolt11InvoiceFeatures) -> Vec<u8> {
294    let mut feature_bytes = vec![];
295    for f in features.le_flags().iter().rev() {
296        f.write(&mut feature_bytes)
297            .expect("Writing to byte vec can't fail");
298    }
299    feature_bytes
300}
301
302/// Outputs hex into an object implementing `fmt::Write`.
303///
304/// Vendored from `bitcoin_hashes` v0.11.0:
305/// <https://docs.rs/bitcoin_hashes/0.11.0/src/bitcoin_hashes/hex.rs.html#173-189>
306pub fn format_hex(data: &[u8], f: &mut std::fmt::Formatter) -> std::fmt::Result {
307    let prec = f.precision().unwrap_or(2 * data.len());
308    let width = f.width().unwrap_or(2 * data.len());
309    for _ in (2 * data.len())..width {
310        f.write_str("0")?;
311    }
312    for ch in data.iter().take(prec / 2) {
313        write!(f, "{:02x}", *ch)?;
314    }
315    if prec < 2 * data.len() && prec % 2 == 1 {
316        write!(f, "{:x}", data[prec / 2] / 16)?;
317    }
318    Ok(())
319}
320
321/// Gets the (approximate) network from a bitcoin address.
322///
323/// This function mimics how `Address.network` is calculated in bitcoin v0.30.
324/// However, that field was removed in more recent versions in part because it
325/// can only distinguish between `Bitcoin`, `Testnet` and `Regtest`.
326///
327/// As of bitcoin v0.32.4, `Address::is_valid_for_network()` performs equality
328/// checks using `NetworkKind` and `KnownHrp`, which only distinguish between
329/// `Bitcoin`, `Testnet` and `Regtest`.
330/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/address/mod.rs.html#709-716>
331/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/network.rs.html#51-58>
332/// <https://docs.rs/bitcoin/0.32.4/src/bitcoin/address/mod.rs.html#200-209>
333pub fn get_network_for_address(address: &Address<NetworkUnchecked>) -> Network {
334    if address.is_valid_for_network(Network::Bitcoin) {
335        Network::Bitcoin
336    } else if address.is_valid_for_network(Network::Testnet) {
337        Network::Testnet
338    } else if address.is_valid_for_network(Network::Regtest) {
339        Network::Regtest
340    } else {
341        panic!("Address is not valid for any network");
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn converts_weight_to_vbytes() {
351        assert_eq!(1, weight_to_vbytes(4));
352        assert_eq!(2, weight_to_vbytes(5));
353    }
354
355    #[test]
356    fn calculate_fee() {
357        let feerate = Feerate { sats_per_kvb: 1000 };
358        assert_eq!(bitcoin::Amount::from_sat(25), feerate.calculate_fee(100));
359        assert_eq!(bitcoin::Amount::from_sat(26), feerate.calculate_fee(101));
360    }
361
362    #[test]
363    fn test_deserialize_amount_or_all() {
364        let all: BitcoinAmountOrAll = serde_json::from_str("\"all\"").unwrap();
365        assert_eq!(all, BitcoinAmountOrAll::All);
366
367        let amount: BitcoinAmountOrAll = serde_json::from_str("12345").unwrap();
368        assert_eq!(
369            amount,
370            BitcoinAmountOrAll::Amount(bitcoin::Amount::from_sat(12345))
371        );
372
373        let all_string = all.to_string();
374        assert_eq!(all_string, "all");
375        let amount_string = amount.to_string();
376        assert_eq!(amount_string, "0.00012345 BTC");
377        let all_parsed = BitcoinAmountOrAll::from_str(&all_string).unwrap();
378        assert_eq!(all, all_parsed);
379        let amount_parsed = BitcoinAmountOrAll::from_str(&amount_string).unwrap();
380        assert_eq!(amount, amount_parsed);
381    }
382}