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