Skip to main content

fedimint_walletv2_server/
lib.rs

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