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