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