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
39pub const SAFE_DEPOSIT_MODULE_CONSENSUS_VERSION: ModuleConsensusVersion =
42 ModuleConsensusVersion::new(2, 2);
43
44pub 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), 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#[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#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
128pub struct WalletSummary {
129 pub spendable_utxos: Vec<TxOutputSummary>,
131 pub unsigned_peg_out_txos: Vec<TxOutputSummary>,
134 pub unsigned_change_utxos: Vec<TxOutputSummary>,
137 pub unconfirmed_peg_out_txos: Vec<TxOutputSummary>,
140 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 pub fn total_spendable_balance(&self) -> Amount {
152 WalletSummary::sum(self.spendable_utxos.iter())
153 }
154
155 pub fn total_unsigned_peg_out_balance(&self) -> Amount {
158 WalletSummary::sum(self.unsigned_peg_out_txos.iter())
159 }
160
161 pub fn total_unsigned_change_balance(&self) -> Amount {
164 WalletSummary::sum(self.unsigned_change_utxos.iter())
165 }
166
167 pub fn total_unconfirmed_peg_out_balance(&self) -> Amount {
171 WalletSummary::sum(self.unconfirmed_peg_out_txos.iter())
172 }
173
174 pub fn total_unconfirmed_change_balance(&self) -> Amount {
177 WalletSummary::sum(self.unconfirmed_change_utxos.iter())
178 }
179
180 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 pub fn total_pending_change_balance(&self) -> Amount {
191 self.total_unsigned_change_balance() + self.total_unconfirmed_change_balance()
192 }
193
194 pub fn total_owned_balance(&self) -> Amount {
197 self.total_spendable_balance() + self.total_pending_change_balance()
198 }
199
200 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 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#[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#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
408pub struct Rbf {
409 pub fees: PegOutFees,
411 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
527pub 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}