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, CoreConsensusVersion, InputMeta, ModuleConsensusVersion,
49    ModuleInit, MultiApiVersion, TransactionItemAmounts, public_api_endpoint,
50};
51#[cfg(not(target_family = "wasm"))]
52use fedimint_core::task::TaskGroup;
53use fedimint_core::task::sleep;
54use fedimint_core::util::FmtCompactAnyhow as _;
55use fedimint_core::{
56    InPoint, NumPeersExt, OutPoint, PeerId, apply, async_trait_maybe_send, push_db_pair_items, util,
57};
58use fedimint_logging::LOG_MODULE_WALLETV2;
59use fedimint_server_core::bitcoin_rpc::ServerBitcoinRpcMonitor;
60use fedimint_server_core::config::{PeerHandleOps, PeerHandleOpsExt};
61use fedimint_server_core::migration::ServerModuleDbMigrationFn;
62use fedimint_server_core::{
63    ConfigGenModuleArgs, EnvVarDoc, ServerModule, ServerModuleInit, ServerModuleInitArgs,
64};
65pub use fedimint_walletv2_common as common;
66use fedimint_walletv2_common::config::{
67    FeeConsensus, WalletClientConfig, WalletConfig, WalletConfigPrivate,
68};
69use fedimint_walletv2_common::endpoint_constants::{
70    CONSENSUS_BLOCK_COUNT_ENDPOINT, CONSENSUS_FEERATE_ENDPOINT, FEDERATION_WALLET_ENDPOINT,
71    OUTPUT_INFO_SLICE_ENDPOINT, PENDING_TRANSACTION_CHAIN_ENDPOINT, RECEIVE_FEE_ENDPOINT,
72    SEND_FEE_ENDPOINT, TRANSACTION_CHAIN_ENDPOINT, TRANSACTION_ID_ENDPOINT,
73};
74use fedimint_walletv2_common::{
75    FederationWallet, MODULE_CONSENSUS_VERSION, TxInfo, WalletInputError, WalletOutputError,
76    descriptor, is_potential_receive, tweak_public_key,
77};
78use futures::StreamExt;
79use miniscript::descriptor::Wsh;
80use rand::rngs::OsRng;
81use secp256k1::ecdsa::Signature;
82use secp256k1::{PublicKey, Scalar, SecretKey};
83use serde::{Deserialize, Serialize};
84use strum::IntoEnumIterator;
85use tracing::{debug, info};
86
87use crate::db::{
88    BlockCountVoteKey, BlockCountVotePrefix, FeeRateVoteKey, FeeRateVotePrefix, TxInfoKey,
89    TxInfoPrefix, UnconfirmedTxKey, UnconfirmedTxPrefix, UnsignedTxKey, UnsignedTxPrefix,
90};
91
92/// Number of confirmations required for a transaction to be considered as
93/// final by the federation. The block that mines the transaction does
94/// not count towards the number of confirmations.
95pub const CONFIRMATION_FINALITY_DELAY: u64 = 6;
96
97/// Maximum number of blocks the consensus block count can advance in a single
98/// consensus item to limit the work done in one `process_consensus_item` step.
99const MAX_BLOCK_COUNT_INCREMENT: u64 = 5;
100
101/// Devimint tests can mine many regtest blocks at once, so use a larger cap in
102/// test environments to avoid waiting on several consensus sessions.
103const TEST_MAX_BLOCK_COUNT_INCREMENT: u64 = 100;
104
105/// Minimum fee rate vote of 1 sat/vB to ensure we never propose a fee rate
106/// below what Bitcoin Core will relay.
107const MIN_FEERATE_VOTE_SATS_PER_KVB: u64 = 1000;
108
109#[derive(Clone, Debug, Eq, PartialEq, Serialize, Encodable, Decodable)]
110pub struct FederationTx {
111    pub tx: Transaction,
112    pub spent_tx_outs: Vec<SpentTxOut>,
113    pub vbytes: u64,
114    pub fee: Amount,
115}
116
117#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Encodable, Decodable)]
118pub struct SpentTxOut {
119    pub value: Amount,
120    pub tweak: sha256::Hash,
121}
122
123async fn pending_txs_unordered(dbtx: &mut DatabaseTransaction<'_>) -> Vec<FederationTx> {
124    let unsigned: Vec<FederationTx> = dbtx
125        .find_by_prefix(&UnsignedTxPrefix)
126        .await
127        .map(|entry| entry.1)
128        .collect()
129        .await;
130
131    let unconfirmed: Vec<FederationTx> = dbtx
132        .find_by_prefix(&UnconfirmedTxPrefix)
133        .await
134        .map(|entry| entry.1)
135        .collect()
136        .await;
137
138    unsigned.into_iter().chain(unconfirmed).collect()
139}
140
141#[derive(Debug, Clone)]
142pub struct WalletInit;
143
144impl ModuleInit for WalletInit {
145    type Common = WalletCommonInit;
146
147    async fn dump_database(
148        &self,
149        dbtx: &mut DatabaseTransaction<'_>,
150        prefix_names: Vec<String>,
151    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
152        let mut wallet: BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> = BTreeMap::new();
153
154        let filtered_prefixes = DbKeyPrefix::iter().filter(|f| {
155            prefix_names.is_empty() || prefix_names.contains(&f.to_string().to_lowercase())
156        });
157
158        for table in filtered_prefixes {
159            match table {
160                DbKeyPrefix::Output => {
161                    push_db_pair_items!(
162                        dbtx,
163                        OutputPrefix,
164                        OutputKey,
165                        Output,
166                        wallet,
167                        "Wallet Outputs"
168                    );
169                }
170                DbKeyPrefix::SpentOutput => {
171                    push_db_pair_items!(
172                        dbtx,
173                        SpentOutputPrefix,
174                        SpentOutputKey,
175                        (),
176                        wallet,
177                        "Wallet Spent Outputs"
178                    );
179                }
180                DbKeyPrefix::BlockCountVote => {
181                    push_db_pair_items!(
182                        dbtx,
183                        BlockCountVotePrefix,
184                        BlockCountVoteKey,
185                        u64,
186                        wallet,
187                        "Wallet Block Count Votes"
188                    );
189                }
190                DbKeyPrefix::FeeRateVote => {
191                    push_db_pair_items!(
192                        dbtx,
193                        FeeRateVotePrefix,
194                        FeeRateVoteKey,
195                        Option<u64>,
196                        wallet,
197                        "Wallet Fee Rate Votes"
198                    );
199                }
200                DbKeyPrefix::TxLog => {
201                    push_db_pair_items!(
202                        dbtx,
203                        TxInfoPrefix,
204                        TxInfoKey,
205                        TxInfo,
206                        wallet,
207                        "Wallet Tx Log"
208                    );
209                }
210                DbKeyPrefix::TxInfoIndex => {
211                    push_db_pair_items!(
212                        dbtx,
213                        TxInfoIndexPrefix,
214                        TxInfoIndexKey,
215                        u64,
216                        wallet,
217                        "Wallet Tx Info Index"
218                    );
219                }
220                DbKeyPrefix::UnsignedTx => {
221                    push_db_pair_items!(
222                        dbtx,
223                        UnsignedTxPrefix,
224                        UnsignedTxKey,
225                        FederationTx,
226                        wallet,
227                        "Wallet Unsigned Transactions"
228                    );
229                }
230                DbKeyPrefix::Signatures => {
231                    push_db_pair_items!(
232                        dbtx,
233                        SignaturesPrefix,
234                        SignaturesKey,
235                        Vec<Signature>,
236                        wallet,
237                        "Wallet Signatures"
238                    );
239                }
240                DbKeyPrefix::UnconfirmedTx => {
241                    push_db_pair_items!(
242                        dbtx,
243                        UnconfirmedTxPrefix,
244                        UnconfirmedTxKey,
245                        FederationTx,
246                        wallet,
247                        "Wallet Unconfirmed Transactions"
248                    );
249                }
250                DbKeyPrefix::FederationWallet => {
251                    push_db_pair_items!(
252                        dbtx,
253                        FederationWalletPrefix,
254                        FederationWalletKey,
255                        FederationWallet,
256                        wallet,
257                        "Federation Wallet"
258                    );
259                }
260            }
261        }
262
263        Box::new(wallet.into_iter())
264    }
265}
266
267#[apply(async_trait_maybe_send!)]
268impl ServerModuleInit for WalletInit {
269    type Module = Wallet;
270
271    fn versions(&self, _core: CoreConsensusVersion) -> &[ModuleConsensusVersion] {
272        &[MODULE_CONSENSUS_VERSION]
273    }
274
275    fn is_enabled_by_default(&self) -> bool {
276        is_env_var_set_opt(FM_ENABLE_MODULE_WALLETV2_ENV).unwrap_or(true)
277    }
278
279    fn get_documented_env_vars(&self) -> Vec<EnvVarDoc> {
280        vec![EnvVarDoc {
281            name: FM_ENABLE_MODULE_WALLETV2_ENV,
282            description: "Set to 0/false to disable the WalletV2 module. Enabled by default.",
283        }]
284    }
285
286    async fn init(&self, args: &ServerModuleInitArgs<Self>) -> anyhow::Result<Self::Module> {
287        Ok(Wallet::new(
288            args.cfg().to_typed()?,
289            args.db(),
290            args.task_group(),
291            args.server_bitcoin_rpc_monitor(),
292        ))
293    }
294
295    fn trusted_dealer_gen(
296        &self,
297        peers: &[PeerId],
298        args: &ConfigGenModuleArgs,
299    ) -> BTreeMap<PeerId, ServerModuleConfig> {
300        let fee_consensus = FeeConsensus::new(0).expect("Relative fee is within range");
301
302        let bitcoin_sks = peers
303            .iter()
304            .map(|peer| (*peer, SecretKey::new(&mut secp256k1::rand::thread_rng())))
305            .collect::<BTreeMap<PeerId, SecretKey>>();
306
307        let bitcoin_pks = bitcoin_sks
308            .iter()
309            .map(|(peer, sk)| (*peer, sk.public_key(secp256k1::SECP256K1)))
310            .collect::<BTreeMap<PeerId, PublicKey>>();
311
312        bitcoin_sks
313            .into_iter()
314            .map(|(peer, bitcoin_sk)| {
315                let config = WalletConfig {
316                    private: WalletConfigPrivate { bitcoin_sk },
317                    consensus: WalletConfigConsensus::new(
318                        bitcoin_pks.clone(),
319                        fee_consensus.clone(),
320                        args.network,
321                    ),
322                };
323
324                (peer, config.to_erased())
325            })
326            .collect()
327    }
328
329    async fn distributed_gen(
330        &self,
331        peers: &(dyn PeerHandleOps + Send + Sync),
332        args: &ConfigGenModuleArgs,
333    ) -> anyhow::Result<ServerModuleConfig> {
334        let fee_consensus = FeeConsensus::new(0).expect("Relative fee is within range");
335
336        let (bitcoin_sk, bitcoin_pk) = secp256k1::generate_keypair(&mut OsRng);
337
338        let bitcoin_pks: BTreeMap<PeerId, PublicKey> = peers
339            .exchange_encodable(bitcoin_pk)
340            .await?
341            .into_iter()
342            .collect();
343
344        let config = WalletConfig {
345            private: WalletConfigPrivate { bitcoin_sk },
346            consensus: WalletConfigConsensus::new(bitcoin_pks, fee_consensus, args.network),
347        };
348
349        Ok(config.to_erased())
350    }
351
352    fn validate_config(&self, identity: &PeerId, config: ServerModuleConfig) -> anyhow::Result<()> {
353        let config = config.to_typed::<WalletConfig>()?;
354
355        ensure!(
356            config
357                .consensus
358                .bitcoin_pks
359                .get(identity)
360                .ok_or(anyhow::anyhow!("No public key for our identity"))?
361                == &config.private.bitcoin_sk.public_key(secp256k1::SECP256K1),
362            "Bitcoin wallet private key doesn't match multisig pubkey"
363        );
364
365        Ok(())
366    }
367
368    fn get_client_config(
369        &self,
370        config: &ServerModuleConsensusConfig,
371    ) -> anyhow::Result<WalletClientConfig> {
372        let config = WalletConfigConsensus::from_erased(config)?;
373
374        Ok(WalletClientConfig {
375            bitcoin_pks: config.bitcoin_pks,
376            descriptor: config.descriptor,
377            send_tx_vbytes: config.send_tx_vbytes,
378            receive_tx_vbytes: config.receive_tx_vbytes,
379            feerate_base: config.feerate_base,
380            dust_limit: config.dust_limit,
381            fee_consensus: config.fee_consensus,
382            network: config.network,
383        })
384    }
385
386    fn get_database_migrations(
387        &self,
388    ) -> BTreeMap<DatabaseVersion, ServerModuleDbMigrationFn<Wallet>> {
389        BTreeMap::new()
390    }
391
392    fn used_db_prefixes(&self) -> Option<BTreeSet<u8>> {
393        Some(DbKeyPrefix::iter().map(|p| p as u8).collect())
394    }
395}
396
397#[apply(async_trait_maybe_send!)]
398impl ServerModule for Wallet {
399    type Common = WalletModuleTypes;
400    type Init = WalletInit;
401
402    async fn consensus_proposal<'a>(
403        &'a self,
404        dbtx: &mut DatabaseTransaction<'_>,
405    ) -> Vec<WalletConsensusItem> {
406        let mut items = dbtx
407            .find_by_prefix(&UnsignedTxPrefix)
408            .await
409            .map(|(key, unsigned_tx)| {
410                let signatures = self.sign_tx(&unsigned_tx);
411
412                self.verify_signatures(
413                    &unsigned_tx,
414                    &signatures,
415                    self.cfg.private.bitcoin_sk.public_key(secp256k1::SECP256K1),
416                )
417                .expect("Our signatures failed verification against our private key");
418
419                WalletConsensusItem::Signatures(key.0, signatures)
420            })
421            .collect::<Vec<WalletConsensusItem>>()
422            .await;
423
424        if let Some(status) = self.btc_rpc.status() {
425            assert_eq!(status.network, self.cfg.consensus.network);
426
427            let block_count_vote = status
428                .block_count
429                .saturating_sub(CONFIRMATION_FINALITY_DELAY);
430
431            let consensus_block_count = self.consensus_block_count(dbtx).await;
432
433            let max_block_count_increment = if is_running_in_test_env() {
434                TEST_MAX_BLOCK_COUNT_INCREMENT
435            } else {
436                MAX_BLOCK_COUNT_INCREMENT
437            };
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            public_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            public_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            public_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            public_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            public_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            public_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            public_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            public_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            public_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    fn supported_api_versions(&self) -> MultiApiVersion {
883        MultiApiVersion::try_from_iter([ApiVersion::new(0, 1)])
884            .expect("walletv2 declares one API version per major version")
885    }
886}
887
888#[derive(Debug)]
889pub struct Wallet {
890    cfg: WalletConfig,
891    db: Database,
892    btc_rpc: ServerBitcoinRpcMonitor,
893}
894
895impl Wallet {
896    fn new(
897        cfg: WalletConfig,
898        db: &Database,
899        task_group: &TaskGroup,
900        btc_rpc: ServerBitcoinRpcMonitor,
901    ) -> Wallet {
902        Self::spawn_broadcast_unconfirmed_txs_task(btc_rpc.clone(), db.clone(), task_group);
903
904        Wallet {
905            cfg,
906            btc_rpc,
907            db: db.clone(),
908        }
909    }
910
911    fn spawn_broadcast_unconfirmed_txs_task(
912        btc_rpc: ServerBitcoinRpcMonitor,
913        db: Database,
914        task_group: &TaskGroup,
915    ) {
916        task_group.spawn_cancellable("broadcast_unconfirmed_transactions", async move {
917            loop {
918                let unconfirmed_txs = db
919                    .begin_transaction_nc()
920                    .await
921                    .find_by_prefix(&UnconfirmedTxPrefix)
922                    .await
923                    .map(|entry| entry.1)
924                    .collect::<Vec<FederationTx>>()
925                    .await;
926
927                for unconfirmed_tx in unconfirmed_txs {
928                    if let Err(err) = btc_rpc.submit_transaction(unconfirmed_tx.tx).await {
929                        debug!(
930                            target: LOG_MODULE_WALLETV2,
931                            err = %err.fmt_compact_anyhow(),
932                            "Error broadcasting unconfirmed transaction"
933                        );
934                    }
935                }
936
937                sleep(common::sleep_duration()).await;
938            }
939        });
940    }
941
942    async fn process_block_count(
943        &self,
944        dbtx: &mut DatabaseTransaction<'_>,
945        block_count_vote: u64,
946        peer: PeerId,
947    ) -> anyhow::Result<()> {
948        let old_consensus_block_count = self.consensus_block_count(dbtx).await;
949
950        let current_vote = dbtx
951            .insert_entry(&BlockCountVoteKey(peer), &block_count_vote)
952            .await
953            .unwrap_or(0);
954
955        ensure!(
956            current_vote < block_count_vote,
957            "Block count vote is redundant"
958        );
959
960        let new_consensus_block_count = self.consensus_block_count(dbtx).await;
961
962        assert!(old_consensus_block_count <= new_consensus_block_count);
963
964        debug!(
965            target: LOG_MODULE_WALLETV2,
966            %peer,
967            vote = block_count_vote,
968            old_consensus = old_consensus_block_count,
969            new_consensus = new_consensus_block_count,
970            advanced = new_consensus_block_count - old_consensus_block_count,
971            "Processed block count vote"
972        );
973
974        // Outside regtest, do not sync blocks that predate the federation itself.
975        // Regtest starts from scratch, so scan from genesis to avoid races where
976        // test deposits are mined before the first walletv2 block count
977        // transition is processed.
978        let scan_from_genesis = self.cfg.consensus.network == bitcoin::Network::Regtest;
979        if old_consensus_block_count == 0 && !scan_from_genesis {
980            return Ok(());
981        }
982
983        // Our bitcoin backend needs to be synced for the following calls to the
984        // get_block rpc to be safe for consensus.
985        self.await_local_sync_to_block_count(
986            new_consensus_block_count + CONFIRMATION_FINALITY_DELAY,
987        )
988        .await;
989
990        for height in old_consensus_block_count..new_consensus_block_count {
991            // Verify network matches (status should be available after sync)
992            if let Some(status) = self.btc_rpc.status() {
993                assert_eq!(status.network, self.cfg.consensus.network);
994            }
995
996            let block_hash = util::retry(
997                "get_block_hash",
998                util::backoff_util::background_backoff(),
999                || self.btc_rpc.get_block_hash(height),
1000            )
1001            .await
1002            .expect("Bitcoind rpc to get_block_hash failed");
1003
1004            let block = util::retry(
1005                "get_block",
1006                util::backoff_util::background_backoff(),
1007                || self.btc_rpc.get_block(&block_hash),
1008            )
1009            .await
1010            .expect("Bitcoind rpc to get_block failed");
1011
1012            assert_eq!(block.block_hash(), block_hash, "Block hash mismatch");
1013
1014            let pks_hash = self.cfg.consensus.bitcoin_pks.consensus_hash();
1015
1016            let txs_num = block.txdata.len();
1017            let mut potential_receives_num: usize = 0;
1018
1019            for tx in block.txdata {
1020                dbtx.remove_entry(&UnconfirmedTxKey(tx.compute_txid()))
1021                    .await;
1022
1023                // We maintain an append-only log of transaction outputs that pass
1024                // the probabilistic receive filter created since the federation was
1025                // established. This is downloaded by clients to detect pegins and
1026                // claim them by index.
1027
1028                for (vout, tx_out) in tx.output.iter().enumerate() {
1029                    if is_potential_receive(&tx_out.script_pubkey, &pks_hash) {
1030                        let outpoint = bitcoin::OutPoint {
1031                            txid: tx.compute_txid(),
1032                            vout: u32::try_from(vout)
1033                                .expect("Bitcoin transaction has more than u32::MAX outputs"),
1034                        };
1035
1036                        let index = dbtx
1037                            .find_by_prefix_sorted_descending(&OutputPrefix)
1038                            .await
1039                            .next()
1040                            .await
1041                            .map_or(0, |entry| entry.0.0 + 1);
1042
1043                        dbtx.insert_new_entry(&OutputKey(index), &Output(outpoint, tx_out.clone()))
1044                            .await;
1045
1046                        debug!(
1047                            target: LOG_MODULE_WALLETV2,
1048                            output_index = index,
1049                            %outpoint,
1050                            value_sat = tx_out.value.to_sat(),
1051                            height,
1052                            "Recorded potential walletv2 receive"
1053                        );
1054
1055                        potential_receives_num += 1;
1056                    }
1057                }
1058            }
1059
1060            debug!(
1061                target: LOG_MODULE_WALLETV2,
1062                height,
1063                txs_num,
1064                potential_receives_num,
1065                "Scanned block"
1066            );
1067        }
1068
1069        Ok(())
1070    }
1071
1072    async fn process_signatures(
1073        &self,
1074        dbtx: &mut DatabaseTransaction<'_>,
1075        txid: bitcoin::Txid,
1076        signatures: Vec<Signature>,
1077        peer: PeerId,
1078    ) -> anyhow::Result<()> {
1079        let mut unsigned = dbtx
1080            .get_value(&UnsignedTxKey(txid))
1081            .await
1082            .context("Unsigned transaction does not exist")?;
1083
1084        let pk = self
1085            .cfg
1086            .consensus
1087            .bitcoin_pks
1088            .get(&peer)
1089            .expect("Failed to get public key of peer from config");
1090
1091        self.verify_signatures(&unsigned, &signatures, *pk)?;
1092
1093        if dbtx
1094            .insert_entry(&SignaturesKey(txid, peer), &signatures)
1095            .await
1096            .is_some()
1097        {
1098            bail!("Already received valid signatures from this peer")
1099        }
1100
1101        let signatures = dbtx
1102            .find_by_prefix(&SignaturesTxidPrefix(txid))
1103            .await
1104            .map(|(key, signatures)| (key.1, signatures))
1105            .collect::<BTreeMap<PeerId, Vec<Signature>>>()
1106            .await;
1107
1108        if signatures.len() == self.cfg.consensus.bitcoin_pks.to_num_peers().threshold() {
1109            dbtx.remove_entry(&UnsignedTxKey(txid)).await;
1110
1111            dbtx.remove_by_prefix(&SignaturesTxidPrefix(txid)).await;
1112
1113            self.finalize_tx(&mut unsigned, &signatures);
1114
1115            dbtx.insert_new_entry(&UnconfirmedTxKey(txid), &unsigned)
1116                .await;
1117
1118            if let Err(err) = self.btc_rpc.submit_transaction(unsigned.tx).await {
1119                debug!(
1120                    target: LOG_MODULE_WALLETV2,
1121                    err = %err.fmt_compact_anyhow(),
1122                    "Error broadcasting finalized transaction"
1123                );
1124            }
1125        }
1126
1127        Ok(())
1128    }
1129
1130    async fn await_local_sync_to_block_count(&self, block_count: u64) {
1131        loop {
1132            if self
1133                .btc_rpc
1134                .status()
1135                .is_some_and(|status| status.block_count >= block_count)
1136            {
1137                break;
1138            }
1139
1140            info!(target: LOG_MODULE_WALLETV2, "Waiting for local bitcoin backend to sync to block count {block_count}");
1141
1142            sleep(common::sleep_duration()).await;
1143        }
1144    }
1145
1146    pub async fn consensus_block_count(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1147        let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1148
1149        let mut counts = dbtx
1150            .find_by_prefix(&BlockCountVotePrefix)
1151            .await
1152            .map(|entry| entry.1)
1153            .collect::<Vec<u64>>()
1154            .await;
1155
1156        assert!(counts.len() <= num_peers.total());
1157
1158        counts.sort_unstable();
1159
1160        counts.reverse();
1161
1162        assert!(counts.last() <= counts.first());
1163
1164        // The block count we select guarantees that any threshold of correct peers can
1165        // increase the consensus block count and any consensus block count has been
1166        // confirmed by a threshold of peers.
1167
1168        counts.get(num_peers.threshold() - 1).copied().unwrap_or(0)
1169    }
1170
1171    pub async fn consensus_feerate(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<u64> {
1172        let num_peers = self.cfg.consensus.bitcoin_pks.to_num_peers();
1173
1174        let mut rates = dbtx
1175            .find_by_prefix(&FeeRateVotePrefix)
1176            .await
1177            .filter_map(|entry| async move { entry.1 })
1178            .collect::<Vec<u64>>()
1179            .await;
1180
1181        assert!(rates.len() <= num_peers.total());
1182
1183        rates.sort_unstable();
1184
1185        assert!(rates.first() <= rates.last());
1186
1187        rates.get(num_peers.threshold() - 1).copied()
1188    }
1189
1190    pub async fn consensus_fee(
1191        &self,
1192        dbtx: &mut DatabaseTransaction<'_>,
1193        tx_vbytes: u64,
1194    ) -> Option<Amount> {
1195        // The minimum feerate is a protection against a catastrophic error in the
1196        // feerate estimation and limits the length of the pending transaction stack.
1197
1198        let pending_txs = pending_txs_unordered(dbtx).await;
1199
1200        assert!(pending_txs.len() <= 32);
1201
1202        let feerate = self
1203            .consensus_feerate(dbtx)
1204            .await?
1205            .max(self.cfg.consensus.feerate_base << pending_txs.len());
1206
1207        let tx_fee = tx_vbytes.saturating_mul(feerate).saturating_div(1000);
1208
1209        let stack_vbytes = pending_txs
1210            .iter()
1211            .map(|t| t.vbytes)
1212            .try_fold(tx_vbytes, u64::checked_add)
1213            .expect("Stack vbytes overflow with at most 32 pending txs");
1214
1215        let stack_fee = stack_vbytes.saturating_mul(feerate).saturating_div(1000);
1216
1217        // Deduct the fees already paid by currently pending transactions
1218        let stack_fee = pending_txs
1219            .iter()
1220            .map(|t| t.fee.to_sat())
1221            .fold(stack_fee, u64::saturating_sub);
1222
1223        Some(Amount::from_sat(tx_fee.max(stack_fee)))
1224    }
1225
1226    pub async fn send_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1227        self.consensus_fee(dbtx, self.cfg.consensus.send_tx_vbytes)
1228            .await
1229    }
1230
1231    pub async fn receive_fee(&self, dbtx: &mut DatabaseTransaction<'_>) -> Option<Amount> {
1232        self.consensus_fee(dbtx, self.cfg.consensus.receive_tx_vbytes)
1233            .await
1234    }
1235
1236    fn descriptor(&self, tweak: &sha256::Hash) -> Wsh<secp256k1::PublicKey> {
1237        descriptor(&self.cfg.consensus.bitcoin_pks, tweak)
1238    }
1239
1240    fn sign_tx(&self, unsigned_tx: &FederationTx) -> Vec<Signature> {
1241        let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1242
1243        unsigned_tx
1244            .spent_tx_outs
1245            .iter()
1246            .enumerate()
1247            .map(|(index, utxo)| {
1248                let descriptor = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1249
1250                let p2wsh_sighash = sighash_cache
1251                    .p2wsh_signature_hash(index, &descriptor, utxo.value, EcdsaSighashType::All)
1252                    .expect("Failed to compute P2WSH segwit sighash");
1253
1254                let scalar = &Scalar::from_be_bytes(utxo.tweak.to_byte_array())
1255                    .expect("Hash is within field order");
1256
1257                let sk = self
1258                    .cfg
1259                    .private
1260                    .bitcoin_sk
1261                    .add_tweak(scalar)
1262                    .expect("Failed to tweak bitcoin secret key");
1263
1264                Secp256k1::new().sign_ecdsa(&p2wsh_sighash.into(), &sk)
1265            })
1266            .collect()
1267    }
1268
1269    fn verify_signatures(
1270        &self,
1271        unsigned_tx: &FederationTx,
1272        signatures: &[Signature],
1273        pk: PublicKey,
1274    ) -> anyhow::Result<()> {
1275        ensure!(
1276            unsigned_tx.spent_tx_outs.len() == signatures.len(),
1277            "Incorrect number of signatures"
1278        );
1279
1280        let mut sighash_cache = SighashCache::new(unsigned_tx.tx.clone());
1281
1282        for ((index, utxo), signature) in unsigned_tx
1283            .spent_tx_outs
1284            .iter()
1285            .enumerate()
1286            .zip(signatures.iter())
1287        {
1288            let code = self.descriptor(&utxo.tweak).ecdsa_sighash_script_code();
1289
1290            let p2wsh_sighash = sighash_cache
1291                .p2wsh_signature_hash(index, &code, utxo.value, EcdsaSighashType::All)
1292                .expect("Failed to compute P2WSH segwit sighash");
1293
1294            let pk = tweak_public_key(&pk, &utxo.tweak);
1295
1296            secp256k1::SECP256K1.verify_ecdsa(&p2wsh_sighash.into(), signature, &pk)?;
1297        }
1298
1299        Ok(())
1300    }
1301
1302    fn finalize_tx(
1303        &self,
1304        federation_tx: &mut FederationTx,
1305        signatures: &BTreeMap<PeerId, Vec<Signature>>,
1306    ) {
1307        assert_eq!(
1308            federation_tx.spent_tx_outs.len(),
1309            federation_tx.tx.input.len()
1310        );
1311
1312        for (index, utxo) in federation_tx.spent_tx_outs.iter().enumerate() {
1313            let satisfier: BTreeMap<PublicKey, bitcoin::ecdsa::Signature> = signatures
1314                .iter()
1315                .map(|(peer, sigs)| {
1316                    assert_eq!(sigs.len(), federation_tx.tx.input.len());
1317
1318                    let pk = *self
1319                        .cfg
1320                        .consensus
1321                        .bitcoin_pks
1322                        .get(peer)
1323                        .expect("Failed to get public key of peer from config");
1324
1325                    let pk = tweak_public_key(&pk, &utxo.tweak);
1326
1327                    (pk, bitcoin::ecdsa::Signature::sighash_all(sigs[index]))
1328                })
1329                .collect();
1330
1331            miniscript::Descriptor::Wsh(self.descriptor(&utxo.tweak))
1332                .satisfy(&mut federation_tx.tx.input[index], satisfier)
1333                .expect("Failed to satisfy descriptor");
1334        }
1335    }
1336
1337    async fn tx_id(&self, dbtx: &mut DatabaseTransaction<'_>, outpoint: OutPoint) -> Option<Txid> {
1338        let index = dbtx.get_value(&TxInfoIndexKey(outpoint)).await?;
1339
1340        dbtx.get_value(&TxInfoKey(index))
1341            .await
1342            .map(|entry| entry.txid)
1343    }
1344
1345    async fn get_outputs(
1346        &self,
1347        dbtx: &mut DatabaseTransaction<'_>,
1348        start_index: u64,
1349        end_index: u64,
1350    ) -> Vec<OutputInfo> {
1351        let spent: BTreeSet<u64> = dbtx
1352            .find_by_range(SpentOutputKey(start_index)..SpentOutputKey(end_index))
1353            .await
1354            .map(|entry| entry.0.0)
1355            .collect()
1356            .await;
1357
1358        dbtx.find_by_range(OutputKey(start_index)..OutputKey(end_index))
1359            .await
1360            .filter_map(|entry| {
1361                std::future::ready(entry.1.1.script_pubkey.is_p2wsh().then(|| OutputInfo {
1362                    index: entry.0.0,
1363                    script: entry.1.1.script_pubkey,
1364                    value: entry.1.1.value,
1365                    spent: spent.contains(&entry.0.0),
1366                    outpoint: Some(entry.1.0),
1367                }))
1368            })
1369            .collect()
1370            .await
1371    }
1372
1373    async fn pending_tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1374        let n_pending = pending_txs_unordered(dbtx).await.len();
1375
1376        dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1377            .await
1378            .take(n_pending)
1379            .map(|entry| entry.1)
1380            .collect()
1381            .await
1382    }
1383
1384    async fn tx_chain(&self, dbtx: &mut DatabaseTransaction<'_>) -> Vec<TxInfo> {
1385        dbtx.find_by_prefix(&TxInfoPrefix)
1386            .await
1387            .map(|entry| entry.1)
1388            .collect()
1389            .await
1390    }
1391
1392    async fn total_txs(&self, dbtx: &mut DatabaseTransaction<'_>) -> u64 {
1393        dbtx.find_by_prefix_sorted_descending(&TxInfoPrefix)
1394            .await
1395            .next()
1396            .await
1397            .map_or(0, |entry| entry.0.0 + 1)
1398    }
1399
1400    /// Get the network for UI display
1401    pub fn network_ui(&self) -> Network {
1402        self.cfg.consensus.network
1403    }
1404
1405    /// Get the current federation wallet info for UI display
1406    pub async fn federation_wallet_ui(&self) -> Option<FederationWallet> {
1407        self.db
1408            .begin_transaction_nc()
1409            .await
1410            .get_value(&FederationWalletKey)
1411            .await
1412    }
1413
1414    /// Get the current consensus block count for UI display
1415    pub async fn consensus_block_count_ui(&self) -> u64 {
1416        self.consensus_block_count(&mut self.db.begin_transaction_nc().await)
1417            .await
1418    }
1419
1420    /// Get the current consensus feerate for UI display
1421    pub async fn consensus_feerate_ui(&self) -> Option<u64> {
1422        self.consensus_feerate(&mut self.db.begin_transaction_nc().await)
1423            .await
1424            .map(|feerate| feerate / 1000)
1425    }
1426
1427    /// Get the current send fee for UI display
1428    pub async fn send_fee_ui(&self) -> Option<Amount> {
1429        self.send_fee(&mut self.db.begin_transaction_nc().await)
1430            .await
1431    }
1432
1433    /// Get the current receive fee for UI display
1434    pub async fn receive_fee_ui(&self) -> Option<Amount> {
1435        self.receive_fee(&mut self.db.begin_transaction_nc().await)
1436            .await
1437    }
1438
1439    /// Get the current pending transaction info for UI display
1440    pub async fn pending_tx_chain_ui(&self) -> Vec<TxInfo> {
1441        self.pending_tx_chain(&mut self.db.begin_transaction_nc().await)
1442            .await
1443    }
1444
1445    /// Get the current transaction log for UI display
1446    pub async fn tx_chain_ui(&self) -> Vec<TxInfo> {
1447        self.tx_chain(&mut self.db.begin_transaction_nc().await)
1448            .await
1449    }
1450
1451    /// Export recovery keys for federation shutdown. Returns None if the
1452    /// federation wallet has not been initialized yet.
1453    pub async fn recovery_keys_ui(&self) -> Option<(BTreeMap<PeerId, String>, String)> {
1454        let wallet = self.federation_wallet_ui().await?;
1455
1456        let pks = self
1457            .cfg
1458            .consensus
1459            .bitcoin_pks
1460            .iter()
1461            .map(|(peer, pk)| (*peer, tweak_public_key(pk, &wallet.tweak).to_string()))
1462            .collect();
1463
1464        let tweak = &Scalar::from_be_bytes(wallet.tweak.to_byte_array())
1465            .expect("Hash is within field order");
1466
1467        let sk = self
1468            .cfg
1469            .private
1470            .bitcoin_sk
1471            .add_tweak(tweak)
1472            .expect("Failed to tweak bitcoin secret key");
1473
1474        let sk = bitcoin::PrivateKey::new(sk, self.cfg.consensus.network).to_wif();
1475
1476        Some((pks, sk))
1477    }
1478}