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