Skip to main content

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