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, OutPoint, 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 miniscript::Descriptor;
21use serde::{Deserialize, Serialize};
22use thiserror::Error;
23
24use crate::keys::CompressedPublicKey;
25use crate::txoproof::{PegInProof, PegInProofError};
26
27pub mod config;
28pub mod endpoint_constants;
29pub mod envs;
30pub mod keys;
31pub mod tweakable;
32pub mod txoproof;
33
34pub const KIND: ModuleKind = ModuleKind::from_static_str("wallet");
35pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(2, 3);
36
37pub const CHECKED_PEG_OUT_FEE_MODULE_CONSENSUS_VERSION: ModuleConsensusVersion =
45 ModuleConsensusVersion::new(2, 3);
46
47pub const SAFE_DEPOSIT_MODULE_CONSENSUS_VERSION: ModuleConsensusVersion =
50 ModuleConsensusVersion::new(2, 2);
51
52pub const FEERATE_MULTIPLIER_DEFAULT: f64 = 2.0;
56
57pub type PartialSig = Vec<u8>;
58
59pub type PegInDescriptor = Descriptor<CompressedPublicKey>;
60
61#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Encodable, Decodable)]
62pub enum WalletConsensusItem {
63 BlockCount(u32), Feerate(Feerate),
66 PegOutSignature(PegOutSignatureItem),
67 ModuleConsensusVersion(ModuleConsensusVersion),
68 #[encodable_default]
69 Default {
70 variant: u64,
71 bytes: Vec<u8>,
72 },
73}
74
75impl std::fmt::Display for WalletConsensusItem {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 WalletConsensusItem::BlockCount(count) => {
79 write!(f, "Wallet Block Count {count}")
80 }
81 WalletConsensusItem::Feerate(feerate) => {
82 write!(
83 f,
84 "Wallet Feerate with sats per kvb {}",
85 feerate.sats_per_kvb
86 )
87 }
88 WalletConsensusItem::PegOutSignature(sig) => {
89 write!(f, "Wallet PegOut signature for Bitcoin TxId {}", sig.txid)
90 }
91 WalletConsensusItem::ModuleConsensusVersion(version) => {
92 write!(
93 f,
94 "Wallet Consensus Version {}.{}",
95 version.major, version.minor
96 )
97 }
98 WalletConsensusItem::Default { variant, .. } => {
99 write!(f, "Unknown Wallet CI variant={variant}")
100 }
101 }
102 }
103}
104
105#[derive(Clone, Debug, Serialize, Deserialize, Encodable, Decodable)]
106pub struct PegOutSignatureItem {
107 pub txid: Txid,
108 pub signature: Vec<secp256k1::ecdsa::Signature>,
109}
110
111#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
112pub struct SpendableUTXO {
113 #[serde(with = "::fedimint_core::encoding::as_hex")]
114 pub tweak: [u8; 33],
115 #[serde(with = "bitcoin::amount::serde::as_sat")]
116 pub amount: bitcoin::Amount,
117}
118
119#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
121pub struct TxOutputSummary {
122 pub outpoint: OutPoint,
123 #[serde(with = "bitcoin::amount::serde::as_sat")]
124 pub amount: Amount,
125}
126
127#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
136pub struct WalletSummary {
137 pub spendable_utxos: Vec<TxOutputSummary>,
139 pub unsigned_peg_out_txos: Vec<TxOutputSummary>,
142 pub unsigned_change_utxos: Vec<TxOutputSummary>,
145 pub unconfirmed_peg_out_txos: Vec<TxOutputSummary>,
148 pub unconfirmed_change_utxos: Vec<TxOutputSummary>,
151}
152
153impl WalletSummary {
154 fn sum<'a>(txos: impl Iterator<Item = &'a TxOutputSummary>) -> Amount {
155 txos.fold(Amount::ZERO, |acc, txo| txo.amount + acc)
156 }
157
158 pub fn total_spendable_balance(&self) -> Amount {
160 WalletSummary::sum(self.spendable_utxos.iter())
161 }
162
163 pub fn total_unsigned_peg_out_balance(&self) -> Amount {
166 WalletSummary::sum(self.unsigned_peg_out_txos.iter())
167 }
168
169 pub fn total_unsigned_change_balance(&self) -> Amount {
172 WalletSummary::sum(self.unsigned_change_utxos.iter())
173 }
174
175 pub fn total_unconfirmed_peg_out_balance(&self) -> Amount {
179 WalletSummary::sum(self.unconfirmed_peg_out_txos.iter())
180 }
181
182 pub fn total_unconfirmed_change_balance(&self) -> Amount {
185 WalletSummary::sum(self.unconfirmed_change_utxos.iter())
186 }
187
188 pub fn total_pending_peg_out_balance(&self) -> Amount {
192 self.total_unsigned_peg_out_balance() + self.total_unconfirmed_peg_out_balance()
193 }
194
195 pub fn total_pending_change_balance(&self) -> Amount {
199 self.total_unsigned_change_balance() + self.total_unconfirmed_change_balance()
200 }
201
202 pub fn total_owned_balance(&self) -> Amount {
205 self.total_spendable_balance() + self.total_pending_change_balance()
206 }
207
208 pub fn pending_peg_out_txos(&self) -> Vec<TxOutputSummary> {
212 self.unsigned_peg_out_txos
213 .clone()
214 .into_iter()
215 .chain(self.unconfirmed_peg_out_txos.clone())
216 .collect()
217 }
218
219 pub fn pending_change_utxos(&self) -> Vec<TxOutputSummary> {
223 self.unsigned_change_utxos
224 .clone()
225 .into_iter()
226 .chain(self.unconfirmed_change_utxos.clone())
227 .collect()
228 }
229}
230
231#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
233pub enum RecoveryItem {
234 Input {
236 outpoint: bitcoin::OutPoint,
238 script: bitcoin::ScriptBuf,
240 },
241}
242
243#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
244pub struct PegOutFees {
245 pub fee_rate: Feerate,
246 pub total_weight: u64,
247}
248
249impl PegOutFees {
250 pub fn new(sats_per_kvb: u64, total_weight: u64) -> Self {
251 PegOutFees {
252 fee_rate: Feerate { sats_per_kvb },
253 total_weight,
254 }
255 }
256
257 pub fn from_amount(amount: bitcoin::Amount) -> Self {
261 PegOutFees::new(amount.to_sat(), 4000)
262 }
263
264 pub fn amount(&self) -> Amount {
265 self.fee_rate.calculate_fee(self.total_weight)
266 }
267}
268
269#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
270pub struct PegOut {
271 pub recipient: Address<NetworkUnchecked>,
272 #[serde(with = "bitcoin::amount::serde::as_sat")]
273 pub amount: bitcoin::Amount,
274 pub fees: PegOutFees,
275}
276
277extensible_associated_module_type!(
278 WalletOutputOutcome,
279 WalletOutputOutcomeV0,
280 UnknownWalletOutputOutcomeVariantError
281);
282
283impl WalletOutputOutcome {
284 pub fn new_v0(txid: bitcoin::Txid) -> WalletOutputOutcome {
285 WalletOutputOutcome::V0(WalletOutputOutcomeV0(txid))
286 }
287}
288
289#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
292pub struct WalletOutputOutcomeV0(pub bitcoin::Txid);
293
294impl std::fmt::Display for WalletOutputOutcomeV0 {
295 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296 write!(f, "Wallet PegOut Bitcoin TxId {}", self.0)
297 }
298}
299
300#[derive(Debug)]
301pub struct WalletCommonInit;
302
303impl CommonModuleInit for WalletCommonInit {
304 const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
305 const KIND: ModuleKind = KIND;
306
307 type ClientConfig = WalletClientConfig;
308
309 fn decoder() -> Decoder {
310 WalletModuleTypes::decoder()
311 }
312}
313
314#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
315pub enum WalletInput {
316 V0(WalletInputV0),
317 V1(WalletInputV1),
318 #[encodable_default]
319 Default {
320 variant: u64,
321 bytes: Vec<u8>,
322 },
323}
324
325impl WalletInput {
326 pub fn maybe_v0_ref(&self) -> Option<&WalletInputV0> {
327 match self {
328 WalletInput::V0(v0) => Some(v0),
329 _ => None,
330 }
331 }
332}
333
334#[derive(
335 Debug,
336 thiserror::Error,
337 Clone,
338 Eq,
339 PartialEq,
340 Hash,
341 serde::Deserialize,
342 serde::Serialize,
343 fedimint_core::encoding::Encodable,
344 fedimint_core::encoding::Decodable,
345)]
346#[error("Unknown {} variant {variant}", stringify!($name))]
347pub struct UnknownWalletInputVariantError {
348 pub variant: u64,
349}
350
351impl std::fmt::Display for WalletInput {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 match &self {
354 WalletInput::V0(inner) => std::fmt::Display::fmt(&inner, f),
355 WalletInput::V1(inner) => std::fmt::Display::fmt(&inner, f),
356 WalletInput::Default { variant, .. } => {
357 write!(f, "Unknown variant (variant={variant})")
358 }
359 }
360 }
361}
362
363impl WalletInput {
364 pub fn new_v0(peg_in_proof: PegInProof) -> WalletInput {
365 WalletInput::V0(WalletInputV0(Box::new(peg_in_proof)))
366 }
367
368 pub fn new_v1(peg_in_proof: &PegInProof) -> WalletInput {
369 WalletInput::V1(WalletInputV1 {
370 outpoint: peg_in_proof.outpoint(),
371 tweak_key: peg_in_proof.tweak_key(),
372 tx_out: peg_in_proof.tx_output(),
373 })
374 }
375}
376
377#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
378pub struct WalletInputV0(pub Box<PegInProof>);
379
380impl std::ops::Deref for WalletInputV0 {
381 type Target = Box<PegInProof>;
382
383 fn deref(&self) -> &Self::Target {
384 &self.0
385 }
386}
387
388impl std::ops::DerefMut for WalletInputV0 {
389 fn deref_mut(&mut self) -> &mut Self::Target {
390 &mut self.0
391 }
392}
393
394#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
395pub struct WalletInputV1 {
396 pub outpoint: bitcoin::OutPoint,
397 pub tweak_key: secp256k1::PublicKey,
398 pub tx_out: TxOut,
399}
400
401impl std::fmt::Display for WalletInputV0 {
402 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403 write!(
404 f,
405 "Wallet PegIn with Bitcoin TxId {}",
406 self.0.outpoint().txid
407 )
408 }
409}
410
411impl std::fmt::Display for WalletInputV1 {
412 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 write!(f, "Wallet PegIn V1 with TxId {}", self.outpoint.txid)
414 }
415}
416
417extensible_associated_module_type!(
418 WalletOutput,
419 WalletOutputV0,
420 UnknownWalletOutputVariantError
421);
422
423impl WalletOutput {
424 pub fn new_v0_peg_out(
425 recipient: Address,
426 amount: bitcoin::Amount,
427 fees: PegOutFees,
428 ) -> WalletOutput {
429 WalletOutput::V0(WalletOutputV0::PegOut(PegOut {
430 recipient: recipient.into_unchecked(),
431 amount,
432 fees,
433 }))
434 }
435 pub fn new_v0_rbf(fees: PegOutFees, txid: Txid) -> WalletOutput {
436 WalletOutput::V0(WalletOutputV0::Rbf(Rbf { fees, txid }))
437 }
438}
439
440#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
441pub enum WalletOutputV0 {
442 PegOut(PegOut),
443 Rbf(Rbf),
444}
445
446#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
448pub struct Rbf {
449 pub fees: PegOutFees,
451 pub txid: Txid,
453}
454
455impl WalletOutputV0 {
456 pub fn amount(&self) -> Amount {
457 match self {
458 WalletOutputV0::PegOut(pegout) => pegout.amount + pegout.fees.amount(),
459 WalletOutputV0::Rbf(rbf) => rbf.fees.amount(),
460 }
461 }
462}
463
464impl std::fmt::Display for WalletOutputV0 {
465 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466 match self {
467 WalletOutputV0::PegOut(pegout) => {
468 write!(
469 f,
470 "Wallet PegOut {} to {}",
471 pegout.amount,
472 pegout.recipient.clone().assume_checked()
473 )
474 }
475 WalletOutputV0::Rbf(rbf) => write!(f, "Wallet RBF {:?} to {}", rbf.fees, rbf.txid),
476 }
477 }
478}
479
480pub struct WalletModuleTypes;
481
482pub fn proprietary_tweak_key() -> ProprietaryKey {
483 ProprietaryKey {
484 prefix: b"fedimint".to_vec(),
485 subtype: 0x00,
486 key: vec![],
487 }
488}
489
490impl std::hash::Hash for PegOutSignatureItem {
491 fn hash<H: Hasher>(&self, state: &mut H) {
492 self.txid.hash(state);
493 for sig in &self.signature {
494 sig.serialize_der().hash(state);
495 }
496 }
497}
498
499impl PartialEq for PegOutSignatureItem {
500 fn eq(&self, other: &PegOutSignatureItem) -> bool {
501 self.txid == other.txid && self.signature == other.signature
502 }
503}
504
505impl Eq for PegOutSignatureItem {}
506
507plugin_types_trait_impl_common!(
508 KIND,
509 WalletModuleTypes,
510 WalletClientConfig,
511 WalletInput,
512 WalletOutput,
513 WalletOutputOutcome,
514 WalletConsensusItem,
515 WalletInputError,
516 WalletOutputError
517);
518
519#[derive(Debug, Error, Encodable, Decodable, Hash, Clone, Eq, PartialEq)]
520pub enum WalletInputError {
521 #[error("Unknown block hash in peg-in proof: {0}")]
522 UnknownPegInProofBlock(BlockHash),
523 #[error("Invalid peg-in proof: {0}")]
524 PegInProofError(#[from] PegInProofError),
525 #[error("The peg-in was already claimed")]
526 PegInAlreadyClaimed,
527 #[error("The wallet input version is not supported by this federation")]
528 UnknownInputVariant(#[from] UnknownWalletInputVariantError),
529 #[error("Unknown UTXO")]
530 UnknownUTXO,
531 #[error("Wrong output script")]
532 WrongOutputScript,
533 #[error("Wrong tx out")]
534 WrongTxOut,
535}
536
537#[derive(Debug, Error, Encodable, Decodable, Hash, Clone, Eq, PartialEq)]
538pub enum WalletOutputError {
539 #[error("Connected bitcoind is on wrong network, expected {0}, got {1}")]
540 WrongNetwork(NetworkLegacyEncodingWrapper, NetworkLegacyEncodingWrapper),
541 #[error("Peg-out fee rate {0:?} is set below consensus {1:?}")]
542 PegOutFeeBelowConsensus(Feerate, Feerate),
543 #[error("Not enough SpendableUTXO")]
544 NotEnoughSpendableUTXO,
545 #[error("Peg out amount was under the dust limit")]
546 PegOutUnderDustLimit,
547 #[error("RBF transaction id not found")]
548 RbfTransactionIdNotFound,
549 #[error("Peg-out fee weight {0} doesn't match actual weight {1}")]
550 TxWeightIncorrect(u64, u64),
551 #[error("Peg-out fee rate is below min relay fee")]
552 BelowMinRelayFee,
553 #[error("The wallet output version is not supported by this federation")]
554 UnknownOutputVariant(#[from] UnknownWalletOutputVariantError),
555}
556
557pub const DEPRECATED_RBF_ERROR: WalletOutputError =
561 WalletOutputError::UnknownOutputVariant(UnknownWalletOutputVariantError { variant: 1 });
562
563#[derive(Debug, Error)]
564pub enum ProcessPegOutSigError {
565 #[error("No unsigned transaction with id {0} exists")]
566 UnknownTransaction(Txid),
567 #[error("Expected {0} signatures, got {1}")]
568 WrongSignatureCount(usize, usize),
569 #[error("Bad Sighash")]
570 SighashError,
571 #[error("Malformed signature: {0}")]
572 MalformedSignature(secp256k1::Error),
573 #[error("Invalid signature")]
574 InvalidSignature,
575 #[error("Duplicate signature")]
576 DuplicateSignature,
577 #[error("Missing change tweak")]
578 MissingOrMalformedChangeTweak,
579 #[error("Error finalizing PSBT {0:?}")]
580 ErrorFinalizingPsbt(Vec<miniscript::psbt::Error>),
581}