]> Untitled Git - bdk/commitdiff
feat(electrum): include option for previous `TxOut`s for fee calculation
authorWei Chen <wzc110@gmail.com>
Tue, 7 May 2024 11:57:11 +0000 (19:57 +0800)
committer志宇 <hello@evanlinjin.me>
Fri, 10 May 2024 06:54:29 +0000 (14:54 +0800)
The previous `TxOut` for transactions received from an external
wallet may be optionally added as floating `TxOut`s to `TxGraph`
to allow for fee calculation.

crates/electrum/src/electrum_ext.rs
crates/electrum/tests/test_electrum.rs
example-crates/example_electrum/src/main.rs
example-crates/wallet_electrum/src/main.rs

index 806cabeb2dc4f15af581998cbd09e025db0b2e05..8559f50377b24dd8bb5851d8a19c508620ff6426 100644 (file)
@@ -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<K: Ord + Clone>(
         &self,
         request: FullScanRequest<K>,
         stop_gap: usize,
         batch_size: usize,
+        fetch_prev_txouts: bool,
     ) -> Result<ElectrumFullScanResult<K>, 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<ElectrumSyncResult, Error>;
+    fn sync(
+        &self,
+        request: SyncRequest,
+        batch_size: usize,
+        fetch_prev_txouts: bool,
+    ) -> Result<ElectrumSyncResult, Error>;
 }
 
 impl<E: ElectrumApi> ElectrumExt for E {
@@ -53,6 +62,7 @@ impl<E: ElectrumApi> ElectrumExt for E {
         mut request: FullScanRequest<K>,
         stop_gap: usize,
         batch_size: usize,
+        fetch_prev_txouts: bool,
     ) -> Result<ElectrumFullScanResult<K>, Error> {
         let mut request_spks = request.spks_by_keychain;
 
@@ -110,6 +120,11 @@ impl<E: ElectrumApi> 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<E: ElectrumApi> ElectrumExt for E {
         Ok(ElectrumFullScanResult(update))
     }
 
-    fn sync(&self, request: SyncRequest, batch_size: usize) -> Result<ElectrumSyncResult, Error> {
+    fn sync(
+        &self,
+        request: SyncRequest,
+        batch_size: usize,
+        fetch_prev_txouts: bool,
+    ) -> Result<ElectrumSyncResult, Error> {
         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, false)?
             .with_confirmation_height_anchor();
 
         let (tip, _) = construct_update_tip(self, request.chain_tip)?;
@@ -165,6 +185,11 @@ impl<E: ElectrumApi> ElectrumExt for E {
             request.outpoints,
         )?;
 
+        // Fetch previous `TxOut`s for fee calculation if flag is enabled.
+        if fetch_prev_txouts {
+            fetch_prev_txout(self, &mut tx_cache, &mut full_scan_res.graph_update)?;
+        }
+
         Ok(ElectrumSyncResult(SyncResult {
             chain_update: full_scan_res.chain_update,
             graph_update: full_scan_res.graph_update,
@@ -374,7 +399,7 @@ fn populate_with_outpoints(
     client: &impl ElectrumApi,
     cps: &BTreeMap<u32, CheckPoint>,
     tx_cache: &mut TxCache,
-    tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
+    graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
     outpoints: impl IntoIterator<Item = OutPoint>,
 ) -> Result<(), Error> {
     for outpoint in outpoints {
@@ -399,18 +424,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 +449,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 +509,27 @@ fn fetch_tx<C: ElectrumApi>(
     })
 }
 
+// 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<C: ElectrumApi>(
+    client: &C,
+    tx_cache: &mut TxCache,
+    graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
+) -> Result<(), Error> {
+    let full_txs: Vec<Arc<Transaction>> =
+        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<I: Ord + Clone>(
     client: &impl ElectrumApi,
     cps: &BTreeMap<u32, CheckPoint>,
index 9905ab9cc268a0ec698f3e0fa49c1a53574514ab..1077fb8d9227c0b72a4915d1d7b8412c43c4975f 100644 (file)
@@ -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)?;
 
index 237d140a1d3fa78d8d3b7575ebb1c375ba545880..e88b1e6fc3d0eafa6cb7a3f5572ffd86524b7c5e 100644 (file)
@@ -190,7 +190,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();
             (
@@ -311,7 +311,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();
 
index a9b194ce807eda1ffb5399e6dae27964f451162e..eca96f32a05eeb6e69a14454226e52ece10687af 100644 (file)
@@ -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();