1#![deny(clippy::pedantic)]
2#![allow(clippy::similar_names)]
3#![allow(clippy::cast_possible_truncation)]
4#![allow(clippy::cast_possible_wrap)]
5#![allow(clippy::default_trait_access)]
6#![allow(clippy::missing_errors_doc)]
7#![allow(clippy::missing_panics_doc)]
8#![allow(clippy::module_name_repetitions)]
9#![allow(clippy::must_use_candidate)]
10#![allow(clippy::single_match_else)]
11#![allow(clippy::too_many_lines)]
12
13pub mod db;
14pub mod envs;
15mod metrics;
16mod pending;
17
18use std::collections::{BTreeMap, BTreeSet};
19
20use anyhow::{Context, anyhow, bail, ensure};
21use bitcoin::absolute::LockTime;
22use bitcoin::hashes::{Hash, sha256};
23use bitcoin::secp256k1::Secp256k1;
24use bitcoin::sighash::{EcdsaSighashType, SighashCache};
25use bitcoin::transaction::Version;
26use bitcoin::{Amount, Network, Sequence, Transaction, TxIn, TxOut, Txid};
27use common::config::WalletConfigConsensus;
28use common::{
29 OutputInfo, WalletCommonInit, WalletConsensusItem, WalletInput, WalletModuleTypes,
30 WalletOutput, WalletOutputOutcome,
31};
32use db::{
33 DbKeyPrefix, FederationWalletKey, FederationWalletPrefix, Output, OutputKey, OutputPrefix,
34 SignaturesKey, SignaturesPrefix, SignaturesTxidPrefix, SpentOutputKey, SpentOutputPrefix,
35 TxInfoIndexKey, TxInfoIndexPrefix,
36};
37use envs::FM_WALLETV2_DISABLE_MEMPOOL_SCAN_ENV;
38use fedimint_core::config::{
39 ServerModuleConfig, ServerModuleConsensusConfig, TypedServerModuleConfig,
40 TypedServerModuleConsensusConfig,
41};
42use fedimint_core::core::ModuleInstanceId;
43use fedimint_core::db::{
44 Database, DatabaseTransaction, DatabaseVersion, IDatabaseTransactionOpsCoreTyped,
45};
46use fedimint_core::encoding::{Decodable, Encodable};
47use fedimint_core::envs::{
48 FM_ENABLE_MODULE_WALLETV2_ENV, is_env_var_set_opt, is_running_in_test_env,
49};
50use fedimint_core::module::audit::Audit;
51use fedimint_core::module::{
52 Amounts, ApiEndpoint, ApiVersion, CoreConsensusVersion, InputMeta, ModuleConsensusVersion,
53 ModuleInit, MultiApiVersion, TransactionItemAmounts, public_api_endpoint,
54};
55#[cfg(not(target_family = "wasm"))]
56use fedimint_core::task::TaskGroup;
57use fedimint_core::task::sleep;
58use fedimint_core::util::FmtCompact as _;
59use fedimint_core::{
60 InPoint, NumPeersExt, OutPoint, PeerId, apply, async_trait_maybe_send, push_db_pair_items, util,
61};
62use fedimint_logging::LOG_MODULE_WALLETV2;
63use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
64use fedimint_server_core::config::{PeerHandleOps, PeerHandleOpsExt};
65use fedimint_server_core::migration::ServerModuleDbMigrationFn;
66use fedimint_server_core::{
67 ConfigGenModuleArgs, EnvVarDoc, ServerModule, ServerModuleInit, ServerModuleInitArgs,
68};
69pub use fedimint_walletv2_common as common;
70use fedimint_walletv2_common::config::{
71 FeeConsensus, WalletClientConfig, WalletConfig, WalletConfigPrivate,
72};
73use fedimint_walletv2_common::endpoint_constants::{
74 CONSENSUS_BLOCK_COUNT_ENDPOINT, CONSENSUS_FEERATE_ENDPOINT, FEDERATION_WALLET_ENDPOINT,
75 OUTPUT_INFO_SLICE_ENDPOINT, PENDING_OUTPUTS_ENDPOINT, PENDING_TRANSACTION_CHAIN_ENDPOINT,
76 RECEIVE_FEE_ENDPOINT, SEND_FEE_ENDPOINT, TRANSACTION_CHAIN_ENDPOINT, TRANSACTION_ID_ENDPOINT,
77};
78pub use fedimint_walletv2_common::{BLOCK_COUNT_VOTE_DELAY, CONFIRMATION_FINALITY_DELAY};
81use fedimint_walletv2_common::{
82 FederationWallet, MODULE_CONSENSUS_VERSION, PendingOutputs, TxInfo, WalletInputError,
83 WalletOutputError, descriptor, is_potential_receive, tweak_public_key,
84};
85use futures::StreamExt;
86use miniscript::descriptor::Wsh;
87use rand::rngs::OsRng;
88use secp256k1::ecdsa::Signature;
89use secp256k1::{PublicKey, Scalar, SecretKey};
90use serde::{Deserialize, Serialize};
91use strum::IntoEnumIterator;
92use tokio::sync::watch;
93use tracing::{debug, info};
94
95use crate::db::{
96 BlockCountVoteKey, BlockCountVotePrefix, FeeRateVoteKey, FeeRateVotePrefix, TxInfoKey,
97 TxInfoPrefix, UnconfirmedTxKey, UnconfirmedTxPrefix, UnsignedTxKey, UnsignedTxPrefix,
98};
99use crate::metrics::{
100 WALLET_BLOCK_COUNT, WALLET_INOUT_FEES_SATS, WALLET_INOUT_SATS, WALLET_PEGIN_FEES_SATS,
101 WALLET_PEGIN_SATS, WALLET_PEGOUT_FEES_SATS, WALLET_PEGOUT_SATS,
102};
103
104const MAX_BLOCK_COUNT_INCREMENT: u64 = 5;
107
108const TEST_MAX_BLOCK_COUNT_INCREMENT: u64 = 100;
111
112const MIN_FEERATE_VOTE_SATS_PER_KVB: u64 = 1000;
115
116#[derive(Clone, Debug, Eq, PartialEq, Serialize, Encodable, Decodable)]
117pub struct FederationTx {
118 pub tx: Transaction,
119 pub spent_tx_outs: Vec<SpentTxOut>,
120 pub vbytes: u64,
121 pub fee: Amount,
122}
123
124#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
125pub struct SpentTxOut {
126 pub value: Amount,
127 pub tweak: sha256::Hash,
128}
129
130async fn pending_txs_unordered(dbtx: &mut DatabaseTransaction<'_>) -> Vec<FederationTx> {
131 let unsigned: Vec<FederationTx> = dbtx
132 .find_by_prefix(&UnsignedTxPrefix)
133 .await
134 .map(|entry| entry.1)
135 .collect()
136 .await;
137
138 let unconfirmed: Vec<FederationTx> = dbtx
139 .find_by_prefix(&UnconfirmedTxPrefix)
140 .await
141 .map(|entry| entry.1)
142 .collect()
143 .await;
144
145 unsigned.into_iter().chain(unconfirmed).collect()
146}
147
148#[derive(Debug, Clone)]
149pub struct WalletInit;
150
151impl ModuleInit for WalletInit {
152 type Common = WalletCommonInit;
153
154 async fn dump_database(
155 &self,
156 dbtx: &mut DatabaseTransaction<'_>,
157 prefix_names: Vec<String>,
158 ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
159 let mut wallet: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
160
161 let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
162 prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
163 });
164
165 for table in filtered_prefixes {
166 match table {
167 DbKeyPrefix::Output => {
168 push_db_pair_items!(
169 dbtx,
170 OutputPrefix,
171 OutputKey,
172 Output,
173 wallet,
174 "Wallet Outputs"
175 );
176 }
177 DbKeyPrefix::SpentOutput => {
178 push_db_pair_items!(
179 dbtx,
180 SpentOutputPrefix,
181 SpentOutputKey,
182 (),
183 wallet,
184 "Wallet Spent Outputs"
185 );
186 }
187 DbKeyPrefix::BlockCountVote => {
188 push_db_pair_items!(
189 dbtx,
190 BlockCountVotePrefix,
191 BlockCountVoteKey,
192 u64,
193 wallet,
194 "Wallet Block Count Votes"
195 );
196 }
197 DbKeyPrefix::FeeRateVote => {
198 push_db_pair_items!(
199 dbtx,
200 FeeRateVotePrefix,
201 FeeRateVoteKey,
202 Option<u64>,
203 wallet,
204 "Wallet Fee Rate Votes"
205 );
206 }
207 DbKeyPrefix::TxLog => {
208 push_db_pair_items!(
209 dbtx,
210 TxInfoPrefix,
211 TxInfoKey,
212 TxInfo,
213 wallet,
214 "Wallet Tx Log"
215 );
216 }
217 DbKeyPrefix::TxInfoIndex => {
218 push_db_pair_items!(
219 dbtx,
220 TxInfoIndexPrefix,
221 TxInfoIndexKey,
222 u64,
223 wallet,
224 "Wallet Tx Info Index"
225 );
226 }
227 DbKeyPrefix::UnsignedTx => {
228 push_db_pair_items!(
229 dbtx,
230 UnsignedTxPrefix,
231 UnsignedTxKey,
232 FederationTx,
233 wallet,
234 "Wallet Unsigned Transactions"
235 );
236 }
237 DbKeyPrefix::Signatures => {
238 push_db_pair_items!(
239 dbtx,
240 SignaturesPrefix,
241 SignaturesKey,
242 Vec<Signature>,
243 wallet,
244 "Wallet Signatures"
245 );
246 }
247 DbKeyPrefix::UnconfirmedTx => {
248 push_db_pair_items!(
249 dbtx,
250 UnconfirmedTxPrefix,
251 UnconfirmedTxKey,
252 FederationTx,
253 wallet,
254 "Wallet Unconfirmed Transactions"
255 );
256 }
257 DbKeyPrefix::FederationWallet => {
258 push_db_pair_items!(
259 dbtx,
260 FederationWalletPrefix,
261 FederationWalletKey,
262 FederationWallet,
263 wallet,
264 "Federation Wallet"
265 );
266 }
267 }
268 }
269
270 Box::new(wallet.into_iter())
271 }
272}
273
274#[apply(async_trait_maybe_send!)]
275impl ServerModuleInit for WalletInit {
276 type Module = Wallet;
277
278 fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
279 &[MODULE_CONSENSUS_VERSION]
280 }
281
282 fn is_enabled_by_default(&self) -> bool {
283 is_env_var_set_opt(FM_ENABLE_MODULE_WALLETV2_ENV).unwrap_or(true)
284 }
285
286 fn get_documented_env_vars(&self) -> Vec<EnvVarDoc> {
287 vec![
288 EnvVarDoc {
289 name: FM_ENABLE_MODULE_WALLETV2_ENV,
290 description: "Set to 0/false to disable the WalletV2 module. Enabled by default.",
291 },
292 EnvVarDoc {
293 name: FM_WALLETV2_DISABLE_MEMPOOL_SCAN_ENV,
294 description: "Set to 1/true to stop scanning the mempool for pending receives. \
295 Peg-in progress is then only reported once a deposit is mined, as \
296 it already is on an esplora backend. Enabled by default.",
297 },
298 ]
299 }
300
301 async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
302 for direction in ["incoming", "outgoing"] {
304 WALLET_INOUT_SATS
305 .with_label_values(&[direction])
306 .get_sample_count();
307 WALLET_INOUT_FEES_SATS
308 .with_label_values(&[direction])
309 .get_sample_count();
310 }
311 WALLET_PEGIN_SATS.get_sample_count();
312 WALLET_PEGIN_FEES_SATS.get_sample_count();
313 WALLET_PEGOUT_SATS.get_sample_count();
314 WALLET_PEGOUT_FEES_SATS.get_sample_count();
315
316 Ok(Wallet::new(
317 args.cfg().to_typed()?,
318 args.db(),
319 args.task_group(),
320 args.server_bitcoin_rpc_monitor(),
321 ))
322 }
323
324 fn trusted_dealer_gen(
325 &self,
326 peers: &[PeerId],
327 args: &ConfigGenModuleArgs,
328 ) -> BTreeMap<PeerId, ServerModuleConfig> {
329 let fee_consensus = FeeConsensus::new(0).expect("Relative fee is within range");
330
331 let bitcoin_sks = peers
332 .iter()
333 .map(|peer| (*peer, SecretKey::new(&mut secp256k1::rand::thread_rng())))
334 .collect::<BTreeMap<PeerId, SecretKey>>();
335
336 let bitcoin_pks = bitcoin_sks
337 .iter()
338 .map(|(peer, sk)| (*peer, sk.public_key(secp256k1::SECP256K1)))
339 .collect::<BTreeMap<PeerId, PublicKey>>();
340
341 bitcoin_sks
342 .into_iter()
343 .map(|(peer, bitcoin_sk)| {
344 let config = WalletConfig {
345 private: WalletConfigPrivate { bitcoin_sk },
346 consensus: WalletConfigConsensus::new(
347 bitcoin_pks.clone(),
348 fee_consensus.clone(),
349 args.network,
350 ),
351 };
352
353 (peer, config.to_erased())
354 })
355 .collect()
356 }
357
358 async fn distributed_gen(
359 &self,
360 peers: &(dyn PeerHandleOps + Send + Sync),
361 args: &ConfigGenModuleArgs,
362 ) -> anyhow::Result<ServerModuleConfig> {
363 let fee_consensus = FeeConsensus::new(0).expect("Relative fee is within range");
364
365 let (bitcoin_sk, bitcoin_pk) = secp256k1::generate_keypair(&mut OsRng);
366
367 let bitcoin_pks: BTreeMap<PeerId, PublicKey> = peers
368 .exchange_encodable(bitcoin_pk)
369 .await?
370 .into_iter()
371 .collect();
372
373 let config = WalletConfig {
374 private: WalletConfigPrivate { bitcoin_sk },
375 consensus: WalletConfigConsensus::new(bitcoin_pks, fee_consensus, args.network),
376 };
377
378 Ok(config.to_erased())
379 }
380
381 fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
382 let config = config.to_typed::<WalletConfig>()?;
383
384 ensure!(
385 config
386 .consensus
387 .bitcoin_pks
388 .get(identity)
389 .ok_or(anyhow::anyhow!("No public key for our identity"))?
390 == &config.private.bitcoin_sk.public_key(secp256k1::SECP256K1),
391 "Bitcoin wallet private key doesn't match multisig pubkey"
392 );
393
394 Ok(())
395 }
396
397 fn get_client_config(
398 &self,
399 config: &ServerModuleConsensusConfig,
400 ) -> anyhow::Result<WalletClientConfig> {
401 let config = WalletConfigConsensus::from_erased(config)?;
402
403 Ok(WalletClientConfig {
404 bitcoin_pks: config.bitcoin_pks,
405 descriptor: config.descriptor,
406 send_tx_vbytes: config.send_tx_vbytes,
407 receive_tx_vbytes: config.receive_tx_vbytes,
408 feerate_base: config.feerate_base,
409 dust_limit: config.dust_limit,
410 fee_consensus: config.fee_consensus,
411 network: config.network,
412 })
413 }
414
415 fn get_database_migrations(
416 &self,
417 ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Wallet>> {
418 BTreeMap::new()
419 }
420
421 fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
422 Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
423 }
424}
425
426#[apply(async_trait_maybe_send!)]
427impl ServerModule for Wallet {
428 type Common = WalletModuleTypes;
429 type Init = WalletInit;
430
431 async fn consensus_proposal<'a>(
432 &'a self,
433 dbtx: &mut DatabaseTransaction<'_>,
434 ) -> Vec<WalletConsensusItem> {
435 let mut items = dbtx
436 .find_by_prefix(&UnsignedTxPrefix)
437 .await
438 .map(|(key, unsigned_tx)| {
439 let signatures = self.sign_tx(&unsigned_tx);
440
441 self.verify_signatures(
442 &unsigned_tx,
443 &signatures,
444 self.cfg.private.bitcoin_sk.public_key(secp256k1::SECP256K1),
445 )
446 .expect("Our signatures failed verification against our private key");
447
448 WalletConsensusItem::Signatures(key.0, signatures)
449 })
450 .collect::<Vec<WalletConsensusItem>>()
451 .await;
452
453 if let Some(status) = self.btc_rpc.status() {
454 assert_eq!(status.network, self.cfg.consensus.network);
455
456 let block_count_vote = status.block_count.saturating_sub(BLOCK_COUNT_VOTE_DELAY);
457
458 let consensus_block_count = self.consensus_block_count(dbtx).await;
459
460 let max_block_count_increment = if is_running_in_test_env() {
461 TEST_MAX_BLOCK_COUNT_INCREMENT
462 } else {
463 MAX_BLOCK_COUNT_INCREMENT
464 };
465
466 let block_count_vote = match consensus_block_count {
467 0 => block_count_vote,
468 _ => block_count_vote.min(consensus_block_count + max_block_count_increment),
469 };
470
471 WALLET_BLOCK_COUNT.set(i64::try_from(block_count_vote).unwrap_or(i64::MAX));
472
473 items.push(WalletConsensusItem::BlockCount(block_count_vote));
474
475 let feerate_vote = status
478 .fee_rate
479 .map(|fee_rate| fee_rate.sats_per_kvb.max(MIN_FEERATE_VOTE_SATS_PER_KVB));
480
481 items.push(WalletConsensusItem::Feerate(feerate_vote));
482 } else {
483 items.push(WalletConsensusItem::Feerate(None));
485 }
486
487 items
488 }
489
490 async fn process_consensus_item<'a, 'b>(
491 &'a self,
492 dbtx: &mut DatabaseTransaction<'b>,
493 consensus_item: WalletConsensusItem,
494 peer: PeerId,
495 ) -> anyhow::Result<()> {
496 match consensus_item {
497 WalletConsensusItem::BlockCount(block_count_vote) => {
498 self.process_block_count(dbtx, block_count_vote, peer).await
499 }
500 WalletConsensusItem::Feerate(feerate) => {
501 if Some(feerate) == dbtx.insert_entry(&FeeRateVoteKey(peer), &feerate).await {
502 return Err(anyhow!("Fee rate vote is redundant"));
503 }
504
505 Ok(())
506 }
507 WalletConsensusItem::Signatures(txid, signatures) => {
508 self.process_signatures(dbtx, txid, signatures, peer).await
509 }
510 WalletConsensusItem::Default { variant, .. } => Err(anyhow!(
511 "Received wallet consensus item with unknown variant {variant}"
512 )),
513 }
514 }
515
516 async fn process_input<'a, 'b, 'c>(
517 &'a self,
518 dbtx: &mut DatabaseTransaction<'c>,
519 input: &'b WalletInput,
520 _in_point: InPoint,
521 ) -> Result<InputMeta, WalletInputError> {
522 let input = input.ensure_v0_ref()?;
523
524 if dbtx
525 .insert_entry(&SpentOutputKey(input.output_index), &())
526 .await
527 .is_some()
528 {
529 return Err(WalletInputError::OutputAlreadySpent);
530 }
531
532 let Output(tracked_outpoint, tracked_output) = dbtx
533 .get_value(&OutputKey(input.output_index))
534 .await
535 .ok_or(WalletInputError::UnknownOutputIndex)?;
536
537 let tweaked_pubkey = self
538 .descriptor(&input.tweak.consensus_hash())
539 .script_pubkey();
540
541 if tracked_output.script_pubkey != tweaked_pubkey {
542 return Err(WalletInputError::WrongTweak);
543 }
544
545 let consensus_receive_fee = self
546 .receive_fee(dbtx)
547 .await
548 .ok_or(WalletInputError::NoConsensusFeerateAvailable)?;
549
550 if input.fee < consensus_receive_fee {
555 return Err(WalletInputError::InsufficientTotalFee);
556 }
557
558 let output_value = tracked_output
559 .value
560 .checked_sub(input.fee)
561 .ok_or(WalletInputError::ArithmeticOverflow)?;
562
563 if let Some(wallet) = dbtx.remove_entry(&FederationWalletKey).await {
564 let change_value = wallet
568 .value
569 .checked_add(output_value)
570 .ok_or(WalletInputError::ArithmeticOverflow)?;
571
572 let tx = Transaction {
573 version: Version(2),
574 lock_time: LockTime::ZERO,
575 input: vec![
576 TxIn {
577 previous_output: wallet.outpoint,
578 script_sig: Default::default(),
579 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
580 witness: bitcoin::Witness::new(),
581 },
582 TxIn {
583 previous_output: tracked_outpoint,
584 script_sig: Default::default(),
585 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
586 witness: bitcoin::Witness::new(),
587 },
588 ],
589 output: vec![TxOut {
590 value: change_value,
591 script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
592 }],
593 };
594
595 dbtx.insert_new_entry(
596 &FederationWalletKey,
597 &FederationWallet {
598 value: change_value,
599 outpoint: bitcoin::OutPoint {
600 txid: tx.compute_txid(),
601 vout: 0,
602 },
603 tweak: wallet.consensus_hash(),
604 },
605 )
606 .await;
607
608 let tx_index = self.total_txs(dbtx).await;
609
610 let created = self.consensus_block_count(dbtx).await;
611
612 dbtx.insert_new_entry(
613 &TxInfoKey(tx_index),
614 &TxInfo {
615 index: tx_index,
616 txid: tx.compute_txid(),
617 input: wallet.value,
618 output: change_value,
619 vbytes: self.cfg.consensus.receive_tx_vbytes,
620 fee: input.fee,
621 created,
622 },
623 )
624 .await;
625
626 dbtx.insert_new_entry(
627 &UnsignedTxKey(tx.compute_txid()),
628 &FederationTx {
629 tx,
630 spent_tx_outs: vec![
631 SpentTxOut {
632 value: wallet.value,
633 tweak: wallet.tweak,
634 },
635 SpentTxOut {
636 value: tracked_output.value,
637 tweak: input.tweak.consensus_hash(),
638 },
639 ],
640 vbytes: self.cfg.consensus.receive_tx_vbytes,
641 fee: input.fee,
642 },
643 )
644 .await;
645 } else {
646 dbtx.insert_new_entry(
647 &FederationWalletKey,
648 &FederationWallet {
649 value: tracked_output.value,
650 outpoint: tracked_outpoint,
651 tweak: input.tweak.consensus_hash(),
652 },
653 )
654 .await;
655 }
656
657 let amount = output_value
658 .to_sat()
659 .checked_mul(1000)
660 .map(fedimint_core::Amount::from_msats)
661 .ok_or(WalletInputError::ArithmeticOverflow)?;
662
663 let fee = self.cfg.consensus.fee_consensus.fee(amount);
664
665 calculate_pegin_metrics(dbtx, amount, fee);
666
667 Ok(InputMeta {
668 amount: TransactionItemAmounts {
669 amounts: Amounts::new_bitcoin(amount),
670 fees: Amounts::new_bitcoin(fee),
671 },
672 pub_key: input.tweak,
673 })
674 }
675
676 async fn process_output<'a, 'b>(
677 &'a self,
678 dbtx: &mut DatabaseTransaction<'b>,
679 output: &'a WalletOutput,
680 outpoint: OutPoint,
681 ) -> Result<TransactionItemAmounts, WalletOutputError> {
682 let output = output.ensure_v0_ref()?;
683
684 if output.value < self.cfg.consensus.dust_limit {
685 return Err(WalletOutputError::UnderDustLimit);
686 }
687
688 let wallet = dbtx
689 .remove_entry(&FederationWalletKey)
690 .await
691 .ok_or(WalletOutputError::NoFederationUTXO)?;
692
693 let consensus_send_fee = self
694 .send_fee(dbtx)
695 .await
696 .ok_or(WalletOutputError::NoConsensusFeerateAvailable)?;
697
698 if output.fee < consensus_send_fee {
703 return Err(WalletOutputError::InsufficientTotalFee);
704 }
705
706 let output_value = output
707 .value
708 .checked_add(output.fee)
709 .ok_or(WalletOutputError::ArithmeticOverflow)?;
710
711 let change_value = wallet
712 .value
713 .checked_sub(output_value)
714 .ok_or(WalletOutputError::ArithmeticOverflow)?;
715
716 if change_value < self.cfg.consensus.dust_limit {
717 return Err(WalletOutputError::ChangeUnderDustLimit);
718 }
719
720 let script_pubkey = output
721 .destination
722 .script_pubkey()
723 .ok_or(WalletOutputError::UnknownScriptVariant)?;
724
725 let tx = Transaction {
726 version: Version(2),
727 lock_time: LockTime::ZERO,
728 input: vec![TxIn {
729 previous_output: wallet.outpoint,
730 script_sig: Default::default(),
731 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
732 witness: bitcoin::Witness::new(),
733 }],
734 output: vec![
735 TxOut {
736 value: change_value,
737 script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
738 },
739 TxOut {
740 value: output.value,
741 script_pubkey,
742 },
743 ],
744 };
745
746 dbtx.insert_new_entry(
747 &FederationWalletKey,
748 &FederationWallet {
749 value: change_value,
750 outpoint: bitcoin::OutPoint {
751 txid: tx.compute_txid(),
752 vout: 0,
753 },
754 tweak: wallet.consensus_hash(),
755 },
756 )
757 .await;
758
759 let tx_index = self.total_txs(dbtx).await;
760
761 let created = self.consensus_block_count(dbtx).await;
762
763 dbtx.insert_new_entry(
764 &TxInfoKey(tx_index),
765 &TxInfo {
766 index: tx_index,
767 txid: tx.compute_txid(),
768 input: wallet.value,
769 output: change_value,
770 vbytes: self.cfg.consensus.send_tx_vbytes,
771 fee: output.fee,
772 created,
773 },
774 )
775 .await;
776
777 dbtx.insert_new_entry(&TxInfoIndexKey(outpoint), &tx_index)
778 .await;
779
780 dbtx.insert_new_entry(
781 &UnsignedTxKey(tx.compute_txid()),
782 &FederationTx {
783 tx,
784 spent_tx_outs: vec![SpentTxOut {
785 value: wallet.value,
786 tweak: wallet.tweak,
787 }],
788 vbytes: self.cfg.consensus.send_tx_vbytes,
789 fee: output.fee,
790 },
791 )
792 .await;
793
794 let amount = output_value
795 .to_sat()
796 .checked_mul(1000)
797 .map(fedimint_core::Amount::from_msats)
798 .ok_or(WalletOutputError::ArithmeticOverflow)?;
799
800 let fee = self.cfg.consensus.fee_consensus.fee(amount);
801
802 calculate_pegout_metrics(dbtx, amount, fee);
803
804 Ok(TransactionItemAmounts {
805 amounts: Amounts::new_bitcoin(amount),
806 fees: Amounts::new_bitcoin(fee),
807 })
808 }
809
810 async fn output_status(
811 &self,
812 _dbtx: &mut DatabaseTransaction<'_>,
813 _outpoint: OutPoint,
814 ) -> Option<WalletOutputOutcome> {
815 None
816 }
817
818 async fn audit(
819 &self,
820 dbtx: &mut DatabaseTransaction<'_>,
821 audit: &mut Audit,
822 module_instance_id: ModuleInstanceId,
823 ) {
824 audit
825 .add_items(
826 dbtx,
827 module_instance_id,
828 &FederationWalletPrefix,
829 |_, wallet| 1000 * wallet.value.to_sat() as i64,
830 )
831 .await;
832 }
833
834 fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
835 vec![
836 public_api_endpoint! {
837 CONSENSUS_BLOCK_COUNT_ENDPOINT,
838 ApiVersion::new(0, 0),
839 async |module: &Wallet, context, _params: ()| -> u64 {
840 let db = context.db();
841 let mut dbtx = db.begin_transaction_nc().await;
842 Ok(module.consensus_block_count(&mut dbtx).await)
843 }
844 },
845 public_api_endpoint! {
846 CONSENSUS_FEERATE_ENDPOINT,
847 ApiVersion::new(0, 0),
848 async |module: &Wallet, context, _params: ()| -> Option<u64> {
849 let db = context.db();
850 let mut dbtx = db.begin_transaction_nc().await;
851 Ok(module.consensus_feerate(&mut dbtx).await)
852 }
853 },
854 public_api_endpoint! {
855 FEDERATION_WALLET_ENDPOINT,
856 ApiVersion::new(0, 0),
857 async |_module: &Wallet, context, _params: ()| -> Option<FederationWallet> {
858 let db = context.db();
859 let mut dbtx = db.begin_transaction_nc().await;
860 Ok(dbtx.get_value(&FederationWalletKey).await)
861 }
862 },
863 public_api_endpoint! {
864 SEND_FEE_ENDPOINT,
865 ApiVersion::new(0, 0),
866 async |module: &Wallet, context, _params: ()| -> Option<Amount> {
867 let db = context.db();
868 let mut dbtx = db.begin_transaction_nc().await;
869 Ok(module.send_fee(&mut dbtx).await)
870 }
871 },
872 public_api_endpoint! {
873 RECEIVE_FEE_ENDPOINT,
874 ApiVersion::new(0, 0),
875 async |module: &Wallet, context, _params: ()| -> Option<Amount> {
876 let db = context.db();
877 let mut dbtx = db.begin_transaction_nc().await;
878 Ok(module.receive_fee(&mut dbtx).await)
879 }
880 },
881 public_api_endpoint! {
882 TRANSACTION_ID_ENDPOINT,
883 ApiVersion::new(0, 0),
884 async |module: &Wallet, context, params: OutPoint| -> Option<Txid> {
885 let db = context.db();
886 let mut dbtx = db.begin_transaction_nc().await;
887 Ok(module.tx_id(&mut dbtx, params).await)
888 }
889 },
890 public_api_endpoint! {
891 OUTPUT_INFO_SLICE_ENDPOINT,
892 ApiVersion::new(0, 0),
893 async |module: &Wallet, context, params: (u64, u64)| -> Vec<OutputInfo> {
894 let db = context.db();
895 let mut dbtx = db.begin_transaction_nc().await;
896 Ok(module.get_outputs(&mut dbtx, params.0, params.1).await)
897 }
898 },
899 public_api_endpoint! {
900 PENDING_TRANSACTION_CHAIN_ENDPOINT,
901 ApiVersion::new(0, 0),
902 async |module: &Wallet, context, _params: ()| -> Vec<TxInfo> {
903 let db = context.db();
904 let mut dbtx = db.begin_transaction_nc().await;
905 Ok(module.pending_tx_chain(&mut dbtx).await)
906 }
907 },
908 public_api_endpoint! {
909 TRANSACTION_CHAIN_ENDPOINT,
910 ApiVersion::new(0, 0),
911 async |module: &Wallet, context, _params: ()| -> Vec<TxInfo> {
912 let db = context.db();
913 let mut dbtx = db.begin_transaction_nc().await;
914 Ok(module.tx_chain(&mut dbtx).await)
915 }
916 },
917 public_api_endpoint! {
918 PENDING_OUTPUTS_ENDPOINT,
919 ApiVersion::new(0, 2),
920 async |module: &Wallet, _context, _params: ()| -> PendingOutputs {
921 Ok(module.pending_outputs.borrow().clone())
925 }
926 },
927 ]
928 }
929
930 fn supported_api_versions(&self) -> MultiApiVersion {
931 MultiApiVersion::try_from_iter([ApiVersion::new(0, 2)])
932 .expect("walletv2 declares one API version per major version")
933 }
934}
935
936#[derive(Debug)]
937pub struct Wallet {
938 cfg: WalletConfig,
939 db: Database,
940 btc_rpc: ServerBitcoinRpcMonitor,
941 pending_outputs: watch::Receiver<PendingOutputs>,
952}
953
954impl Wallet {
955 fn new(
956 cfg: WalletConfig,
957 db: &Database,
958 task_group: &TaskGroup,
959 btc_rpc: ServerBitcoinRpcMonitor,
960 ) -> Wallet {
961 Self::spawn_broadcast_unconfirmed_txs_task(btc_rpc.clone(), db.clone(), task_group);
962
963 let (pending_sender, pending_outputs) = watch::channel(PendingOutputs::default());
964
965 pending::spawn_pending_receive_scanner(
966 btc_rpc.clone(),
967 cfg.consensus.bitcoin_pks.consensus_hash(),
968 pending_sender,
969 task_group,
970 );
971
972 Wallet {
973 cfg,
974 btc_rpc,
975 db: db.clone(),
976 pending_outputs,
977 }
978 }
979
980 fn spawn_broadcast_unconfirmed_txs_task(
981 btc_rpc: ServerBitcoinRpcMonitor,
982 db: Database,
983 task_group: &TaskGroup,
984 ) {
985 task_group.spawn_cancellable("broadcast_unconfirmed_transactions", async move {
986 loop {
987 let unconfirmed_txs = db
988 .begin_transaction_nc()
989 .await
990 .find_by_prefix(&UnconfirmedTxPrefix)
991 .await
992 .map(|entry| entry.1)
993 .collect::<Vec<FederationTx>>()
994 .await;
995
996 for unconfirmed_tx in unconfirmed_txs {
997 if let Err(err) = btc_rpc.submit_transaction(unconfirmed_tx.tx).await {
998 debug!(
999 target: LOG_MODULE_WALLETV2,
1000 err = %err.fmt_compact(),
1001 "Error broadcasting unconfirmed transaction"
1002 );
1003 }
1004 }
1005
1006 sleep(common::sleep_duration()).await;
1007 }
1008 });
1009 }
1010
1011 async fn process_block_count(
1012 &self,
1013 dbtx: &mut DatabaseTransaction<'_>,
1014 block_count_vote: u64,
1015 peer: PeerId,
1016 ) -> anyhow::Result<()> {
1017 let old_consensus_block_count = self.consensus_block_count(dbtx).await;
1018
1019 let current_vote = dbtx
1020 .insert_entry(&BlockCountVoteKey(peer), &block_count_vote)
1021 .await
1022 .unwrap_or(0);
1023
1024 ensure!(
1025 current_vote < block_count_vote,
1026 "Block count vote is redundant"
1027 );
1028
1029 let new_consensus_block_count = self.consensus_block_count(dbtx).await;
1030
1031 assert!(old_consensus_block_count <= new_consensus_block_count);
1032
1033 debug!(
1034 target: LOG_MODULE_WALLETV2,
1035 %peer,
1036 vote = block_count_vote,
1037 old_consensus = old_consensus_block_count,
1038 new_consensus = new_consensus_block_count,
1039 advanced = new_consensus_block_count - old_consensus_block_count,
1040 "Processed block count vote"
1041 );
1042
1043 let scan_from_genesis = self.cfg.consensus.network == bitcoin::Network::Regtest;
1048 if old_consensus_block_count == 0 && !scan_from_genesis {
1049 return Ok(());
1050 }
1051
1052 self.await_local_sync_to_block_count(
1055 new_consensus_block_count + CONFIRMATION_FINALITY_DELAY,
1056 )
1057 .await;
1058
1059 for height in old_consensus_block_count..new_consensus_block_count {
1060 if let Some(status) = self.btc_rpc.status() {
1062 assert_eq!(status.network, self.cfg.consensus.network);
1063 }
1064
1065 let block_hash = util::retry(
1066 "get_block_hash",
1067 util::backoff_util::background_backoff(),
1068 || self.btc_rpc.get_block_hash(height),
1069 )
1070 .await
1071 .expect("Bitcoind rpc to get_block_hash failed");
1072
1073 let block = util::retry(
1074 "get_block",
1075 util::backoff_util::background_backoff(),
1076 || self.btc_rpc.get_block(&block_hash),
1077 )
1078 .await
1079 .expect("Bitcoind rpc to get_block failed");
1080
1081 assert_eq!(block.block_hash(), block_hash, "Block hash mismatch");
1082
1083 let pks_hash = self.cfg.consensus.bitcoin_pks.consensus_hash();
1084
1085 let txs_num = block.txdata.len();
1086 let mut potential_receives_num: usize = 0;
1087
1088 for tx in block.txdata {
1089 dbtx.remove_entry(&UnconfirmedTxKey(tx.compute_txid()))
1090 .await;
1091
1092 for (vout, tx_out) in tx.output.iter().enumerate() {
1098 if is_potential_receive(&tx_out.script_pubkey, &pks_hash) {
1099 let outpoint = bitcoin::OutPoint {
1100 txid: tx.compute_txid(),
1101 vout: u32::try_from(vout)
1102 .expect("Bitcoin transaction has more than u32::MAX outputs"),
1103 };
1104
1105 let index = dbtx
1106 .find_by_prefix_sorted_descending(&OutputPrefix)
1107 .await
1108 .next()
1109 .await
1110 .map_or(0, |entry| entry.0.0 + 1);
1111
1112 dbtx.insert_new_entry(&OutputKey(index), &Output(outpoint, tx_out.clone()))
1113 .await;
1114
1115 debug!(
1116 target: LOG_MODULE_WALLETV2,
1117 output_index = index,
1118 %outpoint,
1119 value_sat = tx_out.value.to_sat(),
1120 height,
1121 "Recorded potential walletv2 receive"
1122 );
1123
1124 potential_receives_num += 1;
1125 }
1126 }
1127 }
1128
1129 debug!(
1130 target: LOG_MODULE_WALLETV2,
1131 height,
1132 txs_num,
1133 potential_receives_num,
1134 "Scanned block"
1135 );
1136 }
1137
1138 Ok(())
1139 }
1140
1141 async fn process_signatures(
1142 &self,
1143 dbtx: &mut DatabaseTransaction<'_>,
1144 txid: bitcoin::Txid,
1145 signatures: Vec<Signature>,
1146 peer: PeerId,
1147 ) -> anyhow::Result<()> {
1148 let mut unsigned = dbtx
1149 .get_value(&UnsignedTxKey(txid))
1150 .await
1151 .context("Unsigned transaction does not exist")?;
1152
1153 let pk = self
1154 .cfg
1155 .consensus
1156 .bitcoin_pks
1157 .get(&peer)
1158 .expect("Failed to get public key of peer from config");
1159
1160 self.verify_signatures(&unsigned, &signatures, *pk)?;
1161
1162 if dbtx
1163 .insert_entry(&SignaturesKey(txid, peer), &signatures)
1164 .await
1165 .is_some()
1166 {
1167 bail!("Already received valid signatures from this peer")
1168 }
1169
1170 let signatures = dbtx
1171 .find_by_prefix(&SignaturesTxidPrefix(txid))
1172 .await
1173 .map(|(key, signatures)| (key.1, signatures))
1174 .collect::<BTreeMap<PeerId, Vec<Signature>>>()
1175 .await;
1176
1177 if signatures.len() == self.cfg.consensus.bitcoin_pks.to_num_peers().threshold() {
1178 dbtx.remove_entry(&UnsignedTxKey(txid)).await;
1179
1180 dbtx.remove_by_prefix(&SignaturesTxidPrefix(txid)).await;
1181
1182 self.finalize_tx(&mut unsigned, &signatures);
1183
1184 dbtx.insert_new_entry(&UnconfirmedTxKey(txid), &unsigned)
1185 .await;
1186
1187 if let Err(err) = self.btc_rpc.submit_transaction(unsigned.tx).await {
1188 debug!(
1189 target: LOG_MODULE_WALLETV2,
1190 err = %err.fmt_compact(),
1191 "Error broadcasting finalized transaction"
1192 );
1193 }
1194 }
1195
1196 Ok(())
1197 }
1198
1199 async fn await_local_sync_to_block_count(&self, block_count: u64) {
1200 loop {
1201 if self
1202 .btc_rpc
1203 .status()
1204 .is_some_and(|status| status.block_count >= block_count)
1205 {
1206 break;
1207 }
1208
1209 info!(target: LOG_MODULE_WALLETV2, "Waiting for local bitcoin backend to sync to block count {block_count}");
1210
1211 sleep(common::sleep_duration()).await;
1212 }
1213 }
1214
1215 pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1216 let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1217
1218 let mut counts = dbtx
1219 .find_by_prefix(&BlockCountVotePrefix)
1220 .await
1221 .map(|entry| entry.1)
1222 .collect::<Vec<u64>>()
1223 .await;
1224
1225 assert!(counts.len() <= num_peers.total());
1226
1227 counts.sort_unstable();
1228
1229 counts.reverse();
1230
1231 assert!(counts.last() <= counts.first());
1232
1233 counts.get(num_peers.threshold() - 1).copied().unwrap_or(0)
1238 }
1239
1240 pub async fn consensus_feerate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<u64> {
1241 let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1242
1243 let mut rates = dbtx
1244 .find_by_prefix(&FeeRateVotePrefix)
1245 .await
1246 .filter_map(|entry| async move { entry.1 })
1247 .collect::<Vec<u64>>()
1248 .await;
1249
1250 assert!(rates.len() <= num_peers.total());
1251
1252 rates.sort_unstable();
1253
1254 assert!(rates.first() <= rates.last());
1255
1256 rates.get(num_peers.threshold() - 1).copied()
1257 }
1258
1259 pub async fn consensus_fee(
1260 &self,
1261 dbtx: &mut DatabaseTransaction<'_>,
1262 tx_vbytes: u64,
1263 ) -> Option<Amount> {
1264 let pending_txs = pending_txs_unordered(dbtx).await;
1268
1269 assert!(pending_txs.len() <= 32);
1270
1271 let feerate = self
1272 .consensus_feerate(dbtx)
1273 .await?
1274 .max(self.cfg.consensus.feerate_base << pending_txs.len());
1275
1276 let tx_fee = tx_vbytes.saturating_mul(feerate).saturating_div(1000);
1277
1278 let stack_vbytes = pending_txs
1279 .iter()
1280 .map(|t| t.vbytes)
1281 .try_fold(tx_vbytes, u64::checked_add)
1282 .expect("Stack vbytes overflow with at most 32 pending txs");
1283
1284 let stack_fee = stack_vbytes.saturating_mul(feerate).saturating_div(1000);
1285
1286 let stack_fee = pending_txs
1288 .iter()
1289 .map(|t| t.fee.to_sat())
1290 .fold(stack_fee, u64::saturating_sub);
1291
1292 Some(Amount::from_sat(tx_fee.max(stack_fee)))
1293 }
1294
1295 pub async fn send_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1296 self.consensus_fee(dbtx, self.cfg.consensus.send_tx_vbytes)
1297 .await
1298 }
1299
1300 pub async fn receive_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1301 self.consensus_fee(dbtx, self.cfg.consensus.receive_tx_vbytes)
1302 .await
1303 }
1304
1305 fn descriptor(&self, tweak: &sha256::Hash) -> Wsh<secp256k1::PublicKey> {
1306 descriptor(&self.cfg.consensus.bitcoin_pks, tweak)
1307 }
1308
1309 fn sign_tx(&self, unsigned_tx: &FederationTx) -> Vec<Signature> {
1310 let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1311
1312 unsigned_tx
1313 .spent_tx_outs
1314 .iter()
1315 .enumerate()
1316 .map(|(index, utxo)| {
1317 let descriptor = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1318
1319 let p2wsh_sighash = sighash_cache
1320 .p2wsh_signature_hash(index, &descriptor, utxo.value, EcdsaSighashType::All)
1321 .expect("Failed to compute P2WSH segwit sighash");
1322
1323 let scalar = &Scalar::from_be_bytes(utxo.tweak.to_byte_array())
1324 .expect("Hash is within field order");
1325
1326 let sk = self
1327 .cfg
1328 .private
1329 .bitcoin_sk
1330 .add_tweak(scalar)
1331 .expect("Failed to tweak bitcoin secret key");
1332
1333 Secp256k1::new().sign_ecdsa(&p2wsh_sighash.into(), &sk)
1334 })
1335 .collect()
1336 }
1337
1338 fn verify_signatures(
1339 &self,
1340 unsigned_tx: &FederationTx,
1341 signatures: &[Signature],
1342 pk: PublicKey,
1343 ) -> anyhow::Result<()> {
1344 ensure!(
1345 unsigned_tx.spent_tx_outs.len() == signatures.len(),
1346 "Incorrect number of signatures"
1347 );
1348
1349 let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1350
1351 for ((index, utxo), signature) in unsigned_tx
1352 .spent_tx_outs
1353 .iter()
1354 .enumerate()
1355 .zip(signatures.iter())
1356 {
1357 let code = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1358
1359 let p2wsh_sighash = sighash_cache
1360 .p2wsh_signature_hash(index, &code, utxo.value, EcdsaSighashType::All)
1361 .expect("Failed to compute P2WSH segwit sighash");
1362
1363 let pk = tweak_public_key(&pk, &utxo.tweak);
1364
1365 secp256k1::SECP256K1.verify_ecdsa(&p2wsh_sighash.into(), signature, &pk)?;
1366 }
1367
1368 Ok(())
1369 }
1370
1371 fn finalize_tx(
1372 &self,
1373 federation_tx: &mut FederationTx,
1374 signatures: &BTreeMap<PeerId, Vec<Signature>>,
1375 ) {
1376 assert_eq!(
1377 federation_tx.spent_tx_outs.len(),
1378 federation_tx.tx.input.len()
1379 );
1380
1381 for (index, utxo) in federation_tx.spent_tx_outs.iter().enumerate() {
1382 let satisfier: BTreeMap<PublicKey, bitcoin::ecdsa::Signature> = signatures
1383 .iter()
1384 .map(|(peer, sigs)| {
1385 assert_eq!(sigs.len(), federation_tx.tx.input.len());
1386
1387 let pk = *self
1388 .cfg
1389 .consensus
1390 .bitcoin_pks
1391 .get(peer)
1392 .expect("Failed to get public key of peer from config");
1393
1394 let pk = tweak_public_key(&pk, &utxo.tweak);
1395
1396 (pk, bitcoin::ecdsa::Signature::sighash_all(sigs[index]))
1397 })
1398 .collect();
1399
1400 miniscript::Descriptor::Wsh(self.descriptor(&utxo.tweak))
1401 .satisfy(&mut federation_tx.tx.input[index], satisfier)
1402 .expect("Failed to satisfy descriptor");
1403 }
1404 }
1405
1406 async fn tx_id(&self, dbtx: &mut DatabaseTransaction<'_>, outpoint: OutPoint) -> Option<Txid> {
1407 let index = dbtx.get_value(&TxInfoIndexKey(outpoint)).await?;
1408
1409 dbtx.get_value(&TxInfoKey(index))
1410 .await
1411 .map(|entry| entry.txid)
1412 }
1413
1414 async fn get_outputs(
1415 &self,
1416 dbtx: &mut DatabaseTransaction<'_>,
1417 start_index: u64,
1418 end_index: u64,
1419 ) -> Vec<OutputInfo> {
1420 let spent: BTreeSet<u64> = dbtx
1421 .find_by_range(SpentOutputKey(start_index)..SpentOutputKey(end_index))
1422 .await
1423 .map(|entry| entry.0.0)
1424 .collect()
1425 .await;
1426
1427 dbtx.find_by_range(OutputKey(start_index)..OutputKey(end_index))
1428 .await
1429 .filter_map(|entry| {
1430 std::future::ready(entry.1.1.script_pubkey.is_p2wsh().then(|| OutputInfo {
1431 index: entry.0.0,
1432 script: entry.1.1.script_pubkey,
1433 value: entry.1.1.value,
1434 spent: spent.contains(&entry.0.0),
1435 outpoint: Some(entry.1.0),
1436 }))
1437 })
1438 .collect()
1439 .await
1440 }
1441
1442 async fn pending_tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1443 let n_pending = pending_txs_unordered(dbtx).await.len();
1444
1445 dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1446 .await
1447 .take(n_pending)
1448 .map(|entry| entry.1)
1449 .collect()
1450 .await
1451 }
1452
1453 async fn tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1454 dbtx.find_by_prefix(&TxInfoPrefix)
1455 .await
1456 .map(|entry| entry.1)
1457 .collect()
1458 .await
1459 }
1460
1461 async fn total_txs(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1462 dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1463 .await
1464 .next()
1465 .await
1466 .map_or(0, |entry| entry.0.0 + 1)
1467 }
1468
1469 pub fn network_ui(&self) -> Network {
1471 self.cfg.consensus.network
1472 }
1473
1474 pub async fn federation_wallet_ui(&self) -> Option<FederationWallet> {
1476 self.db
1477 .begin_transaction_nc()
1478 .await
1479 .get_value(&FederationWalletKey)
1480 .await
1481 }
1482
1483 pub async fn consensus_block_count_ui(&self) -> u64 {
1485 self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1486 .await
1487 }
1488
1489 pub async fn consensus_feerate_ui(&self) -> Option<u64> {
1491 self.consensus_feerate(&mut self.db.begin_transaction_nc().await)
1492 .await
1493 .map(|feerate| feerate / 1000)
1494 }
1495
1496 pub async fn send_fee_ui(&self) -> Option<Amount> {
1498 self.send_fee(&mut self.db.begin_transaction_nc().await)
1499 .await
1500 }
1501
1502 pub async fn receive_fee_ui(&self) -> Option<Amount> {
1504 self.receive_fee(&mut self.db.begin_transaction_nc().await)
1505 .await
1506 }
1507
1508 pub async fn pending_tx_chain_ui(&self) -> Vec<TxInfo> {
1510 self.pending_tx_chain(&mut self.db.begin_transaction_nc().await)
1511 .await
1512 }
1513
1514 pub async fn tx_chain_ui(&self) -> Vec<TxInfo> {
1516 self.tx_chain(&mut self.db.begin_transaction_nc().await)
1517 .await
1518 }
1519
1520 pub async fn recovery_keys_ui(&self) -> Option<(BTreeMap<PeerId, String>, String)> {
1523 let wallet = self.federation_wallet_ui().await?;
1524
1525 let pks = self
1526 .cfg
1527 .consensus
1528 .bitcoin_pks
1529 .iter()
1530 .map(|(peer, pk)| (*peer, tweak_public_key(pk, &wallet.tweak).to_string()))
1531 .collect();
1532
1533 let tweak = &Scalar::from_be_bytes(wallet.tweak.to_byte_array())
1534 .expect("Hash is within field order");
1535
1536 let sk = self
1537 .cfg
1538 .private
1539 .bitcoin_sk
1540 .add_tweak(tweak)
1541 .expect("Failed to tweak bitcoin secret key");
1542
1543 let sk = bitcoin::PrivateKey::new(sk, self.cfg.consensus.network).to_wif();
1544
1545 Some((pks, sk))
1546 }
1547}
1548
1549fn calculate_pegin_metrics(
1550 dbtx: &mut DatabaseTransaction<'_>,
1551 amount: fedimint_core::Amount,
1552 fee: fedimint_core::Amount,
1553) {
1554 dbtx.on_commit(move || {
1555 WALLET_INOUT_SATS
1556 .with_label_values(&["incoming"])
1557 .observe(amount.sats_f64());
1558 WALLET_INOUT_FEES_SATS
1559 .with_label_values(&["incoming"])
1560 .observe(fee.sats_f64());
1561 WALLET_PEGIN_SATS.observe(amount.sats_f64());
1562 WALLET_PEGIN_FEES_SATS.observe(fee.sats_f64());
1563 });
1564}
1565
1566fn calculate_pegout_metrics(
1567 dbtx: &mut DatabaseTransaction<'_>,
1568 amount: fedimint_core::Amount,
1569 fee: fedimint_core::Amount,
1570) {
1571 dbtx.on_commit(move || {
1572 WALLET_INOUT_SATS
1573 .with_label_values(&["outgoing"])
1574 .observe(amount.sats_f64());
1575 WALLET_INOUT_FEES_SATS
1576 .with_label_values(&["outgoing"])
1577 .observe(fee.sats_f64());
1578 WALLET_PEGOUT_SATS.observe(amount.sats_f64());
1579 WALLET_PEGOUT_FEES_SATS.observe(fee.sats_f64());
1580 });
1581}