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