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
468 .fee_rate
469 .sats_per_kvb
470 .max(MIN_FEERATE_VOTE_SATS_PER_KVB);
471
472 items.push(WalletConsensusItem::Feerate(Some(feerate_vote)));
473 } else {
474 items.push(WalletConsensusItem::Feerate(None));
476 }
477
478 items
479 }
480
481 async fn process_consensus_item<'a, 'b>(
482 &'a self,
483 dbtx: &mut DatabaseTransaction<'b>,
484 consensus_item: WalletConsensusItem,
485 peer: PeerId,
486 ) -> anyhow::Result<()> {
487 match consensus_item {
488 WalletConsensusItem::BlockCount(block_count_vote) => {
489 self.process_block_count(dbtx, block_count_vote, peer).await
490 }
491 WalletConsensusItem::Feerate(feerate) => {
492 if Some(feerate) == dbtx.insert_entry(&FeeRateVoteKey(peer), &feerate).await {
493 return Err(anyhow!("Fee rate vote is redundant"));
494 }
495
496 Ok(())
497 }
498 WalletConsensusItem::Signatures(txid, signatures) => {
499 self.process_signatures(dbtx, txid, signatures, peer).await
500 }
501 WalletConsensusItem::Default { variant, .. } => Err(anyhow!(
502 "Received wallet consensus item with unknown variant {variant}"
503 )),
504 }
505 }
506
507 async fn process_input<'a, 'b, 'c>(
508 &'a self,
509 dbtx: &mut DatabaseTransaction<'c>,
510 input: &'b WalletInput,
511 _in_point: InPoint,
512 ) -> Result<InputMeta, WalletInputError> {
513 let input = input.ensure_v0_ref()?;
514
515 if dbtx
516 .insert_entry(&SpentOutputKey(input.output_index), &())
517 .await
518 .is_some()
519 {
520 return Err(WalletInputError::OutputAlreadySpent);
521 }
522
523 let Output(tracked_outpoint, tracked_output) = dbtx
524 .get_value(&OutputKey(input.output_index))
525 .await
526 .ok_or(WalletInputError::UnknownOutputIndex)?;
527
528 let tweaked_pubkey = self
529 .descriptor(&input.tweak.consensus_hash())
530 .script_pubkey();
531
532 if tracked_output.script_pubkey != tweaked_pubkey {
533 return Err(WalletInputError::WrongTweak);
534 }
535
536 let consensus_receive_fee = self
537 .receive_fee(dbtx)
538 .await
539 .ok_or(WalletInputError::NoConsensusFeerateAvailable)?;
540
541 if input.fee < consensus_receive_fee {
546 return Err(WalletInputError::InsufficientTotalFee);
547 }
548
549 let output_value = tracked_output
550 .value
551 .checked_sub(input.fee)
552 .ok_or(WalletInputError::ArithmeticOverflow)?;
553
554 if let Some(wallet) = dbtx.remove_entry(&FederationWalletKey).await {
555 let change_value = wallet
559 .value
560 .checked_add(output_value)
561 .ok_or(WalletInputError::ArithmeticOverflow)?;
562
563 let tx = Transaction {
564 version: Version(2),
565 lock_time: LockTime::ZERO,
566 input: vec![
567 TxIn {
568 previous_output: wallet.outpoint,
569 script_sig: Default::default(),
570 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
571 witness: bitcoin::Witness::new(),
572 },
573 TxIn {
574 previous_output: tracked_outpoint,
575 script_sig: Default::default(),
576 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
577 witness: bitcoin::Witness::new(),
578 },
579 ],
580 output: vec![TxOut {
581 value: change_value,
582 script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
583 }],
584 };
585
586 dbtx.insert_new_entry(
587 &FederationWalletKey,
588 &FederationWallet {
589 value: change_value,
590 outpoint: bitcoin::OutPoint {
591 txid: tx.compute_txid(),
592 vout: 0,
593 },
594 tweak: wallet.consensus_hash(),
595 },
596 )
597 .await;
598
599 let tx_index = self.total_txs(dbtx).await;
600
601 let created = self.consensus_block_count(dbtx).await;
602
603 dbtx.insert_new_entry(
604 &TxInfoKey(tx_index),
605 &TxInfo {
606 index: tx_index,
607 txid: tx.compute_txid(),
608 input: wallet.value,
609 output: change_value,
610 vbytes: self.cfg.consensus.receive_tx_vbytes,
611 fee: input.fee,
612 created,
613 },
614 )
615 .await;
616
617 dbtx.insert_new_entry(
618 &UnsignedTxKey(tx.compute_txid()),
619 &FederationTx {
620 tx,
621 spent_tx_outs: vec![
622 SpentTxOut {
623 value: wallet.value,
624 tweak: wallet.tweak,
625 },
626 SpentTxOut {
627 value: tracked_output.value,
628 tweak: input.tweak.consensus_hash(),
629 },
630 ],
631 vbytes: self.cfg.consensus.receive_tx_vbytes,
632 fee: input.fee,
633 },
634 )
635 .await;
636 } else {
637 dbtx.insert_new_entry(
638 &FederationWalletKey,
639 &FederationWallet {
640 value: tracked_output.value,
641 outpoint: tracked_outpoint,
642 tweak: input.tweak.consensus_hash(),
643 },
644 )
645 .await;
646 }
647
648 let amount = output_value
649 .to_sat()
650 .checked_mul(1000)
651 .map(fedimint_core::Amount::from_msats)
652 .ok_or(WalletInputError::ArithmeticOverflow)?;
653
654 let fee = self.cfg.consensus.fee_consensus.fee(amount);
655
656 calculate_pegin_metrics(dbtx, amount, fee);
657
658 Ok(InputMeta {
659 amount: TransactionItemAmounts {
660 amounts: Amounts::new_bitcoin(amount),
661 fees: Amounts::new_bitcoin(fee),
662 },
663 pub_key: input.tweak,
664 })
665 }
666
667 async fn process_output<'a, 'b>(
668 &'a self,
669 dbtx: &mut DatabaseTransaction<'b>,
670 output: &'a WalletOutput,
671 outpoint: OutPoint,
672 ) -> Result<TransactionItemAmounts, WalletOutputError> {
673 let output = output.ensure_v0_ref()?;
674
675 if output.value < self.cfg.consensus.dust_limit {
676 return Err(WalletOutputError::UnderDustLimit);
677 }
678
679 let wallet = dbtx
680 .remove_entry(&FederationWalletKey)
681 .await
682 .ok_or(WalletOutputError::NoFederationUTXO)?;
683
684 let consensus_send_fee = self
685 .send_fee(dbtx)
686 .await
687 .ok_or(WalletOutputError::NoConsensusFeerateAvailable)?;
688
689 if output.fee < consensus_send_fee {
694 return Err(WalletOutputError::InsufficientTotalFee);
695 }
696
697 let output_value = output
698 .value
699 .checked_add(output.fee)
700 .ok_or(WalletOutputError::ArithmeticOverflow)?;
701
702 let change_value = wallet
703 .value
704 .checked_sub(output_value)
705 .ok_or(WalletOutputError::ArithmeticOverflow)?;
706
707 if change_value < self.cfg.consensus.dust_limit {
708 return Err(WalletOutputError::ChangeUnderDustLimit);
709 }
710
711 let script_pubkey = output
712 .destination
713 .script_pubkey()
714 .ok_or(WalletOutputError::UnknownScriptVariant)?;
715
716 let tx = Transaction {
717 version: Version(2),
718 lock_time: LockTime::ZERO,
719 input: vec![TxIn {
720 previous_output: wallet.outpoint,
721 script_sig: Default::default(),
722 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
723 witness: bitcoin::Witness::new(),
724 }],
725 output: vec![
726 TxOut {
727 value: change_value,
728 script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
729 },
730 TxOut {
731 value: output.value,
732 script_pubkey,
733 },
734 ],
735 };
736
737 dbtx.insert_new_entry(
738 &FederationWalletKey,
739 &FederationWallet {
740 value: change_value,
741 outpoint: bitcoin::OutPoint {
742 txid: tx.compute_txid(),
743 vout: 0,
744 },
745 tweak: wallet.consensus_hash(),
746 },
747 )
748 .await;
749
750 let tx_index = self.total_txs(dbtx).await;
751
752 let created = self.consensus_block_count(dbtx).await;
753
754 dbtx.insert_new_entry(
755 &TxInfoKey(tx_index),
756 &TxInfo {
757 index: tx_index,
758 txid: tx.compute_txid(),
759 input: wallet.value,
760 output: change_value,
761 vbytes: self.cfg.consensus.send_tx_vbytes,
762 fee: output.fee,
763 created,
764 },
765 )
766 .await;
767
768 dbtx.insert_new_entry(&TxInfoIndexKey(outpoint), &tx_index)
769 .await;
770
771 dbtx.insert_new_entry(
772 &UnsignedTxKey(tx.compute_txid()),
773 &FederationTx {
774 tx,
775 spent_tx_outs: vec![SpentTxOut {
776 value: wallet.value,
777 tweak: wallet.tweak,
778 }],
779 vbytes: self.cfg.consensus.send_tx_vbytes,
780 fee: output.fee,
781 },
782 )
783 .await;
784
785 let amount = output_value
786 .to_sat()
787 .checked_mul(1000)
788 .map(fedimint_core::Amount::from_msats)
789 .ok_or(WalletOutputError::ArithmeticOverflow)?;
790
791 let fee = self.cfg.consensus.fee_consensus.fee(amount);
792
793 calculate_pegout_metrics(dbtx, amount, fee);
794
795 Ok(TransactionItemAmounts {
796 amounts: Amounts::new_bitcoin(amount),
797 fees: Amounts::new_bitcoin(fee),
798 })
799 }
800
801 async fn output_status(
802 &self,
803 _dbtx: &mut DatabaseTransaction<'_>,
804 _outpoint: OutPoint,
805 ) -> Option<WalletOutputOutcome> {
806 None
807 }
808
809 async fn audit(
810 &self,
811 dbtx: &mut DatabaseTransaction<'_>,
812 audit: &mut Audit,
813 module_instance_id: ModuleInstanceId,
814 ) {
815 audit
816 .add_items(
817 dbtx,
818 module_instance_id,
819 &FederationWalletPrefix,
820 |_, wallet| 1000 * wallet.value.to_sat() as i64,
821 )
822 .await;
823 }
824
825 fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
826 vec![
827 public_api_endpoint! {
828 CONSENSUS_BLOCK_COUNT_ENDPOINT,
829 ApiVersion::new(0, 0),
830 async |module: &Wallet, context, _params: ()| -> u64 {
831 let db = context.db();
832 let mut dbtx = db.begin_transaction_nc().await;
833 Ok(module.consensus_block_count(&mut dbtx).await)
834 }
835 },
836 public_api_endpoint! {
837 CONSENSUS_FEERATE_ENDPOINT,
838 ApiVersion::new(0, 0),
839 async |module: &Wallet, context, _params: ()| -> Option<u64> {
840 let db = context.db();
841 let mut dbtx = db.begin_transaction_nc().await;
842 Ok(module.consensus_feerate(&mut dbtx).await)
843 }
844 },
845 public_api_endpoint! {
846 FEDERATION_WALLET_ENDPOINT,
847 ApiVersion::new(0, 0),
848 async |_module: &Wallet, context, _params: ()| -> Option<FederationWallet> {
849 let db = context.db();
850 let mut dbtx = db.begin_transaction_nc().await;
851 Ok(dbtx.get_value(&FederationWalletKey).await)
852 }
853 },
854 public_api_endpoint! {
855 SEND_FEE_ENDPOINT,
856 ApiVersion::new(0, 0),
857 async |module: &Wallet, context, _params: ()| -> Option<Amount> {
858 let db = context.db();
859 let mut dbtx = db.begin_transaction_nc().await;
860 Ok(module.send_fee(&mut dbtx).await)
861 }
862 },
863 public_api_endpoint! {
864 RECEIVE_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.receive_fee(&mut dbtx).await)
870 }
871 },
872 public_api_endpoint! {
873 TRANSACTION_ID_ENDPOINT,
874 ApiVersion::new(0, 0),
875 async |module: &Wallet, context, params: OutPoint| -> Option<Txid> {
876 let db = context.db();
877 let mut dbtx = db.begin_transaction_nc().await;
878 Ok(module.tx_id(&mut dbtx, params).await)
879 }
880 },
881 public_api_endpoint! {
882 OUTPUT_INFO_SLICE_ENDPOINT,
883 ApiVersion::new(0, 0),
884 async |module: &Wallet, context, params: (u64, u64)| -> Vec<OutputInfo> {
885 let db = context.db();
886 let mut dbtx = db.begin_transaction_nc().await;
887 Ok(module.get_outputs(&mut dbtx, params.0, params.1).await)
888 }
889 },
890 public_api_endpoint! {
891 PENDING_TRANSACTION_CHAIN_ENDPOINT,
892 ApiVersion::new(0, 0),
893 async |module: &Wallet, context, _params: ()| -> Vec<TxInfo> {
894 let db = context.db();
895 let mut dbtx = db.begin_transaction_nc().await;
896 Ok(module.pending_tx_chain(&mut dbtx).await)
897 }
898 },
899 public_api_endpoint! {
900 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.tx_chain(&mut dbtx).await)
906 }
907 },
908 ]
909 }
910
911 fn supported_api_versions(&self) -> MultiApiVersion {
912 MultiApiVersion::try_from_iter([ApiVersion::new(0, 1)])
913 .expect("walletv2 declares one API version per major version")
914 }
915}
916
917#[derive(Debug)]
918pub struct Wallet {
919 cfg: WalletConfig,
920 db: Database,
921 btc_rpc: ServerBitcoinRpcMonitor,
922}
923
924impl Wallet {
925 fn new(
926 cfg: WalletConfig,
927 db: &Database,
928 task_group: &TaskGroup,
929 btc_rpc: ServerBitcoinRpcMonitor,
930 ) -> Wallet {
931 Self::spawn_broadcast_unconfirmed_txs_task(btc_rpc.clone(), db.clone(), task_group);
932
933 Wallet {
934 cfg,
935 btc_rpc,
936 db: db.clone(),
937 }
938 }
939
940 fn spawn_broadcast_unconfirmed_txs_task(
941 btc_rpc: ServerBitcoinRpcMonitor,
942 db: Database,
943 task_group: &TaskGroup,
944 ) {
945 task_group.spawn_cancellable("broadcast_unconfirmed_transactions", async move {
946 loop {
947 let unconfirmed_txs = db
948 .begin_transaction_nc()
949 .await
950 .find_by_prefix(&UnconfirmedTxPrefix)
951 .await
952 .map(|entry| entry.1)
953 .collect::<Vec<FederationTx>>()
954 .await;
955
956 for unconfirmed_tx in unconfirmed_txs {
957 if let Err(err) = btc_rpc.submit_transaction(unconfirmed_tx.tx).await {
958 debug!(
959 target: LOG_MODULE_WALLETV2,
960 err = %err.fmt_compact_anyhow(),
961 "Error broadcasting unconfirmed transaction"
962 );
963 }
964 }
965
966 sleep(common::sleep_duration()).await;
967 }
968 });
969 }
970
971 async fn process_block_count(
972 &self,
973 dbtx: &mut DatabaseTransaction<'_>,
974 block_count_vote: u64,
975 peer: PeerId,
976 ) -> anyhow::Result<()> {
977 let old_consensus_block_count = self.consensus_block_count(dbtx).await;
978
979 let current_vote = dbtx
980 .insert_entry(&BlockCountVoteKey(peer), &block_count_vote)
981 .await
982 .unwrap_or(0);
983
984 ensure!(
985 current_vote < block_count_vote,
986 "Block count vote is redundant"
987 );
988
989 let new_consensus_block_count = self.consensus_block_count(dbtx).await;
990
991 assert!(old_consensus_block_count <= new_consensus_block_count);
992
993 debug!(
994 target: LOG_MODULE_WALLETV2,
995 %peer,
996 vote = block_count_vote,
997 old_consensus = old_consensus_block_count,
998 new_consensus = new_consensus_block_count,
999 advanced = new_consensus_block_count - old_consensus_block_count,
1000 "Processed block count vote"
1001 );
1002
1003 let scan_from_genesis = self.cfg.consensus.network == bitcoin::Network::Regtest;
1008 if old_consensus_block_count == 0 && !scan_from_genesis {
1009 return Ok(());
1010 }
1011
1012 self.await_local_sync_to_block_count(
1015 new_consensus_block_count + CONFIRMATION_FINALITY_DELAY,
1016 )
1017 .await;
1018
1019 for height in old_consensus_block_count..new_consensus_block_count {
1020 if let Some(status) = self.btc_rpc.status() {
1022 assert_eq!(status.network, self.cfg.consensus.network);
1023 }
1024
1025 let block_hash = util::retry(
1026 "get_block_hash",
1027 util::backoff_util::background_backoff(),
1028 || self.btc_rpc.get_block_hash(height),
1029 )
1030 .await
1031 .expect("Bitcoind rpc to get_block_hash failed");
1032
1033 let block = util::retry(
1034 "get_block",
1035 util::backoff_util::background_backoff(),
1036 || self.btc_rpc.get_block(&block_hash),
1037 )
1038 .await
1039 .expect("Bitcoind rpc to get_block failed");
1040
1041 assert_eq!(block.block_hash(), block_hash, "Block hash mismatch");
1042
1043 let pks_hash = self.cfg.consensus.bitcoin_pks.consensus_hash();
1044
1045 let txs_num = block.txdata.len();
1046 let mut potential_receives_num: usize = 0;
1047
1048 for tx in block.txdata {
1049 dbtx.remove_entry(&UnconfirmedTxKey(tx.compute_txid()))
1050 .await;
1051
1052 for (vout, tx_out) in tx.output.iter().enumerate() {
1058 if is_potential_receive(&tx_out.script_pubkey, &pks_hash) {
1059 let outpoint = bitcoin::OutPoint {
1060 txid: tx.compute_txid(),
1061 vout: u32::try_from(vout)
1062 .expect("Bitcoin transaction has more than u32::MAX outputs"),
1063 };
1064
1065 let index = dbtx
1066 .find_by_prefix_sorted_descending(&OutputPrefix)
1067 .await
1068 .next()
1069 .await
1070 .map_or(0, |entry| entry.0.0 + 1);
1071
1072 dbtx.insert_new_entry(&OutputKey(index), &Output(outpoint, tx_out.clone()))
1073 .await;
1074
1075 debug!(
1076 target: LOG_MODULE_WALLETV2,
1077 output_index = index,
1078 %outpoint,
1079 value_sat = tx_out.value.to_sat(),
1080 height,
1081 "Recorded potential walletv2 receive"
1082 );
1083
1084 potential_receives_num += 1;
1085 }
1086 }
1087 }
1088
1089 debug!(
1090 target: LOG_MODULE_WALLETV2,
1091 height,
1092 txs_num,
1093 potential_receives_num,
1094 "Scanned block"
1095 );
1096 }
1097
1098 Ok(())
1099 }
1100
1101 async fn process_signatures(
1102 &self,
1103 dbtx: &mut DatabaseTransaction<'_>,
1104 txid: bitcoin::Txid,
1105 signatures: Vec<Signature>,
1106 peer: PeerId,
1107 ) -> anyhow::Result<()> {
1108 let mut unsigned = dbtx
1109 .get_value(&UnsignedTxKey(txid))
1110 .await
1111 .context("Unsigned transaction does not exist")?;
1112
1113 let pk = self
1114 .cfg
1115 .consensus
1116 .bitcoin_pks
1117 .get(&peer)
1118 .expect("Failed to get public key of peer from config");
1119
1120 self.verify_signatures(&unsigned, &signatures, *pk)?;
1121
1122 if dbtx
1123 .insert_entry(&SignaturesKey(txid, peer), &signatures)
1124 .await
1125 .is_some()
1126 {
1127 bail!("Already received valid signatures from this peer")
1128 }
1129
1130 let signatures = dbtx
1131 .find_by_prefix(&SignaturesTxidPrefix(txid))
1132 .await
1133 .map(|(key, signatures)| (key.1, signatures))
1134 .collect::<BTreeMap<PeerId, Vec<Signature>>>()
1135 .await;
1136
1137 if signatures.len() == self.cfg.consensus.bitcoin_pks.to_num_peers().threshold() {
1138 dbtx.remove_entry(&UnsignedTxKey(txid)).await;
1139
1140 dbtx.remove_by_prefix(&SignaturesTxidPrefix(txid)).await;
1141
1142 self.finalize_tx(&mut unsigned, &signatures);
1143
1144 dbtx.insert_new_entry(&UnconfirmedTxKey(txid), &unsigned)
1145 .await;
1146
1147 if let Err(err) = self.btc_rpc.submit_transaction(unsigned.tx).await {
1148 debug!(
1149 target: LOG_MODULE_WALLETV2,
1150 err = %err.fmt_compact_anyhow(),
1151 "Error broadcasting finalized transaction"
1152 );
1153 }
1154 }
1155
1156 Ok(())
1157 }
1158
1159 async fn await_local_sync_to_block_count(&self, block_count: u64) {
1160 loop {
1161 if self
1162 .btc_rpc
1163 .status()
1164 .is_some_and(|status| status.block_count >= block_count)
1165 {
1166 break;
1167 }
1168
1169 info!(target: LOG_MODULE_WALLETV2, "Waiting for local bitcoin backend to sync to block count {block_count}");
1170
1171 sleep(common::sleep_duration()).await;
1172 }
1173 }
1174
1175 pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1176 let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1177
1178 let mut counts = dbtx
1179 .find_by_prefix(&BlockCountVotePrefix)
1180 .await
1181 .map(|entry| entry.1)
1182 .collect::<Vec<u64>>()
1183 .await;
1184
1185 assert!(counts.len() <= num_peers.total());
1186
1187 counts.sort_unstable();
1188
1189 counts.reverse();
1190
1191 assert!(counts.last() <= counts.first());
1192
1193 counts.get(num_peers.threshold() - 1).copied().unwrap_or(0)
1198 }
1199
1200 pub async fn consensus_feerate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<u64> {
1201 let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1202
1203 let mut rates = dbtx
1204 .find_by_prefix(&FeeRateVotePrefix)
1205 .await
1206 .filter_map(|entry| async move { entry.1 })
1207 .collect::<Vec<u64>>()
1208 .await;
1209
1210 assert!(rates.len() <= num_peers.total());
1211
1212 rates.sort_unstable();
1213
1214 assert!(rates.first() <= rates.last());
1215
1216 rates.get(num_peers.threshold() - 1).copied()
1217 }
1218
1219 pub async fn consensus_fee(
1220 &self,
1221 dbtx: &mut DatabaseTransaction<'_>,
1222 tx_vbytes: u64,
1223 ) -> Option<Amount> {
1224 let pending_txs = pending_txs_unordered(dbtx).await;
1228
1229 assert!(pending_txs.len() <= 32);
1230
1231 let feerate = self
1232 .consensus_feerate(dbtx)
1233 .await?
1234 .max(self.cfg.consensus.feerate_base << pending_txs.len());
1235
1236 let tx_fee = tx_vbytes.saturating_mul(feerate).saturating_div(1000);
1237
1238 let stack_vbytes = pending_txs
1239 .iter()
1240 .map(|t| t.vbytes)
1241 .try_fold(tx_vbytes, u64::checked_add)
1242 .expect("Stack vbytes overflow with at most 32 pending txs");
1243
1244 let stack_fee = stack_vbytes.saturating_mul(feerate).saturating_div(1000);
1245
1246 let stack_fee = pending_txs
1248 .iter()
1249 .map(|t| t.fee.to_sat())
1250 .fold(stack_fee, u64::saturating_sub);
1251
1252 Some(Amount::from_sat(tx_fee.max(stack_fee)))
1253 }
1254
1255 pub async fn send_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1256 self.consensus_fee(dbtx, self.cfg.consensus.send_tx_vbytes)
1257 .await
1258 }
1259
1260 pub async fn receive_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1261 self.consensus_fee(dbtx, self.cfg.consensus.receive_tx_vbytes)
1262 .await
1263 }
1264
1265 fn descriptor(&self, tweak: &sha256::Hash) -> Wsh<secp256k1::PublicKey> {
1266 descriptor(&self.cfg.consensus.bitcoin_pks, tweak)
1267 }
1268
1269 fn sign_tx(&self, unsigned_tx: &FederationTx) -> Vec<Signature> {
1270 let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1271
1272 unsigned_tx
1273 .spent_tx_outs
1274 .iter()
1275 .enumerate()
1276 .map(|(index, utxo)| {
1277 let descriptor = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1278
1279 let p2wsh_sighash = sighash_cache
1280 .p2wsh_signature_hash(index, &descriptor, utxo.value, EcdsaSighashType::All)
1281 .expect("Failed to compute P2WSH segwit sighash");
1282
1283 let scalar = &Scalar::from_be_bytes(utxo.tweak.to_byte_array())
1284 .expect("Hash is within field order");
1285
1286 let sk = self
1287 .cfg
1288 .private
1289 .bitcoin_sk
1290 .add_tweak(scalar)
1291 .expect("Failed to tweak bitcoin secret key");
1292
1293 Secp256k1::new().sign_ecdsa(&p2wsh_sighash.into(), &sk)
1294 })
1295 .collect()
1296 }
1297
1298 fn verify_signatures(
1299 &self,
1300 unsigned_tx: &FederationTx,
1301 signatures: &[Signature],
1302 pk: PublicKey,
1303 ) -> anyhow::Result<()> {
1304 ensure!(
1305 unsigned_tx.spent_tx_outs.len() == signatures.len(),
1306 "Incorrect number of signatures"
1307 );
1308
1309 let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1310
1311 for ((index, utxo), signature) in unsigned_tx
1312 .spent_tx_outs
1313 .iter()
1314 .enumerate()
1315 .zip(signatures.iter())
1316 {
1317 let code = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1318
1319 let p2wsh_sighash = sighash_cache
1320 .p2wsh_signature_hash(index, &code, utxo.value, EcdsaSighashType::All)
1321 .expect("Failed to compute P2WSH segwit sighash");
1322
1323 let pk = tweak_public_key(&pk, &utxo.tweak);
1324
1325 secp256k1::SECP256K1.verify_ecdsa(&p2wsh_sighash.into(), signature, &pk)?;
1326 }
1327
1328 Ok(())
1329 }
1330
1331 fn finalize_tx(
1332 &self,
1333 federation_tx: &mut FederationTx,
1334 signatures: &BTreeMap<PeerId, Vec<Signature>>,
1335 ) {
1336 assert_eq!(
1337 federation_tx.spent_tx_outs.len(),
1338 federation_tx.tx.input.len()
1339 );
1340
1341 for (index, utxo) in federation_tx.spent_tx_outs.iter().enumerate() {
1342 let satisfier: BTreeMap<PublicKey, bitcoin::ecdsa::Signature> = signatures
1343 .iter()
1344 .map(|(peer, sigs)| {
1345 assert_eq!(sigs.len(), federation_tx.tx.input.len());
1346
1347 let pk = *self
1348 .cfg
1349 .consensus
1350 .bitcoin_pks
1351 .get(peer)
1352 .expect("Failed to get public key of peer from config");
1353
1354 let pk = tweak_public_key(&pk, &utxo.tweak);
1355
1356 (pk, bitcoin::ecdsa::Signature::sighash_all(sigs[index]))
1357 })
1358 .collect();
1359
1360 miniscript::Descriptor::Wsh(self.descriptor(&utxo.tweak))
1361 .satisfy(&mut federation_tx.tx.input[index], satisfier)
1362 .expect("Failed to satisfy descriptor");
1363 }
1364 }
1365
1366 async fn tx_id(&self, dbtx: &mut DatabaseTransaction<'_>, outpoint: OutPoint) -> Option<Txid> {
1367 let index = dbtx.get_value(&TxInfoIndexKey(outpoint)).await?;
1368
1369 dbtx.get_value(&TxInfoKey(index))
1370 .await
1371 .map(|entry| entry.txid)
1372 }
1373
1374 async fn get_outputs(
1375 &self,
1376 dbtx: &mut DatabaseTransaction<'_>,
1377 start_index: u64,
1378 end_index: u64,
1379 ) -> Vec<OutputInfo> {
1380 let spent: BTreeSet<u64> = dbtx
1381 .find_by_range(SpentOutputKey(start_index)..SpentOutputKey(end_index))
1382 .await
1383 .map(|entry| entry.0.0)
1384 .collect()
1385 .await;
1386
1387 dbtx.find_by_range(OutputKey(start_index)..OutputKey(end_index))
1388 .await
1389 .filter_map(|entry| {
1390 std::future::ready(entry.1.1.script_pubkey.is_p2wsh().then(|| OutputInfo {
1391 index: entry.0.0,
1392 script: entry.1.1.script_pubkey,
1393 value: entry.1.1.value,
1394 spent: spent.contains(&entry.0.0),
1395 outpoint: Some(entry.1.0),
1396 }))
1397 })
1398 .collect()
1399 .await
1400 }
1401
1402 async fn pending_tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1403 let n_pending = pending_txs_unordered(dbtx).await.len();
1404
1405 dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1406 .await
1407 .take(n_pending)
1408 .map(|entry| entry.1)
1409 .collect()
1410 .await
1411 }
1412
1413 async fn tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1414 dbtx.find_by_prefix(&TxInfoPrefix)
1415 .await
1416 .map(|entry| entry.1)
1417 .collect()
1418 .await
1419 }
1420
1421 async fn total_txs(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1422 dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1423 .await
1424 .next()
1425 .await
1426 .map_or(0, |entry| entry.0.0 + 1)
1427 }
1428
1429 pub fn network_ui(&self) -> Network {
1431 self.cfg.consensus.network
1432 }
1433
1434 pub async fn federation_wallet_ui(&self) -> Option<FederationWallet> {
1436 self.db
1437 .begin_transaction_nc()
1438 .await
1439 .get_value(&FederationWalletKey)
1440 .await
1441 }
1442
1443 pub async fn consensus_block_count_ui(&self) -> u64 {
1445 self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1446 .await
1447 }
1448
1449 pub async fn consensus_feerate_ui(&self) -> Option<u64> {
1451 self.consensus_feerate(&mut self.db.begin_transaction_nc().await)
1452 .await
1453 .map(|feerate| feerate / 1000)
1454 }
1455
1456 pub async fn send_fee_ui(&self) -> Option<Amount> {
1458 self.send_fee(&mut self.db.begin_transaction_nc().await)
1459 .await
1460 }
1461
1462 pub async fn receive_fee_ui(&self) -> Option<Amount> {
1464 self.receive_fee(&mut self.db.begin_transaction_nc().await)
1465 .await
1466 }
1467
1468 pub async fn pending_tx_chain_ui(&self) -> Vec<TxInfo> {
1470 self.pending_tx_chain(&mut self.db.begin_transaction_nc().await)
1471 .await
1472 }
1473
1474 pub async fn tx_chain_ui(&self) -> Vec<TxInfo> {
1476 self.tx_chain(&mut self.db.begin_transaction_nc().await)
1477 .await
1478 }
1479
1480 pub async fn recovery_keys_ui(&self) -> Option<(BTreeMap<PeerId, String>, String)> {
1483 let wallet = self.federation_wallet_ui().await?;
1484
1485 let pks = self
1486 .cfg
1487 .consensus
1488 .bitcoin_pks
1489 .iter()
1490 .map(|(peer, pk)| (*peer, tweak_public_key(pk, &wallet.tweak).to_string()))
1491 .collect();
1492
1493 let tweak = &Scalar::from_be_bytes(wallet.tweak.to_byte_array())
1494 .expect("Hash is within field order");
1495
1496 let sk = self
1497 .cfg
1498 .private
1499 .bitcoin_sk
1500 .add_tweak(tweak)
1501 .expect("Failed to tweak bitcoin secret key");
1502
1503 let sk = bitcoin::PrivateKey::new(sk, self.cfg.consensus.network).to_wif();
1504
1505 Some((pks, sk))
1506 }
1507}
1508
1509fn calculate_pegin_metrics(
1510 dbtx: &mut DatabaseTransaction<'_>,
1511 amount: fedimint_core::Amount,
1512 fee: fedimint_core::Amount,
1513) {
1514 dbtx.on_commit(move || {
1515 WALLET_INOUT_SATS
1516 .with_label_values(&["incoming"])
1517 .observe(amount.sats_f64());
1518 WALLET_INOUT_FEES_SATS
1519 .with_label_values(&["incoming"])
1520 .observe(fee.sats_f64());
1521 WALLET_PEGIN_SATS.observe(amount.sats_f64());
1522 WALLET_PEGIN_FEES_SATS.observe(fee.sats_f64());
1523 });
1524}
1525
1526fn calculate_pegout_metrics(
1527 dbtx: &mut DatabaseTransaction<'_>,
1528 amount: fedimint_core::Amount,
1529 fee: fedimint_core::Amount,
1530) {
1531 dbtx.on_commit(move || {
1532 WALLET_INOUT_SATS
1533 .with_label_values(&["outgoing"])
1534 .observe(amount.sats_f64());
1535 WALLET_INOUT_FEES_SATS
1536 .with_label_values(&["outgoing"])
1537 .observe(fee.sats_f64());
1538 WALLET_PEGOUT_SATS.observe(amount.sats_f64());
1539 WALLET_PEGOUT_FEES_SATS.observe(fee.sats_f64());
1540 });
1541}