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