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::return_self_not_must_use)]
7
8use std::hash::Hasher;
9
10use bitcoin::address::NetworkUnchecked;
11use bitcoin::psbt::raw::ProprietaryKey;
12use bitcoin::{secp256k1, Address, Amount, BlockHash, Network, Txid};
13use config::WalletClientConfig;
14use fedimint_core::core::{Decoder, ModuleInstanceId, ModuleKind};
15use fedimint_core::encoding::{Decodable, Encodable};
16use fedimint_core::module::{CommonModuleInit, ModuleCommon, ModuleConsensusVersion};
17use fedimint_core::{extensible_associated_module_type, plugin_types_trait_impl_common, Feerate};
18use impl_tools::autoimpl;
19use miniscript::Descriptor;
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22use tracing::error;
23
24use crate::keys::CompressedPublicKey;
25use crate::txoproof::{PegInProof, PegInProofError};
26
27pub mod config;
28pub mod endpoint_constants;
29pub mod envs;
30pub mod keys;
31pub mod tweakable;
32pub mod txoproof;
33
34pub const KIND: ModuleKind = ModuleKind::from_static_str("wallet");
35pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(2, 1);
36
37pub const CONFIRMATION_TARGET: u16 = 1;
46
47pub const FEERATE_MULTIPLIER: u64 = 4;
51
52pub type PartialSig = Vec<u8>;
53
54pub type PegInDescriptor = Descriptor<CompressedPublicKey>;
55
56#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Encodable, Decodable)]
57pub enum WalletConsensusItem {
58 BlockCount(u32), Feerate(Feerate),
61 PegOutSignature(PegOutSignatureItem),
62 #[encodable_default]
63 Default {
64 variant: u64,
65 bytes: Vec<u8>,
66 },
67}
68
69impl std::fmt::Display for WalletConsensusItem {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 WalletConsensusItem::BlockCount(count) => {
73 write!(f, "Wallet Block Count {count}")
74 }
75 WalletConsensusItem::Feerate(feerate) => {
76 write!(
77 f,
78 "Wallet Feerate with sats per kvb {}",
79 feerate.sats_per_kvb
80 )
81 }
82 WalletConsensusItem::PegOutSignature(sig) => {
83 write!(f, "Wallet PegOut signature for Bitcoin TxId {}", sig.txid)
84 }
85 WalletConsensusItem::Default { variant, .. } => {
86 write!(f, "Unknown Wallet CI variant={variant}")
87 }
88 }
89 }
90}
91
92#[derive(Clone, Debug, Serialize, Deserialize, Encodable, Decodable)]
93pub struct PegOutSignatureItem {
94 pub txid: Txid,
95 pub signature: Vec<secp256k1::ecdsa::Signature>,
96}
97
98#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
99pub struct SpendableUTXO {
100 #[serde(with = "::fedimint_core::encoding::as_hex")]
101 pub tweak: [u8; 33],
102 #[serde(with = "bitcoin::amount::serde::as_sat")]
103 pub amount: bitcoin::Amount,
104}
105
106#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
108pub struct TxOutputSummary {
109 pub outpoint: bitcoin::OutPoint,
110 #[serde(with = "bitcoin::amount::serde::as_sat")]
111 pub amount: bitcoin::Amount,
112}
113
114#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
123pub struct WalletSummary {
124 pub spendable_utxos: Vec<TxOutputSummary>,
126 pub unsigned_peg_out_txos: Vec<TxOutputSummary>,
129 pub unsigned_change_utxos: Vec<TxOutputSummary>,
132 pub unconfirmed_peg_out_txos: Vec<TxOutputSummary>,
135 pub unconfirmed_change_utxos: Vec<TxOutputSummary>,
138}
139
140impl WalletSummary {
141 fn sum<'a>(txos: impl Iterator<Item = &'a TxOutputSummary>) -> Amount {
142 txos.fold(Amount::ZERO, |acc, txo| txo.amount + acc)
143 }
144
145 pub fn total_spendable_balance(&self) -> Amount {
147 WalletSummary::sum(self.spendable_utxos.iter())
148 }
149
150 pub fn total_unsigned_peg_out_balance(&self) -> Amount {
153 WalletSummary::sum(self.unsigned_peg_out_txos.iter())
154 }
155
156 pub fn total_unsigned_change_balance(&self) -> Amount {
159 WalletSummary::sum(self.unsigned_change_utxos.iter())
160 }
161
162 pub fn total_unconfirmed_peg_out_balance(&self) -> Amount {
166 WalletSummary::sum(self.unconfirmed_peg_out_txos.iter())
167 }
168
169 pub fn total_unconfirmed_change_balance(&self) -> Amount {
172 WalletSummary::sum(self.unconfirmed_change_utxos.iter())
173 }
174
175 pub fn total_pending_peg_out_balance(&self) -> Amount {
179 self.total_unsigned_peg_out_balance() + self.total_unconfirmed_peg_out_balance()
180 }
181
182 pub fn total_pending_change_balance(&self) -> Amount {
186 self.total_unsigned_change_balance() + self.total_unconfirmed_change_balance()
187 }
188
189 pub fn total_owned_balance(&self) -> Amount {
192 self.total_spendable_balance() + self.total_pending_change_balance()
193 }
194
195 pub fn pending_peg_out_txos(&self) -> Vec<TxOutputSummary> {
199 self.unsigned_peg_out_txos
200 .clone()
201 .into_iter()
202 .chain(self.unconfirmed_peg_out_txos.clone())
203 .collect()
204 }
205
206 pub fn pending_change_utxos(&self) -> Vec<TxOutputSummary> {
210 self.unsigned_change_utxos
211 .clone()
212 .into_iter()
213 .chain(self.unconfirmed_change_utxos.clone())
214 .collect()
215 }
216}
217
218#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
219pub struct PegOutFees {
220 pub fee_rate: Feerate,
221 pub total_weight: u64,
222}
223
224impl PegOutFees {
225 pub fn new(sats_per_kvb: u64, total_weight: u64) -> Self {
226 PegOutFees {
227 fee_rate: Feerate { sats_per_kvb },
228 total_weight,
229 }
230 }
231
232 pub fn amount(&self) -> Amount {
233 self.fee_rate.calculate_fee(self.total_weight)
234 }
235}
236
237#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
238pub struct PegOut {
239 pub recipient: Address<NetworkUnchecked>,
240 #[serde(with = "bitcoin::amount::serde::as_sat")]
241 pub amount: bitcoin::Amount,
242 pub fees: PegOutFees,
243}
244
245extensible_associated_module_type!(
246 WalletOutputOutcome,
247 WalletOutputOutcomeV0,
248 UnknownWalletOutputOutcomeVariantError
249);
250
251impl WalletOutputOutcome {
252 pub fn new_v0(txid: bitcoin::Txid) -> WalletOutputOutcome {
253 WalletOutputOutcome::V0(WalletOutputOutcomeV0(txid))
254 }
255}
256
257#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
260pub struct WalletOutputOutcomeV0(pub bitcoin::Txid);
261
262impl std::fmt::Display for WalletOutputOutcomeV0 {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 write!(f, "Wallet PegOut Bitcoin TxId {}", self.0)
265 }
266}
267
268#[derive(Debug)]
269pub struct WalletCommonInit;
270
271impl CommonModuleInit for WalletCommonInit {
272 const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
273 const KIND: ModuleKind = KIND;
274
275 type ClientConfig = WalletClientConfig;
276
277 fn decoder() -> Decoder {
278 WalletModuleTypes::decoder()
279 }
280}
281
282extensible_associated_module_type!(WalletInput, WalletInputV0, UnknownWalletInputVariantError);
283
284impl WalletInput {
285 pub fn new_v0(peg_in_proof: PegInProof) -> WalletInput {
286 WalletInput::V0(WalletInputV0(Box::new(peg_in_proof)))
287 }
288}
289
290#[autoimpl(Deref, DerefMut using self.0)]
291#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
292pub struct WalletInputV0(pub Box<PegInProof>);
293
294impl std::fmt::Display for WalletInputV0 {
295 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296 write!(
297 f,
298 "Wallet PegIn with Bitcoin TxId {}",
299 self.0.outpoint().txid
300 )
301 }
302}
303
304extensible_associated_module_type!(
305 WalletOutput,
306 WalletOutputV0,
307 UnknownWalletOutputVariantError
308);
309
310impl WalletOutput {
311 pub fn new_v0_peg_out(
312 recipient: Address<NetworkUnchecked>,
313 amount: bitcoin::Amount,
314 fees: PegOutFees,
315 ) -> WalletOutput {
316 WalletOutput::V0(WalletOutputV0::PegOut(PegOut {
317 recipient,
318 amount,
319 fees,
320 }))
321 }
322 pub fn new_v0_rbf(fees: PegOutFees, txid: Txid) -> WalletOutput {
323 WalletOutput::V0(WalletOutputV0::Rbf(Rbf { fees, txid }))
324 }
325}
326
327#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
328pub enum WalletOutputV0 {
329 PegOut(PegOut),
330 Rbf(Rbf),
331}
332
333#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
335pub struct Rbf {
336 pub fees: PegOutFees,
338 pub txid: Txid,
340}
341
342impl WalletOutputV0 {
343 pub fn amount(&self) -> Amount {
344 match self {
345 WalletOutputV0::PegOut(pegout) => pegout.amount + pegout.fees.amount(),
346 WalletOutputV0::Rbf(rbf) => rbf.fees.amount(),
347 }
348 }
349}
350
351impl std::fmt::Display for WalletOutputV0 {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 match self {
354 WalletOutputV0::PegOut(pegout) => {
355 write!(
356 f,
357 "Wallet PegOut {} to {}",
358 pegout.amount,
359 pegout.recipient.clone().assume_checked()
360 )
361 }
362 WalletOutputV0::Rbf(rbf) => write!(f, "Wallet RBF {:?} to {}", rbf.fees, rbf.txid),
363 }
364 }
365}
366
367pub struct WalletModuleTypes;
368
369pub fn proprietary_tweak_key() -> ProprietaryKey {
370 ProprietaryKey {
371 prefix: b"fedimint".to_vec(),
372 subtype: 0x00,
373 key: vec![],
374 }
375}
376
377impl std::hash::Hash for PegOutSignatureItem {
378 fn hash<H: Hasher>(&self, state: &mut H) {
379 self.txid.hash(state);
380 for sig in &self.signature {
381 sig.serialize_der().hash(state);
382 }
383 }
384}
385
386impl PartialEq for PegOutSignatureItem {
387 fn eq(&self, other: &PegOutSignatureItem) -> bool {
388 self.txid == other.txid && self.signature == other.signature
389 }
390}
391
392impl Eq for PegOutSignatureItem {}
393
394plugin_types_trait_impl_common!(
395 KIND,
396 WalletModuleTypes,
397 WalletClientConfig,
398 WalletInput,
399 WalletOutput,
400 WalletOutputOutcome,
401 WalletConsensusItem,
402 WalletInputError,
403 WalletOutputError
404);
405
406#[derive(Debug, Error, Encodable, Decodable, Hash, Clone, Eq, PartialEq)]
407pub enum WalletCreationError {
408 #[error("Connected bitcoind is on wrong network, expected {0}, got {1}")]
409 WrongNetwork(Network, Network),
410 #[error("Error querying bitcoind: {0}")]
411 RpcError(String),
412}
413
414#[derive(Debug, Error, Encodable, Decodable, Hash, Clone, Eq, PartialEq)]
415pub enum WalletInputError {
416 #[error("Unknown block hash in peg-in proof: {0}")]
417 UnknownPegInProofBlock(BlockHash),
418 #[error("Invalid peg-in proof: {0}")]
419 PegInProofError(#[from] PegInProofError),
420 #[error("The peg-in was already claimed")]
421 PegInAlreadyClaimed,
422 #[error("The wallet input version is not supported by this federation")]
423 UnknownInputVariant(#[from] UnknownWalletInputVariantError),
424}
425
426#[derive(Debug, Error, Encodable, Decodable, Hash, Clone, Eq, PartialEq)]
427pub enum WalletOutputError {
428 #[error("Connected bitcoind is on wrong network, expected {0}, got {1}")]
429 WrongNetwork(Network, Network),
430 #[error("Peg-out fee rate {0:?} is set below consensus {1:?}")]
431 PegOutFeeBelowConsensus(Feerate, Feerate),
432 #[error("Not enough SpendableUTXO")]
433 NotEnoughSpendableUTXO,
434 #[error("Peg out amount was under the dust limit")]
435 PegOutUnderDustLimit,
436 #[error("RBF transaction id not found")]
437 RbfTransactionIdNotFound,
438 #[error("Peg-out fee weight {0} doesn't match actual weight {1}")]
439 TxWeightIncorrect(u64, u64),
440 #[error("Peg-out fee rate is below min relay fee")]
441 BelowMinRelayFee,
442 #[error("The wallet output version is not supported by this federation")]
443 UnknownOutputVariant(#[from] UnknownWalletOutputVariantError),
444}
445
446pub const DEPRECATED_RBF_ERROR: WalletOutputError =
450 WalletOutputError::UnknownOutputVariant(UnknownWalletOutputVariantError { variant: 1 });
451
452#[derive(Debug, Error)]
453pub enum ProcessPegOutSigError {
454 #[error("No unsigned transaction with id {0} exists")]
455 UnknownTransaction(Txid),
456 #[error("Expected {0} signatures, got {1}")]
457 WrongSignatureCount(usize, usize),
458 #[error("Bad Sighash")]
459 SighashError,
460 #[error("Malformed signature: {0}")]
461 MalformedSignature(secp256k1::Error),
462 #[error("Invalid signature")]
463 InvalidSignature,
464 #[error("Duplicate signature")]
465 DuplicateSignature,
466 #[error("Missing change tweak")]
467 MissingOrMalformedChangeTweak,
468 #[error("Error finalizing PSBT {0:?}")]
469 ErrorFinalizingPsbt(Vec<miniscript::psbt::Error>),
470}