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