diff --git a/crates/electrum/src/electrum_ext.rs b/crates/electrum/src/electrum_ext.rs index 806cabeb2d..715a62ead7 100644 --- a/crates/electrum/src/electrum_ext.rs +++ b/crates/electrum/src/electrum_ext.rs @@ -20,15 +20,18 @@ pub trait ElectrumExt { /// /// - `request`: struct with data required to perform a spk-based blockchain client full scan, /// see [`FullScanRequest`] - /// - /// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated - /// transactions. `batch_size` specifies the max number of script pubkeys to request for in a - /// single batch request. + /// - `stop_gap`: the full scan for each keychain stops after a gap of script pubkeys with no + /// associated transactions + /// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch + /// request + /// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee + /// calculation fn full_scan( &self, request: FullScanRequest, stop_gap: usize, batch_size: usize, + fetch_prev_txouts: bool, ) -> Result, Error>; /// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified @@ -36,15 +39,21 @@ pub trait ElectrumExt { /// /// - `request`: struct with data required to perform a spk-based blockchain client sync, /// see [`SyncRequest`] - /// - /// `batch_size` specifies the max number of script pubkeys to request for in a single batch - /// request. + /// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch + /// request + /// - `fetch_prev_txouts`: specifies whether or not we want previous `TxOut`s for fee + /// calculation /// /// If the scripts to sync are unknown, such as when restoring or importing a keychain that /// may include scripts that have been used, use [`full_scan`] with the keychain. /// /// [`full_scan`]: ElectrumExt::full_scan - fn sync(&self, request: SyncRequest, batch_size: usize) -> Result; + fn sync( + &self, + request: SyncRequest, + batch_size: usize, + fetch_prev_txouts: bool, + ) -> Result; } impl ElectrumExt for E { @@ -53,6 +62,7 @@ impl ElectrumExt for E { mut request: FullScanRequest, stop_gap: usize, batch_size: usize, + fetch_prev_txouts: bool, ) -> Result, Error> { let mut request_spks = request.spks_by_keychain; @@ -110,6 +120,11 @@ impl ElectrumExt for E { continue; // reorg } + // Fetch previous `TxOut`s for fee calculation if flag is enabled. + if fetch_prev_txouts { + fetch_prev_txout(self, &mut request.tx_cache, &mut graph_update)?; + } + let chain_update = tip; let keychain_update = request_spks @@ -133,14 +148,19 @@ impl ElectrumExt for E { Ok(ElectrumFullScanResult(update)) } - fn sync(&self, request: SyncRequest, batch_size: usize) -> Result { + fn sync( + &self, + request: SyncRequest, + batch_size: usize, + fetch_prev_txouts: bool, + ) -> Result { let mut tx_cache = request.tx_cache.clone(); let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone()) .cache_txs(request.tx_cache) .set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk))); let mut full_scan_res = self - .full_scan(full_scan_req, usize::MAX, batch_size)? + .full_scan(full_scan_req, usize::MAX, batch_size, fetch_prev_txouts)? .with_confirmation_height_anchor(); let (tip, _) = construct_update_tip(self, request.chain_tip)?; @@ -374,7 +394,7 @@ fn populate_with_outpoints( client: &impl ElectrumApi, cps: &BTreeMap, tx_cache: &mut TxCache, - tx_graph: &mut TxGraph, + graph_update: &mut TxGraph, outpoints: impl IntoIterator, ) -> Result<(), Error> { for outpoint in outpoints { @@ -399,18 +419,18 @@ fn populate_with_outpoints( continue; } has_residing = true; - if tx_graph.get_tx(res.tx_hash).is_none() { - let _ = tx_graph.insert_tx(tx.clone()); + if graph_update.get_tx(res.tx_hash).is_none() { + let _ = graph_update.insert_tx(tx.clone()); } } else { if has_spending { continue; } - let res_tx = match tx_graph.get_tx(res.tx_hash) { + let res_tx = match graph_update.get_tx(res.tx_hash) { Some(tx) => tx, None => { let res_tx = fetch_tx(client, tx_cache, res.tx_hash)?; - let _ = tx_graph.insert_tx(Arc::clone(&res_tx)); + let _ = graph_update.insert_tx(Arc::clone(&res_tx)); res_tx } }; @@ -424,7 +444,7 @@ fn populate_with_outpoints( }; if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) { - let _ = tx_graph.insert_anchor(res.tx_hash, anchor); + let _ = graph_update.insert_anchor(res.tx_hash, anchor); } } } @@ -484,6 +504,27 @@ fn fetch_tx( }) } +// Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions, +// which we do not have by default. This data is needed to calculate the transaction fee. +fn fetch_prev_txout( + client: &C, + tx_cache: &mut TxCache, + graph_update: &mut TxGraph, +) -> Result<(), Error> { + let full_txs: Vec> = + graph_update.full_txs().map(|tx_node| tx_node.tx).collect(); + for tx in full_txs { + for vin in &tx.input { + let outpoint = vin.previous_output; + let prev_tx = fetch_tx(client, tx_cache, outpoint.txid)?; + for txout in prev_tx.output.clone() { + let _ = graph_update.insert_txout(outpoint, txout); + } + } + } + Ok(()) +} + fn populate_with_spks( client: &impl ElectrumApi, cps: &BTreeMap, diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 9905ab9cc2..1077fb8d92 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -66,6 +66,7 @@ fn scan_detects_confirmed_tx() -> Result<()> { SyncRequest::from_chain_tip(recv_chain.tip()) .chain_spks(core::iter::once(spk_to_track)), 5, + true, )? .with_confirmation_time_height_anchor(&client)?; @@ -83,6 +84,29 @@ fn scan_detects_confirmed_tx() -> Result<()> { }, ); + for tx in recv_graph.graph().full_txs() { + // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the + // floating txouts available from the transaction's previous outputs. + let fee = recv_graph + .graph() + .calculate_fee(&tx.tx) + .expect("fee must exist"); + + // Retrieve the fee in the transaction data from `bitcoind`. + let tx_fee = env + .bitcoind + .client + .get_transaction(&tx.txid, None) + .expect("Tx must exist") + .fee + .expect("Fee must exist") + .abs() + .to_sat() as u64; + + // Check that the calculated fee matches the fee from the transaction data. + assert_eq!(fee, tx_fee); + } + Ok(()) } @@ -132,6 +156,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> { .sync( SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]), 5, + false, )? .with_confirmation_time_height_anchor(&client)?; @@ -162,6 +187,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> { .sync( SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]), 5, + false, )? .with_confirmation_time_height_anchor(&client)?; diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index a58750c42d..d03ba32dca 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -182,7 +182,7 @@ fn main() -> anyhow::Result<()> { }; let res = client - .full_scan::<_>(request, stop_gap, scan_options.batch_size) + .full_scan::<_>(request, stop_gap, scan_options.batch_size, false) .context("scanning the blockchain")? .with_confirmation_height_anchor(); ( @@ -303,7 +303,7 @@ fn main() -> anyhow::Result<()> { }); let res = client - .sync(request, scan_options.batch_size) + .sync(request, scan_options.batch_size, false) .context("scanning the blockchain")? .with_confirmation_height_anchor(); diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index a9b194ce80..eca96f32a0 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -53,7 +53,7 @@ fn main() -> Result<(), anyhow::Error> { .inspect_spks_for_all_keychains(|_, _, _| std::io::stdout().flush().expect("must flush")); let mut update = client - .full_scan(request, STOP_GAP, BATCH_SIZE)? + .full_scan(request, STOP_GAP, BATCH_SIZE, false)? .with_confirmation_time_height_anchor(&client)?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();