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