fedimint_wallet_common/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::missing_panics_doc)]
4#![allow(clippy::module_name_repetitions)]
5#![allow(clippy::must_use_candidate)]
6#![allow(clippy::needless_lifetimes)]
7#![allow(clippy::return_self_not_must_use)]
8
9use std::hash::Hasher;
10
11use bitcoin::address::NetworkUnchecked;
12use bitcoin::psbt::raw::ProprietaryKey;
13use bitcoin::{secp256k1, Address, Amount, BlockHash, TxOut, Txid};
14use config::WalletClientConfig;
15use fedimint_core::core::{Decoder, ModuleInstanceId, ModuleKind};
16use fedimint_core::encoding::btc::NetworkLegacyEncodingWrapper;
17use fedimint_core::encoding::{Decodable, Encodable};
18use fedimint_core::module::{CommonModuleInit, ModuleCommon, ModuleConsensusVersion};
19use fedimint_core::{extensible_associated_module_type, plugin_types_trait_impl_common, Feerate};
20use impl_tools::autoimpl;
21use miniscript::Descriptor;
22use serde::{Deserialize, Serialize};
23use thiserror::Error;
24use tracing::error;
25
26use crate::keys::CompressedPublicKey;
27use crate::txoproof::{PegInProof, PegInProofError};
28
29pub mod config;
30pub mod endpoint_constants;
31pub mod envs;
32pub mod keys;
33pub mod tweakable;
34pub mod txoproof;
35
36pub const KIND: ModuleKind = ModuleKind::from_static_str("wallet");
37pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(2, 2);
38
39/// Module consensus version that introduced support for processing Bitcoin
40/// transactions that exceed the `ALEPH_BFT_UNIT_BYTE_LIMIT`.
41pub const SAFE_DEPOSIT_MODULE_CONSENSUS_VERSION: ModuleConsensusVersion =
42    ModuleConsensusVersion::new(2, 2);
43
44/// Used for estimating a feerate that will confirm within a target number of
45/// blocks.
46///
47/// Since the wallet's UTXOs are a shared resource, we need to reduce the risk
48/// of a peg-out transaction getting stuck in the mempool, hence we use a low
49/// confirmation target. Other fee bumping techniques, such as RBF and CPFP, can
50/// help mitigate this problem but are out-of-scope for this version of the
51/// wallet.
52pub const CONFIRMATION_TARGET: u16 = 1;
53
54/// To further mitigate the risk of a peg-out transaction getting stuck in the
55/// mempool, we multiply the feerate estimate returned from the backend by this
56/// value.
57pub const FEERATE_MULTIPLIER_DEFAULT: f64 = 2.0;
58
59pub type PartialSig = Vec<u8>;
60
61pub type PegInDescriptor = Descriptor<CompressedPublicKey>;
62
63#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Encodable, Decodable)]
64pub enum WalletConsensusItem {
65    BlockCount(u32), /* FIXME: use block hash instead, but needs more complicated
66                      * * verification logic */
67    Feerate(Feerate),
68    PegOutSignature(PegOutSignatureItem),
69    ModuleConsensusVersion(ModuleConsensusVersion),
70    #[encodable_default]
71    Default {
72        variant: u64,
73        bytes: Vec<u8>,
74    },
75}
76
77impl std::fmt::Display for WalletConsensusItem {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            WalletConsensusItem::BlockCount(count) => {
81                write!(f, "Wallet Block Count {count}")
82            }
83            WalletConsensusItem::Feerate(feerate) => {
84                write!(
85                    f,
86                    "Wallet Feerate with sats per kvb {}",
87                    feerate.sats_per_kvb
88                )
89            }
90            WalletConsensusItem::PegOutSignature(sig) => {
91                write!(f, "Wallet PegOut signature for Bitcoin TxId {}", sig.txid)
92            }
93            WalletConsensusItem::ModuleConsensusVersion(version) => {
94                write!(
95                    f,
96                    "Wallet Consensus Version {}.{}",
97                    version.major, version.minor
98                )
99            }
100            WalletConsensusItem::Default { variant, .. } => {
101                write!(f, "Unknown Wallet CI variant={variant}")
102            }
103        }
104    }
105}
106
107#[derive(Clone, Debug, Serialize, Deserialize, Encodable, Decodable)]
108pub struct PegOutSignatureItem {
109    pub txid: Txid,
110    pub signature: Vec<secp256k1::ecdsa::Signature>,
111}
112
113#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
114pub struct SpendableUTXO {
115    #[serde(with = "::fedimint_core::encoding::as_hex")]
116    pub tweak: [u8; 33],
117    #[serde(with = "bitcoin::amount::serde::as_sat")]
118    pub amount: bitcoin::Amount,
119}
120
121/// A transaction output, either unspent or consumed
122#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
123pub struct TxOutputSummary {
124    pub outpoint: bitcoin::OutPoint,
125    #[serde(with = "bitcoin::amount::serde::as_sat")]
126    pub amount: bitcoin::Amount,
127}
128
129/// Summary of the coins within the wallet.
130///
131/// Coins within the wallet go from spendable, to consumed in a transaction that
132/// does not have threshold signatures (unsigned), to threshold signed and
133/// unconfirmed on-chain (unconfirmed).
134///
135/// This summary provides the most granular view possible of coins in the
136/// wallet.
137#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
138pub struct WalletSummary {
139    /// All UTXOs available as inputs for transactions
140    pub spendable_utxos: Vec<TxOutputSummary>,
141    /// Transaction outputs consumed in peg-out transactions that have not
142    /// reached threshold signatures
143    pub unsigned_peg_out_txos: Vec<TxOutputSummary>,
144    /// Change UTXOs created from peg-out transactions that have not reached
145    /// threshold signatures
146    pub unsigned_change_utxos: Vec<TxOutputSummary>,
147    /// Transaction outputs consumed in peg-out transactions that have reached
148    /// threshold signatures waiting for finality delay confirmations
149    pub unconfirmed_peg_out_txos: Vec<TxOutputSummary>,
150    /// Change UTXOs created from peg-out transactions that have reached
151    /// threshold signatures waiting for finality delay confirmations
152    pub unconfirmed_change_utxos: Vec<TxOutputSummary>,
153}
154
155impl WalletSummary {
156    fn sum<'a>(txos: impl Iterator<Item = &'a TxOutputSummary>) -> Amount {
157        txos.fold(Amount::ZERO, |acc, txo| txo.amount + acc)
158    }
159
160    /// Total amount of all spendable UTXOs
161    pub fn total_spendable_balance(&self) -> Amount {
162        WalletSummary::sum(self.spendable_utxos.iter())
163    }
164
165    /// Total amount of all transaction outputs from peg-out transactions that
166    /// have not reached threshold signatures
167    pub fn total_unsigned_peg_out_balance(&self) -> Amount {
168        WalletSummary::sum(self.unsigned_peg_out_txos.iter())
169    }
170
171    /// Total amount of all change UTXOs from peg-out transactions that have not
172    /// reached threshold signatures
173    pub fn total_unsigned_change_balance(&self) -> Amount {
174        WalletSummary::sum(self.unsigned_change_utxos.iter())
175    }
176
177    /// Total amount of all transaction outputs from peg-out transactions that
178    /// have reached threshold signatures waiting for finality delay
179    /// confirmations
180    pub fn total_unconfirmed_peg_out_balance(&self) -> Amount {
181        WalletSummary::sum(self.unconfirmed_peg_out_txos.iter())
182    }
183
184    /// Total amount of all change UTXOs from peg-out transactions that have
185    /// reached threshold signatures waiting for finality delay confirmations
186    pub fn total_unconfirmed_change_balance(&self) -> Amount {
187        WalletSummary::sum(self.unconfirmed_change_utxos.iter())
188    }
189
190    /// Total amount of all transaction outputs from peg-out transactions that
191    /// are either waiting for threshold signatures or confirmations. This is
192    /// the total in-flight amount leaving the wallet.
193    pub fn total_pending_peg_out_balance(&self) -> Amount {
194        self.total_unsigned_peg_out_balance() + self.total_unconfirmed_peg_out_balance()
195    }
196
197    /// Total amount of all change UTXOs from peg-out transactions that are
198    /// either waiting for threshold signatures or confirmations. This is the
199    /// total in-flight amount that will become spendable by the wallet.
200    pub fn total_pending_change_balance(&self) -> Amount {
201        self.total_unsigned_change_balance() + self.total_unconfirmed_change_balance()
202    }
203
204    /// Total amount of immediately spendable UTXOs and pending change UTXOs.
205    /// This is the spendable balance once all transactions confirm.
206    pub fn total_owned_balance(&self) -> Amount {
207        self.total_spendable_balance() + self.total_pending_change_balance()
208    }
209
210    /// All transaction outputs from peg-out transactions that are either
211    /// waiting for threshold signatures or confirmations. These are all the
212    /// in-flight coins leaving the wallet.
213    pub fn pending_peg_out_txos(&self) -> Vec<TxOutputSummary> {
214        self.unsigned_peg_out_txos
215            .clone()
216            .into_iter()
217            .chain(self.unconfirmed_peg_out_txos.clone())
218            .collect()
219    }
220
221    /// All change UTXOs from peg-out transactions that are either waiting for
222    /// threshold signatures or confirmations. These are all the in-flight coins
223    /// that will become spendable by the wallet.
224    pub fn pending_change_utxos(&self) -> Vec<TxOutputSummary> {
225        self.unsigned_change_utxos
226            .clone()
227            .into_iter()
228            .chain(self.unconfirmed_change_utxos.clone())
229            .collect()
230    }
231}
232
233#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
234pub struct PegOutFees {
235    pub fee_rate: Feerate,
236    pub total_weight: u64,
237}
238
239impl PegOutFees {
240    pub fn new(sats_per_kvb: u64, total_weight: u64) -> Self {
241        PegOutFees {
242            fee_rate: Feerate { sats_per_kvb },
243            total_weight,
244        }
245    }
246
247    pub fn amount(&self) -> Amount {
248        self.fee_rate.calculate_fee(self.total_weight)
249    }
250}
251
252#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
253pub struct PegOut {
254    pub recipient: Address<NetworkUnchecked>,
255    #[serde(with = "bitcoin::amount::serde::as_sat")]
256    pub amount: bitcoin::Amount,
257    pub fees: PegOutFees,
258}
259
260extensible_associated_module_type!(
261    WalletOutputOutcome,
262    WalletOutputOutcomeV0,
263    UnknownWalletOutputOutcomeVariantError
264);
265
266impl WalletOutputOutcome {
267    pub fn new_v0(txid: bitcoin::Txid) -> WalletOutputOutcome {
268        WalletOutputOutcome::V0(WalletOutputOutcomeV0(txid))
269    }
270}
271
272/// Contains the Bitcoin transaction id of the transaction created by the
273/// withdraw request
274#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
275pub struct WalletOutputOutcomeV0(pub bitcoin::Txid);
276
277impl std::fmt::Display for WalletOutputOutcomeV0 {
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        write!(f, "Wallet PegOut Bitcoin TxId {}", self.0)
280    }
281}
282
283#[derive(Debug)]
284pub struct WalletCommonInit;
285
286impl CommonModuleInit for WalletCommonInit {
287    const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
288    const KIND: ModuleKind = KIND;
289
290    type ClientConfig = WalletClientConfig;
291
292    fn decoder() -> Decoder {
293        WalletModuleTypes::decoder()
294    }
295}
296
297#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
298pub enum WalletInput {
299    V0(WalletInputV0),
300    V1(WalletInputV1),
301    #[encodable_default]
302    Default {
303        variant: u64,
304        bytes: Vec<u8>,
305    },
306}
307
308impl WalletInput {
309    pub fn maybe_v0_ref(&self) -> Option<&WalletInputV0> {
310        match self {
311            WalletInput::V0(v0) => Some(v0),
312            _ => None,
313        }
314    }
315}
316
317#[derive(
318    Debug,
319    thiserror::Error,
320    Clone,
321    Eq,
322    PartialEq,
323    Hash,
324    serde::Deserialize,
325    serde::Serialize,
326    fedimint_core::encoding::Encodable,
327    fedimint_core::encoding::Decodable,
328)]
329#[error("Unknown {} variant {variant}", stringify!($name))]
330pub struct UnknownWalletInputVariantError {
331    pub variant: u64,
332}
333
334impl std::fmt::Display for WalletInput {
335    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336        match &self {
337            WalletInput::V0(inner) => std::fmt::Display::fmt(&inner, f),
338            WalletInput::V1(inner) => std::fmt::Display::fmt(&inner, f),
339            WalletInput::Default { variant, .. } => {
340                write!(f, "Unknown variant (variant={variant})")
341            }
342        }
343    }
344}
345
346impl WalletInput {
347    pub fn new_v0(peg_in_proof: PegInProof) -> WalletInput {
348        WalletInput::V0(WalletInputV0(Box::new(peg_in_proof)))
349    }
350
351    pub fn new_v1(peg_in_proof: &PegInProof) -> WalletInput {
352        WalletInput::V1(WalletInputV1 {
353            outpoint: peg_in_proof.outpoint(),
354            tweak_contract_key: *peg_in_proof.tweak_contract_key(),
355            tx_out: peg_in_proof.tx_output(),
356        })
357    }
358}
359
360#[autoimpl(Deref, DerefMut using self.0)]
361#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
362pub struct WalletInputV0(pub Box<PegInProof>);
363
364#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
365pub struct WalletInputV1 {
366    pub outpoint: bitcoin::OutPoint,
367    pub tweak_contract_key: secp256k1::PublicKey,
368    pub tx_out: TxOut,
369}
370
371impl std::fmt::Display for WalletInputV0 {
372    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373        write!(
374            f,
375            "Wallet PegIn with Bitcoin TxId {}",
376            self.0.outpoint().txid
377        )
378    }
379}
380
381impl std::fmt::Display for WalletInputV1 {
382    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383        write!(f, "Wallet PegIn V1 with TxId {}", self.outpoint.txid)
384    }
385}
386
387extensible_associated_module_type!(
388    WalletOutput,
389    WalletOutputV0,
390    UnknownWalletOutputVariantError
391);
392
393impl WalletOutput {
394    pub fn new_v0_peg_out(
395        recipient: Address,
396        amount: bitcoin::Amount,
397        fees: PegOutFees,
398    ) -> WalletOutput {
399        WalletOutput::V0(WalletOutputV0::PegOut(PegOut {
400            recipient: recipient.into_unchecked(),
401            amount,
402            fees,
403        }))
404    }
405    pub fn new_v0_rbf(fees: PegOutFees, txid: Txid) -> WalletOutput {
406        WalletOutput::V0(WalletOutputV0::Rbf(Rbf { fees, txid }))
407    }
408}
409
410#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
411pub enum WalletOutputV0 {
412    PegOut(PegOut),
413    Rbf(Rbf),
414}
415
416/// Allows a user to bump the fees of a `PendingTransaction`
417#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
418pub struct Rbf {
419    /// Fees expressed as an increase over existing peg-out fees
420    pub fees: PegOutFees,
421    /// Bitcoin tx id to bump the fees for
422    pub txid: Txid,
423}
424
425impl WalletOutputV0 {
426    pub fn amount(&self) -> Amount {
427        match self {
428            WalletOutputV0::PegOut(pegout) => pegout.amount + pegout.fees.amount(),
429            WalletOutputV0::Rbf(rbf) => rbf.fees.amount(),
430        }
431    }
432}
433
434impl std::fmt::Display for WalletOutputV0 {
435    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436        match self {
437            WalletOutputV0::PegOut(pegout) => {
438                write!(
439                    f,
440                    "Wallet PegOut {} to {}",
441                    pegout.amount,
442                    pegout.recipient.clone().assume_checked()
443                )
444            }
445            WalletOutputV0::Rbf(rbf) => write!(f, "Wallet RBF {:?} to {}", rbf.fees, rbf.txid),
446        }
447    }
448}
449
450pub struct WalletModuleTypes;
451
452pub fn proprietary_tweak_key() -> ProprietaryKey {
453    ProprietaryKey {
454        prefix: b"fedimint".to_vec(),
455        subtype: 0x00,
456        key: vec![],
457    }
458}
459
460impl std::hash::Hash for PegOutSignatureItem {
461    fn hash<H: Hasher>(&self, state: &mut H) {
462        self.txid.hash(state);
463        for sig in &self.signature {
464            sig.serialize_der().hash(state);
465        }
466    }
467}
468
469impl PartialEq for PegOutSignatureItem {
470    fn eq(&self, other: &PegOutSignatureItem) -> bool {
471        self.txid == other.txid && self.signature == other.signature
472    }
473}
474
475impl Eq for PegOutSignatureItem {}
476
477plugin_types_trait_impl_common!(
478    KIND,
479    WalletModuleTypes,
480    WalletClientConfig,
481    WalletInput,
482    WalletOutput,
483    WalletOutputOutcome,
484    WalletConsensusItem,
485    WalletInputError,
486    WalletOutputError
487);
488
489#[derive(Debug, Error, Clone)]
490pub enum WalletCreationError {
491    #[error("Connected bitcoind is on wrong network, expected {0}, got {1}")]
492    WrongNetwork(NetworkLegacyEncodingWrapper, NetworkLegacyEncodingWrapper),
493    #[error("Error querying bitcoind: {0}")]
494    RpcError(String),
495    #[error("Feerate source error: {0}")]
496    FeerateSourceError(String),
497    #[error("Block count source error: {0}")]
498    BlockCountSourceError(String),
499}
500
501#[derive(Debug, Error, Encodable, Decodable, Hash, Clone, Eq, PartialEq)]
502pub enum WalletInputError {
503    #[error("Unknown block hash in peg-in proof: {0}")]
504    UnknownPegInProofBlock(BlockHash),
505    #[error("Invalid peg-in proof: {0}")]
506    PegInProofError(#[from] PegInProofError),
507    #[error("The peg-in was already claimed")]
508    PegInAlreadyClaimed,
509    #[error("The wallet input version is not supported by this federation")]
510    UnknownInputVariant(#[from] UnknownWalletInputVariantError),
511    #[error("Unknown UTXO")]
512    UnknownUTXO,
513    #[error("Wrong output script")]
514    WrongOutputScript,
515    #[error("Wrong tx out")]
516    WrongTxOut,
517}
518
519#[derive(Debug, Error, Encodable, Decodable, Hash, Clone, Eq, PartialEq)]
520pub enum WalletOutputError {
521    #[error("Connected bitcoind is on wrong network, expected {0}, got {1}")]
522    WrongNetwork(NetworkLegacyEncodingWrapper, NetworkLegacyEncodingWrapper),
523    #[error("Peg-out fee rate {0:?} is set below consensus {1:?}")]
524    PegOutFeeBelowConsensus(Feerate, Feerate),
525    #[error("Not enough SpendableUTXO")]
526    NotEnoughSpendableUTXO,
527    #[error("Peg out amount was under the dust limit")]
528    PegOutUnderDustLimit,
529    #[error("RBF transaction id not found")]
530    RbfTransactionIdNotFound,
531    #[error("Peg-out fee weight {0} doesn't match actual weight {1}")]
532    TxWeightIncorrect(u64, u64),
533    #[error("Peg-out fee rate is below min relay fee")]
534    BelowMinRelayFee,
535    #[error("The wallet output version is not supported by this federation")]
536    UnknownOutputVariant(#[from] UnknownWalletOutputVariantError),
537}
538
539// For backwards-compatibility with old clients, we use an UnknownOutputVariant
540// error when a client attempts a deprecated RBF withdrawal.
541// see: https://github.com/fedimint/fedimint/issues/5453
542pub const DEPRECATED_RBF_ERROR: WalletOutputError =
543    WalletOutputError::UnknownOutputVariant(UnknownWalletOutputVariantError { variant: 1 });
544
545#[derive(Debug, Error)]
546pub enum ProcessPegOutSigError {
547    #[error("No unsigned transaction with id {0} exists")]
548    UnknownTransaction(Txid),
549    #[error("Expected {0} signatures, got {1}")]
550    WrongSignatureCount(usize, usize),
551    #[error("Bad Sighash")]
552    SighashError,
553    #[error("Malformed signature: {0}")]
554    MalformedSignature(secp256k1::Error),
555    #[error("Invalid signature")]
556    InvalidSignature,
557    #[error("Duplicate signature")]
558    DuplicateSignature,
559    #[error("Missing change tweak")]
560    MissingOrMalformedChangeTweak,
561    #[error("Error finalizing PSBT {0:?}")]
562    ErrorFinalizingPsbt(Vec<miniscript::psbt::Error>),
563}