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