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