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
38pub const SAFE_DEPOSIT_MODULE_CONSENSUS_VERSION: ModuleConsensusVersion =
41 ModuleConsensusVersion::new(2, 2);
42
43pub 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), 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#[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#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
127pub struct WalletSummary {
128 pub spendable_utxos: Vec<TxOutputSummary>,
130 pub unsigned_peg_out_txos: Vec<TxOutputSummary>,
133 pub unsigned_change_utxos: Vec<TxOutputSummary>,
136 pub unconfirmed_peg_out_txos: Vec<TxOutputSummary>,
139 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 pub fn total_spendable_balance(&self) -> Amount {
151 WalletSummary::sum(self.spendable_utxos.iter())
152 }
153
154 pub fn total_unsigned_peg_out_balance(&self) -> Amount {
157 WalletSummary::sum(self.unsigned_peg_out_txos.iter())
158 }
159
160 pub fn total_unsigned_change_balance(&self) -> Amount {
163 WalletSummary::sum(self.unsigned_change_utxos.iter())
164 }
165
166 pub fn total_unconfirmed_peg_out_balance(&self) -> Amount {
170 WalletSummary::sum(self.unconfirmed_peg_out_txos.iter())
171 }
172
173 pub fn total_unconfirmed_change_balance(&self) -> Amount {
176 WalletSummary::sum(self.unconfirmed_change_utxos.iter())
177 }
178
179 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 pub fn total_pending_change_balance(&self) -> Amount {
190 self.total_unsigned_change_balance() + self.total_unconfirmed_change_balance()
191 }
192
193 pub fn total_owned_balance(&self) -> Amount {
196 self.total_spendable_balance() + self.total_pending_change_balance()
197 }
198
199 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 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#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
224pub enum RecoveryItem {
225 Input {
227 outpoint: bitcoin::OutPoint,
229 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 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#[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#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
426pub struct Rbf {
427 pub fees: PegOutFees,
429 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
535pub 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}