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