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