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