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    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::{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, FEDERATION_WALLET_ENDPOINT,
69    OUTPUT_INFO_SLICE_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::Output => {
155                    push_db_pair_items!(
156                        dbtx,
157                        OutputPrefix,
158                        OutputKey,
159                        Output,
160                        wallet,
161                        "Wallet Outputs"
162                    );
163                }
164                DbKeyPrefix::SpentOutput => {
165                    push_db_pair_items!(
166                        dbtx,
167                        SpentOutputPrefix,
168                        SpentOutputKey,
169                        (),
170                        wallet,
171                        "Wallet Spent Outputs"
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                self.verify_signatures(
410                    &unsigned_tx,
411                    &signatures,
412                    self.cfg.private.bitcoin_sk.public_key(secp256k1::SECP256K1),
413                )
414                .expect("Our signatures failed verification against our private key");
415
416                WalletConsensusItem::Signatures(key.0, signatures)
417            })
418            .collect::<Vec<WalletConsensusItem>>()
419            .await;
420
421        if let Some(status) = self.btc_rpc.status() {
422            assert_eq!(status.network, self.cfg.consensus.network);
423
424            let block_count_vote = status
425                .block_count
426                .saturating_sub(CONFIRMATION_FINALITY_DELAY);
427
428            let consensus_block_count = self.consensus_block_count(dbtx).await;
429
430            let block_count_vote = match consensus_block_count {
431                0 => block_count_vote,
432                _ => block_count_vote.min(consensus_block_count + MAX_BLOCK_COUNT_INCREMENT),
433            };
434
435            items.push(WalletConsensusItem::BlockCount(block_count_vote));
436
437            let feerate_vote = status
438                .fee_rate
439                .sats_per_kvb
440                .max(MIN_FEERATE_VOTE_SATS_PER_KVB);
441
442            items.push(WalletConsensusItem::Feerate(Some(feerate_vote)));
443        } else {
444            // Bitcoin backend not connected, retract fee rate vote
445            items.push(WalletConsensusItem::Feerate(None));
446        }
447
448        items
449    }
450
451    async fn process_consensus_item<'a, 'b>(
452        &'a self,
453        dbtx: &mut DatabaseTransaction<'b>,
454        consensus_item: WalletConsensusItem,
455        peer: PeerId,
456    ) -> anyhow::Result<()> {
457        match consensus_item {
458            WalletConsensusItem::BlockCount(block_count_vote) => {
459                self.process_block_count(dbtx, block_count_vote, peer).await
460            }
461            WalletConsensusItem::Feerate(feerate) => {
462                if Some(feerate) == dbtx.insert_entry(&FeeRateVoteKey(peer), &feerate).await {
463                    return Err(anyhow!("Fee rate vote is redundant"));
464                }
465
466                Ok(())
467            }
468            WalletConsensusItem::Signatures(txid, signatures) => {
469                self.process_signatures(dbtx, txid, signatures, peer).await
470            }
471            WalletConsensusItem::Default { variant, .. } => Err(anyhow!(
472                "Received wallet consensus item with unknown variant {variant}"
473            )),
474        }
475    }
476
477    async fn process_input<'a, 'b, 'c>(
478        &'a self,
479        dbtx: &mut DatabaseTransaction<'c>,
480        input: &'b WalletInput,
481        _in_point: InPoint,
482    ) -> Result<InputMeta, WalletInputError> {
483        let input = input.ensure_v0_ref()?;
484
485        if dbtx
486            .insert_entry(&SpentOutputKey(input.output_index), &())
487            .await
488            .is_some()
489        {
490            return Err(WalletInputError::OutputAlreadySpent);
491        }
492
493        let Output(tracked_outpoint, tracked_output) = dbtx
494            .get_value(&OutputKey(input.output_index))
495            .await
496            .ok_or(WalletInputError::UnknownOutputIndex)?;
497
498        let tweaked_pubkey = self
499            .descriptor(&input.tweak.consensus_hash())
500            .script_pubkey();
501
502        if tracked_output.script_pubkey != tweaked_pubkey {
503            return Err(WalletInputError::WrongTweak);
504        }
505
506        let consensus_receive_fee = self
507            .receive_fee(dbtx)
508            .await
509            .ok_or(WalletInputError::NoConsensusFeerateAvailable)?;
510
511        // We allow for a higher fee such that a guardian could construct a CPFP
512        // transaction. This is the last line of defense should the federations
513        // transactions ever get stuck due to a critical failure of the feerate
514        // estimation.
515        if input.fee < consensus_receive_fee {
516            return Err(WalletInputError::InsufficientTotalFee);
517        }
518
519        let output_value = tracked_output
520            .value
521            .checked_sub(input.fee)
522            .ok_or(WalletInputError::ArithmeticOverflow)?;
523
524        if let Some(wallet) = dbtx.remove_entry(&FederationWalletKey).await {
525            // Assuming the first receive into the federation is made through a
526            // standard transaction, its output value is over the P2WSH dust
527            // limit. By induction so is this change value.
528            let change_value = wallet
529                .value
530                .checked_add(output_value)
531                .ok_or(WalletInputError::ArithmeticOverflow)?;
532
533            let tx = Transaction {
534                version: Version(2),
535                lock_time: LockTime::ZERO,
536                input: vec![
537                    TxIn {
538                        previous_output: wallet.outpoint,
539                        script_sig: Default::default(),
540                        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
541                        witness: bitcoin::Witness::new(),
542                    },
543                    TxIn {
544                        previous_output: tracked_outpoint,
545                        script_sig: Default::default(),
546                        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
547                        witness: bitcoin::Witness::new(),
548                    },
549                ],
550                output: vec![TxOut {
551                    value: change_value,
552                    script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
553                }],
554            };
555
556            dbtx.insert_new_entry(
557                &FederationWalletKey,
558                &FederationWallet {
559                    value: change_value,
560                    outpoint: bitcoin::OutPoint {
561                        txid: tx.compute_txid(),
562                        vout: 0,
563                    },
564                    tweak: wallet.consensus_hash(),
565                },
566            )
567            .await;
568
569            let tx_index = self.total_txs(dbtx).await;
570
571            let created = self.consensus_block_count(dbtx).await;
572
573            dbtx.insert_new_entry(
574                &TxInfoKey(tx_index),
575                &TxInfo {
576                    index: tx_index,
577                    txid: tx.compute_txid(),
578                    input: wallet.value,
579                    output: change_value,
580                    vbytes: self.cfg.consensus.receive_tx_vbytes,
581                    fee: input.fee,
582                    created,
583                },
584            )
585            .await;
586
587            dbtx.insert_new_entry(
588                &UnsignedTxKey(tx.compute_txid()),
589                &FederationTx {
590                    tx,
591                    spent_tx_outs: vec![
592                        SpentTxOut {
593                            value: wallet.value,
594                            tweak: wallet.tweak,
595                        },
596                        SpentTxOut {
597                            value: tracked_output.value,
598                            tweak: input.tweak.consensus_hash(),
599                        },
600                    ],
601                    vbytes: self.cfg.consensus.receive_tx_vbytes,
602                    fee: input.fee,
603                },
604            )
605            .await;
606        } else {
607            dbtx.insert_new_entry(
608                &FederationWalletKey,
609                &FederationWallet {
610                    value: tracked_output.value,
611                    outpoint: tracked_outpoint,
612                    tweak: input.tweak.consensus_hash(),
613                },
614            )
615            .await;
616        }
617
618        let amount = output_value
619            .to_sat()
620            .checked_mul(1000)
621            .map(fedimint_core::Amount::from_msats)
622            .ok_or(WalletInputError::ArithmeticOverflow)?;
623
624        Ok(InputMeta {
625            amount: TransactionItemAmounts {
626                amounts: Amounts::new_bitcoin(amount),
627                fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.fee(amount)),
628            },
629            pub_key: input.tweak,
630        })
631    }
632
633    async fn process_output<'a, 'b>(
634        &'a self,
635        dbtx: &mut DatabaseTransaction<'b>,
636        output: &'a WalletOutput,
637        outpoint: OutPoint,
638    ) -> Result<TransactionItemAmounts, WalletOutputError> {
639        let output = output.ensure_v0_ref()?;
640
641        if output.value < self.cfg.consensus.dust_limit {
642            return Err(WalletOutputError::UnderDustLimit);
643        }
644
645        let wallet = dbtx
646            .remove_entry(&FederationWalletKey)
647            .await
648            .ok_or(WalletOutputError::NoFederationUTXO)?;
649
650        let consensus_send_fee = self
651            .send_fee(dbtx)
652            .await
653            .ok_or(WalletOutputError::NoConsensusFeerateAvailable)?;
654
655        // We allow for a higher fee such that a guardian could construct a CPFP
656        // transaction. This is the last line of defense should the federations
657        // transactions ever get stuck due to a critical failure of the feerate
658        // estimation.
659        if output.fee < consensus_send_fee {
660            return Err(WalletOutputError::InsufficientTotalFee);
661        }
662
663        let output_value = output
664            .value
665            .checked_add(output.fee)
666            .ok_or(WalletOutputError::ArithmeticOverflow)?;
667
668        let change_value = wallet
669            .value
670            .checked_sub(output_value)
671            .ok_or(WalletOutputError::ArithmeticOverflow)?;
672
673        if change_value < self.cfg.consensus.dust_limit {
674            return Err(WalletOutputError::ChangeUnderDustLimit);
675        }
676
677        let script_pubkey = output
678            .destination
679            .script_pubkey()
680            .ok_or(WalletOutputError::UnknownScriptVariant)?;
681
682        let tx = Transaction {
683            version: Version(2),
684            lock_time: LockTime::ZERO,
685            input: vec![TxIn {
686                previous_output: wallet.outpoint,
687                script_sig: Default::default(),
688                sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
689                witness: bitcoin::Witness::new(),
690            }],
691            output: vec![
692                TxOut {
693                    value: change_value,
694                    script_pubkey: self.descriptor(&wallet.consensus_hash()).script_pubkey(),
695                },
696                TxOut {
697                    value: output.value,
698                    script_pubkey,
699                },
700            ],
701        };
702
703        dbtx.insert_new_entry(
704            &FederationWalletKey,
705            &FederationWallet {
706                value: change_value,
707                outpoint: bitcoin::OutPoint {
708                    txid: tx.compute_txid(),
709                    vout: 0,
710                },
711                tweak: wallet.consensus_hash(),
712            },
713        )
714        .await;
715
716        let tx_index = self.total_txs(dbtx).await;
717
718        let created = self.consensus_block_count(dbtx).await;
719
720        dbtx.insert_new_entry(
721            &TxInfoKey(tx_index),
722            &TxInfo {
723                index: tx_index,
724                txid: tx.compute_txid(),
725                input: wallet.value,
726                output: change_value,
727                vbytes: self.cfg.consensus.send_tx_vbytes,
728                fee: output.fee,
729                created,
730            },
731        )
732        .await;
733
734        dbtx.insert_new_entry(&TxInfoIndexKey(outpoint), &tx_index)
735            .await;
736
737        dbtx.insert_new_entry(
738            &UnsignedTxKey(tx.compute_txid()),
739            &FederationTx {
740                tx,
741                spent_tx_outs: vec![SpentTxOut {
742                    value: wallet.value,
743                    tweak: wallet.tweak,
744                }],
745                vbytes: self.cfg.consensus.send_tx_vbytes,
746                fee: output.fee,
747            },
748        )
749        .await;
750
751        let amount = output_value
752            .to_sat()
753            .checked_mul(1000)
754            .map(fedimint_core::Amount::from_msats)
755            .ok_or(WalletOutputError::ArithmeticOverflow)?;
756
757        Ok(TransactionItemAmounts {
758            amounts: Amounts::new_bitcoin(amount),
759            fees: Amounts::new_bitcoin(self.cfg.consensus.fee_consensus.fee(amount)),
760        })
761    }
762
763    async fn output_status(
764        &self,
765        _dbtx: &mut DatabaseTransaction<'_>,
766        _outpoint: OutPoint,
767    ) -> Option<WalletOutputOutcome> {
768        None
769    }
770
771    async fn audit(
772        &self,
773        dbtx: &mut DatabaseTransaction<'_>,
774        audit: &mut Audit,
775        module_instance_id: ModuleInstanceId,
776    ) {
777        audit
778            .add_items(
779                dbtx,
780                module_instance_id,
781                &FederationWalletPrefix,
782                |_, wallet| 1000 * wallet.value.to_sat() as i64,
783            )
784            .await;
785    }
786
787    fn api_endpoints(&self) -> Vec<ApiEndpoint<Self>> {
788        vec![
789            api_endpoint! {
790                CONSENSUS_BLOCK_COUNT_ENDPOINT,
791                ApiVersion::new(0, 0),
792                async |module: &Wallet, context, _params: ()| -> u64 {
793                    let db = context.db();
794                    let mut dbtx = db.begin_transaction_nc().await;
795                    Ok(module.consensus_block_count(&mut dbtx).await)
796                }
797            },
798            api_endpoint! {
799                CONSENSUS_FEERATE_ENDPOINT,
800                ApiVersion::new(0, 0),
801                async |module: &Wallet, context, _params: ()| -> Option<u64> {
802                    let db = context.db();
803                    let mut dbtx = db.begin_transaction_nc().await;
804                    Ok(module.consensus_feerate(&mut dbtx).await)
805                }
806            },
807            api_endpoint! {
808                FEDERATION_WALLET_ENDPOINT,
809                ApiVersion::new(0, 0),
810                async |_module: &Wallet, context, _params: ()| -> Option<FederationWallet> {
811                    let db = context.db();
812                    let mut dbtx = db.begin_transaction_nc().await;
813                    Ok(dbtx.get_value(&FederationWalletKey).await)
814                }
815            },
816            api_endpoint! {
817                SEND_FEE_ENDPOINT,
818                ApiVersion::new(0, 0),
819                async |module: &Wallet, context, _params: ()| -> Option<Amount> {
820                    let db = context.db();
821                    let mut dbtx = db.begin_transaction_nc().await;
822                    Ok(module.send_fee(&mut dbtx).await)
823                }
824            },
825            api_endpoint! {
826                RECEIVE_FEE_ENDPOINT,
827                ApiVersion::new(0, 0),
828                async |module: &Wallet, context, _params: ()| -> Option<Amount> {
829                    let db = context.db();
830                    let mut dbtx = db.begin_transaction_nc().await;
831                    Ok(module.receive_fee(&mut dbtx).await)
832                }
833            },
834            api_endpoint! {
835                TRANSACTION_ID_ENDPOINT,
836                ApiVersion::new(0, 0),
837                async |module: &Wallet, context, params: OutPoint| -> Option<Txid> {
838                    let db = context.db();
839                    let mut dbtx = db.begin_transaction_nc().await;
840                    Ok(module.tx_id(&mut dbtx, params).await)
841                }
842            },
843            api_endpoint! {
844                OUTPUT_INFO_SLICE_ENDPOINT,
845                ApiVersion::new(0, 0),
846                async |module: &Wallet, context, params: (u64, u64)| -> Vec<OutputInfo> {
847                    let db = context.db();
848                    let mut dbtx = db.begin_transaction_nc().await;
849                    Ok(module.get_outputs(&mut dbtx, params.0, params.1).await)
850                }
851            },
852            api_endpoint! {
853                PENDING_TRANSACTION_CHAIN_ENDPOINT,
854                ApiVersion::new(0, 0),
855                async |module: &Wallet, context, _params: ()| -> Vec<TxInfo> {
856                    let db = context.db();
857                    let mut dbtx = db.begin_transaction_nc().await;
858                    Ok(module.pending_tx_chain(&mut dbtx).await)
859                }
860            },
861            api_endpoint! {
862                TRANSACTION_CHAIN_ENDPOINT,
863                ApiVersion::new(0, 0),
864                async |module: &Wallet, context, _params: ()| -> Vec<TxInfo> {
865                    let db = context.db();
866                    let mut dbtx = db.begin_transaction_nc().await;
867                    Ok(module.tx_chain(&mut dbtx).await)
868                }
869            },
870        ]
871    }
872}
873
874#[derive(Debug)]
875pub struct Wallet {
876    cfg: WalletConfig,
877    db: Database,
878    btc_rpc: ServerBitcoinRpcMonitor,
879}
880
881impl Wallet {
882    fn new(
883        cfg: WalletConfig,
884        db: &Database,
885        task_group: &TaskGroup,
886        btc_rpc: ServerBitcoinRpcMonitor,
887    ) -> Wallet {
888        Self::spawn_broadcast_unconfirmed_txs_task(btc_rpc.clone(), db.clone(), task_group);
889
890        Wallet {
891            cfg,
892            btc_rpc,
893            db: db.clone(),
894        }
895    }
896
897    fn spawn_broadcast_unconfirmed_txs_task(
898        btc_rpc: ServerBitcoinRpcMonitor,
899        db: Database,
900        task_group: &TaskGroup,
901    ) {
902        task_group.spawn_cancellable("broadcast_unconfirmed_transactions", async move {
903            loop {
904                let unconfirmed_txs = db
905                    .begin_transaction_nc()
906                    .await
907                    .find_by_prefix(&UnconfirmedTxPrefix)
908                    .await
909                    .map(|entry| entry.1)
910                    .collect::<Vec<FederationTx>>()
911                    .await;
912
913                for unconfirmed_tx in unconfirmed_txs {
914                    btc_rpc.submit_transaction(unconfirmed_tx.tx).await;
915                }
916
917                sleep(common::sleep_duration()).await;
918            }
919        });
920    }
921
922    async fn process_block_count(
923        &self,
924        dbtx: &mut DatabaseTransaction<'_>,
925        block_count_vote: u64,
926        peer: PeerId,
927    ) -> anyhow::Result<()> {
928        let old_consensus_block_count = self.consensus_block_count(dbtx).await;
929
930        let current_vote = dbtx
931            .insert_entry(&BlockCountVoteKey(peer), &block_count_vote)
932            .await
933            .unwrap_or(0);
934
935        ensure!(
936            current_vote < block_count_vote,
937            "Block count vote is redundant"
938        );
939
940        let new_consensus_block_count = self.consensus_block_count(dbtx).await;
941
942        assert!(old_consensus_block_count <= new_consensus_block_count);
943
944        // We do not sync blocks that predate the federation itself.
945        if old_consensus_block_count == 0 {
946            return Ok(());
947        }
948
949        // Our bitcoin backend needs to be synced for the following calls to the
950        // get_block rpc to be safe for consensus.
951        self.await_local_sync_to_block_count(
952            new_consensus_block_count + CONFIRMATION_FINALITY_DELAY,
953        )
954        .await;
955
956        for height in old_consensus_block_count..new_consensus_block_count {
957            // Verify network matches (status should be available after sync)
958            if let Some(status) = self.btc_rpc.status() {
959                assert_eq!(status.network, self.cfg.consensus.network);
960            }
961
962            let block_hash = util::retry(
963                "get_block_hash",
964                util::backoff_util::background_backoff(),
965                || self.btc_rpc.get_block_hash(height),
966            )
967            .await
968            .expect("Bitcoind rpc to get_block_hash failed");
969
970            let block = util::retry(
971                "get_block",
972                util::backoff_util::background_backoff(),
973                || self.btc_rpc.get_block(&block_hash),
974            )
975            .await
976            .expect("Bitcoind rpc to get_block failed");
977
978            assert_eq!(block.block_hash(), block_hash, "Block hash mismatch");
979
980            let pks_hash = self.cfg.consensus.bitcoin_pks.consensus_hash();
981
982            for tx in block.txdata {
983                dbtx.remove_entry(&UnconfirmedTxKey(tx.compute_txid()))
984                    .await;
985
986                // We maintain an append-only log of transaction outputs that pass
987                // the probabilistic receive filter created since the federation was
988                // established. This is downloaded by clients to detect pegins and
989                // claim them by index.
990
991                for (vout, tx_out) in tx.output.iter().enumerate() {
992                    if is_potential_receive(&tx_out.script_pubkey, &pks_hash) {
993                        let outpoint = bitcoin::OutPoint {
994                            txid: tx.compute_txid(),
995                            vout: u32::try_from(vout)
996                                .expect("Bitcoin transaction has more than u32::MAX outputs"),
997                        };
998
999                        let index = dbtx
1000                            .find_by_prefix_sorted_descending(&OutputPrefix)
1001                            .await
1002                            .next()
1003                            .await
1004                            .map_or(0, |entry| entry.0.0 + 1);
1005
1006                        dbtx.insert_new_entry(&OutputKey(index), &Output(outpoint, tx_out.clone()))
1007                            .await;
1008                    }
1009                }
1010            }
1011        }
1012
1013        Ok(())
1014    }
1015
1016    async fn process_signatures(
1017        &self,
1018        dbtx: &mut DatabaseTransaction<'_>,
1019        txid: bitcoin::Txid,
1020        signatures: Vec<Signature>,
1021        peer: PeerId,
1022    ) -> anyhow::Result<()> {
1023        let mut unsigned = dbtx
1024            .get_value(&UnsignedTxKey(txid))
1025            .await
1026            .context("Unsigned transaction does not exist")?;
1027
1028        let pk = self
1029            .cfg
1030            .consensus
1031            .bitcoin_pks
1032            .get(&peer)
1033            .expect("Failed to get public key of peer from config");
1034
1035        self.verify_signatures(&unsigned, &signatures, *pk)?;
1036
1037        if dbtx
1038            .insert_entry(&SignaturesKey(txid, peer), &signatures)
1039            .await
1040            .is_some()
1041        {
1042            bail!("Already received valid signatures from this peer")
1043        }
1044
1045        let signatures = dbtx
1046            .find_by_prefix(&SignaturesTxidPrefix(txid))
1047            .await
1048            .map(|(key, signatures)| (key.1, signatures))
1049            .collect::<BTreeMap<PeerId, Vec<Signature>>>()
1050            .await;
1051
1052        if signatures.len() == self.cfg.consensus.bitcoin_pks.to_num_peers().threshold() {
1053            dbtx.remove_entry(&UnsignedTxKey(txid)).await;
1054
1055            dbtx.remove_by_prefix(&SignaturesTxidPrefix(txid)).await;
1056
1057            self.finalize_tx(&mut unsigned, &signatures);
1058
1059            dbtx.insert_new_entry(&UnconfirmedTxKey(txid), &unsigned)
1060                .await;
1061
1062            self.btc_rpc.submit_transaction(unsigned.tx).await;
1063        }
1064
1065        Ok(())
1066    }
1067
1068    async fn await_local_sync_to_block_count(&self, block_count: u64) {
1069        loop {
1070            if self
1071                .btc_rpc
1072                .status()
1073                .is_some_and(|status| status.block_count >= block_count)
1074            {
1075                break;
1076            }
1077
1078            info!(target: LOG_MODULE_WALLETV2, "Waiting for local bitcoin backend to sync to block count {block_count}");
1079
1080            sleep(common::sleep_duration()).await;
1081        }
1082    }
1083
1084    pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1085        let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1086
1087        let mut counts = dbtx
1088            .find_by_prefix(&BlockCountVotePrefix)
1089            .await
1090            .map(|entry| entry.1)
1091            .collect::<Vec<u64>>()
1092            .await;
1093
1094        assert!(counts.len() <= num_peers.total());
1095
1096        counts.sort_unstable();
1097
1098        counts.reverse();
1099
1100        assert!(counts.last() <= counts.first());
1101
1102        // The block count we select guarantees that any threshold of correct peers can
1103        // increase the consensus block count and any consensus block count has been
1104        // confirmed by a threshold of peers.
1105
1106        counts.get(num_peers.threshold() - 1).copied().unwrap_or(0)
1107    }
1108
1109    pub async fn consensus_feerate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<u64> {
1110        let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1111
1112        let mut rates = dbtx
1113            .find_by_prefix(&FeeRateVotePrefix)
1114            .await
1115            .filter_map(|entry| async move { entry.1 })
1116            .collect::<Vec<u64>>()
1117            .await;
1118
1119        assert!(rates.len() <= num_peers.total());
1120
1121        rates.sort_unstable();
1122
1123        assert!(rates.first() <= rates.last());
1124
1125        rates.get(num_peers.threshold() - 1).copied()
1126    }
1127
1128    pub async fn consensus_fee(
1129        &self,
1130        dbtx: &mut DatabaseTransaction<'_>,
1131        tx_vbytes: u64,
1132    ) -> Option<Amount> {
1133        // The minimum feerate is a protection against a catastrophic error in the
1134        // feerate estimation and limits the length of the pending transaction stack.
1135
1136        let pending_txs = pending_txs_unordered(dbtx).await;
1137
1138        assert!(pending_txs.len() <= 32);
1139
1140        let feerate = self
1141            .consensus_feerate(dbtx)
1142            .await?
1143            .max(self.cfg.consensus.feerate_base << pending_txs.len());
1144
1145        let tx_fee = tx_vbytes.saturating_mul(feerate).saturating_div(1000);
1146
1147        let stack_vbytes = pending_txs
1148            .iter()
1149            .map(|t| t.vbytes)
1150            .try_fold(tx_vbytes, u64::checked_add)
1151            .expect("Stack vbytes overflow with at most 32 pending txs");
1152
1153        let stack_fee = stack_vbytes.saturating_mul(feerate).saturating_div(1000);
1154
1155        // Deduct the fees already paid by currently pending transactions
1156        let stack_fee = pending_txs
1157            .iter()
1158            .map(|t| t.fee.to_sat())
1159            .fold(stack_fee, u64::saturating_sub);
1160
1161        Some(Amount::from_sat(tx_fee.max(stack_fee)))
1162    }
1163
1164    pub async fn send_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1165        self.consensus_fee(dbtx, self.cfg.consensus.send_tx_vbytes)
1166            .await
1167    }
1168
1169    pub async fn receive_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1170        self.consensus_fee(dbtx, self.cfg.consensus.receive_tx_vbytes)
1171            .await
1172    }
1173
1174    fn descriptor(&self, tweak: &sha256::Hash) -> Wsh<secp256k1::PublicKey> {
1175        descriptor(&self.cfg.consensus.bitcoin_pks, tweak)
1176    }
1177
1178    fn sign_tx(&self, unsigned_tx: &FederationTx) -> Vec<Signature> {
1179        let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1180
1181        unsigned_tx
1182            .spent_tx_outs
1183            .iter()
1184            .enumerate()
1185            .map(|(index, utxo)| {
1186                let descriptor = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1187
1188                let p2wsh_sighash = sighash_cache
1189                    .p2wsh_signature_hash(index, &descriptor, utxo.value, EcdsaSighashType::All)
1190                    .expect("Failed to compute P2WSH segwit sighash");
1191
1192                let scalar = &Scalar::from_be_bytes(utxo.tweak.to_byte_array())
1193                    .expect("Hash is within field order");
1194
1195                let sk = self
1196                    .cfg
1197                    .private
1198                    .bitcoin_sk
1199                    .add_tweak(scalar)
1200                    .expect("Failed to tweak bitcoin secret key");
1201
1202                Secp256k1::new().sign_ecdsa(&p2wsh_sighash.into(), &sk)
1203            })
1204            .collect()
1205    }
1206
1207    fn verify_signatures(
1208        &self,
1209        unsigned_tx: &FederationTx,
1210        signatures: &[Signature],
1211        pk: PublicKey,
1212    ) -> anyhow::Result<()> {
1213        ensure!(
1214            unsigned_tx.spent_tx_outs.len() == signatures.len(),
1215            "Incorrect number of signatures"
1216        );
1217
1218        let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1219
1220        for ((index, utxo), signature) in unsigned_tx
1221            .spent_tx_outs
1222            .iter()
1223            .enumerate()
1224            .zip(signatures.iter())
1225        {
1226            let code = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1227
1228            let p2wsh_sighash = sighash_cache
1229                .p2wsh_signature_hash(index, &code, utxo.value, EcdsaSighashType::All)
1230                .expect("Failed to compute P2WSH segwit sighash");
1231
1232            let pk = tweak_public_key(&pk, &utxo.tweak);
1233
1234            secp256k1::SECP256K1.verify_ecdsa(&p2wsh_sighash.into(), signature, &pk)?;
1235        }
1236
1237        Ok(())
1238    }
1239
1240    fn finalize_tx(
1241        &self,
1242        federation_tx: &mut FederationTx,
1243        signatures: &BTreeMap<PeerId, Vec<Signature>>,
1244    ) {
1245        assert_eq!(
1246            federation_tx.spent_tx_outs.len(),
1247            federation_tx.tx.input.len()
1248        );
1249
1250        for (index, utxo) in federation_tx.spent_tx_outs.iter().enumerate() {
1251            let satisfier: BTreeMap<PublicKey, bitcoin::ecdsa::Signature> = signatures
1252                .iter()
1253                .map(|(peer, sigs)| {
1254                    assert_eq!(sigs.len(), federation_tx.tx.input.len());
1255
1256                    let pk = *self
1257                        .cfg
1258                        .consensus
1259                        .bitcoin_pks
1260                        .get(peer)
1261                        .expect("Failed to get public key of peer from config");
1262
1263                    let pk = tweak_public_key(&pk, &utxo.tweak);
1264
1265                    (pk, bitcoin::ecdsa::Signature::sighash_all(sigs[index]))
1266                })
1267                .collect();
1268
1269            miniscript::Descriptor::Wsh(self.descriptor(&utxo.tweak))
1270                .satisfy(&mut federation_tx.tx.input[index], satisfier)
1271                .expect("Failed to satisfy descriptor");
1272        }
1273    }
1274
1275    async fn tx_id(&self, dbtx: &mut DatabaseTransaction<'_>, outpoint: OutPoint) -> Option<Txid> {
1276        let index = dbtx.get_value(&TxInfoIndexKey(outpoint)).await?;
1277
1278        dbtx.get_value(&TxInfoKey(index))
1279            .await
1280            .map(|entry| entry.txid)
1281    }
1282
1283    async fn get_outputs(
1284        &self,
1285        dbtx: &mut DatabaseTransaction<'_>,
1286        start_index: u64,
1287        end_index: u64,
1288    ) -> Vec<OutputInfo> {
1289        let spent: BTreeSet<u64> = dbtx
1290            .find_by_range(SpentOutputKey(start_index)..SpentOutputKey(end_index))
1291            .await
1292            .map(|entry| entry.0.0)
1293            .collect()
1294            .await;
1295
1296        dbtx.find_by_range(OutputKey(start_index)..OutputKey(end_index))
1297            .await
1298            .filter_map(|entry| {
1299                std::future::ready(entry.1.1.script_pubkey.is_p2wsh().then(|| OutputInfo {
1300                    index: entry.0.0,
1301                    script: entry.1.1.script_pubkey,
1302                    value: entry.1.1.value,
1303                    spent: spent.contains(&entry.0.0),
1304                }))
1305            })
1306            .collect()
1307            .await
1308    }
1309
1310    async fn pending_tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1311        let n_pending = pending_txs_unordered(dbtx).await.len();
1312
1313        dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1314            .await
1315            .take(n_pending)
1316            .map(|entry| entry.1)
1317            .collect()
1318            .await
1319    }
1320
1321    async fn tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1322        dbtx.find_by_prefix(&TxInfoPrefix)
1323            .await
1324            .map(|entry| entry.1)
1325            .collect()
1326            .await
1327    }
1328
1329    async fn total_txs(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1330        dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1331            .await
1332            .next()
1333            .await
1334            .map_or(0, |entry| entry.0.0 + 1)
1335    }
1336
1337    /// Get the network for UI display
1338    pub fn network_ui(&self) -> Network {
1339        self.cfg.consensus.network
1340    }
1341
1342    /// Get the current federation wallet info for UI display
1343    pub async fn federation_wallet_ui(&self) -> Option<FederationWallet> {
1344        self.db
1345            .begin_transaction_nc()
1346            .await
1347            .get_value(&FederationWalletKey)
1348            .await
1349    }
1350
1351    /// Get the current consensus block count for UI display
1352    pub async fn consensus_block_count_ui(&self) -> u64 {
1353        self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1354            .await
1355    }
1356
1357    /// Get the current consensus feerate for UI display
1358    pub async fn consensus_feerate_ui(&self) -> Option<u64> {
1359        self.consensus_feerate(&mut self.db.begin_transaction_nc().await)
1360            .await
1361            .map(|feerate| feerate / 1000)
1362    }
1363
1364    /// Get the current send fee for UI display
1365    pub async fn send_fee_ui(&self) -> Option<Amount> {
1366        self.send_fee(&mut self.db.begin_transaction_nc().await)
1367            .await
1368    }
1369
1370    /// Get the current receive fee for UI display
1371    pub async fn receive_fee_ui(&self) -> Option<Amount> {
1372        self.receive_fee(&mut self.db.begin_transaction_nc().await)
1373            .await
1374    }
1375
1376    /// Get the current pending transaction info for UI display
1377    pub async fn pending_tx_chain_ui(&self) -> Vec<TxInfo> {
1378        self.pending_tx_chain(&mut self.db.begin_transaction_nc().await)
1379            .await
1380    }
1381
1382    /// Get the current transaction log for UI display
1383    pub async fn tx_chain_ui(&self) -> Vec<TxInfo> {
1384        self.tx_chain(&mut self.db.begin_transaction_nc().await)
1385            .await
1386    }
1387
1388    /// Export recovery keys for federation shutdown. Returns None if the
1389    /// federation wallet has not been initialized yet.
1390    pub async fn recovery_keys_ui(&self) -> Option<(BTreeMap<PeerId, String>, String)> {
1391        let wallet = self.federation_wallet_ui().await?;
1392
1393        let pks = self
1394            .cfg
1395            .consensus
1396            .bitcoin_pks
1397            .iter()
1398            .map(|(peer, pk)| (*peer, tweak_public_key(pk, &wallet.tweak).to_string()))
1399            .collect();
1400
1401        let tweak = &Scalar::from_be_bytes(wallet.tweak.to_byte_array())
1402            .expect("Hash is within field order");
1403
1404        let sk = self
1405            .cfg
1406            .private
1407            .bitcoin_sk
1408            .add_tweak(tweak)
1409            .expect("Failed to tweak bitcoin secret key");
1410
1411        let sk = bitcoin::PrivateKey::new(sk, self.cfg.consensus.network).to_wif();
1412
1413        Some((pks, sk))
1414    }
1415}