Skip to content

Commit 93a4ae6

Browse files
committed
feat(electrum): include option for previous TxOuts for fee calculation
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.
1 parent 7a69d35 commit 93a4ae6

File tree

4 files changed

+99
-14
lines changed

4 files changed

+99
-14
lines changed

crates/electrum/src/electrum_ext.rs

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ pub trait ElectrumExt {
2323
///
2424
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
2525
/// transactions. `batch_size` specifies the max number of script pubkeys to request for in a
26-
/// single batch request.
26+
/// single batch request. `fetch_prev_txouts` specifies whether or not we want previous `TxOut`s
27+
/// for fee calculation.
2728
fn full_scan<K: Ord + Clone>(
2829
&self,
2930
request: FullScanRequest<K>,
3031
stop_gap: usize,
3132
batch_size: usize,
33+
fetch_prev_txouts: bool,
3234
) -> Result<ElectrumFullScanResult<K>, Error>;
3335

3436
/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
@@ -38,13 +40,19 @@ pub trait ElectrumExt {
3840
/// see [`SyncRequest`]
3941
///
4042
/// `batch_size` specifies the max number of script pubkeys to request for in a single batch
41-
/// request.
43+
/// request. `fetch_prev_txouts` specifies whether or not we want previous `TxOut`s for fee
44+
/// calculation.
4245
///
4346
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
4447
/// may include scripts that have been used, use [`full_scan`] with the keychain.
4548
///
4649
/// [`full_scan`]: ElectrumExt::full_scan
47-
fn sync(&self, request: SyncRequest, batch_size: usize) -> Result<ElectrumSyncResult, Error>;
50+
fn sync(
51+
&self,
52+
request: SyncRequest,
53+
batch_size: usize,
54+
fetch_prev_txouts: bool,
55+
) -> Result<ElectrumSyncResult, Error>;
4856
}
4957

5058
impl<E: ElectrumApi> ElectrumExt for E {
@@ -53,6 +61,7 @@ impl<E: ElectrumApi> ElectrumExt for E {
5361
mut request: FullScanRequest<K>,
5462
stop_gap: usize,
5563
batch_size: usize,
64+
fetch_prev_txouts: bool,
5665
) -> Result<ElectrumFullScanResult<K>, Error> {
5766
let mut request_spks = request.spks_by_keychain;
5867

@@ -104,6 +113,18 @@ impl<E: ElectrumApi> ElectrumExt for E {
104113
}
105114
}
106115

116+
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
117+
if fetch_prev_txouts {
118+
let _ = graph_update.apply_update(fetch_prev_txout(
119+
self,
120+
&mut request.tx_cache,
121+
graph_update
122+
.full_txs()
123+
.map(|tx_node| tx_node.tx)
124+
.collect::<Vec<_>>(),
125+
)?);
126+
}
127+
107128
// check for reorgs during scan process
108129
let server_blockhash = self.block_header(tip.height() as usize)?.block_hash();
109130
if tip.hash() != server_blockhash {
@@ -133,14 +154,19 @@ impl<E: ElectrumApi> ElectrumExt for E {
133154
Ok(ElectrumFullScanResult(update))
134155
}
135156

136-
fn sync(&self, request: SyncRequest, batch_size: usize) -> Result<ElectrumSyncResult, Error> {
157+
fn sync(
158+
&self,
159+
request: SyncRequest,
160+
batch_size: usize,
161+
fetch_prev_txouts: bool,
162+
) -> Result<ElectrumSyncResult, Error> {
137163
let mut tx_cache = request.tx_cache.clone();
138164

139165
let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
140166
.cache_txs(request.tx_cache)
141167
.set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
142168
let mut full_scan_res = self
143-
.full_scan(full_scan_req, usize::MAX, batch_size)?
169+
.full_scan(full_scan_req, usize::MAX, batch_size, fetch_prev_txouts)?
144170
.with_confirmation_height_anchor();
145171

146172
let (tip, _) = construct_update_tip(self, request.chain_tip)?;
@@ -165,6 +191,19 @@ impl<E: ElectrumApi> ElectrumExt for E {
165191
request.outpoints,
166192
)?;
167193

194+
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
195+
if fetch_prev_txouts {
196+
let _ = full_scan_res.graph_update.apply_update(fetch_prev_txout(
197+
self,
198+
&mut tx_cache,
199+
full_scan_res
200+
.graph_update
201+
.full_txs()
202+
.map(|tx_node| tx_node.tx)
203+
.collect::<Vec<_>>(),
204+
)?);
205+
}
206+
168207
Ok(ElectrumSyncResult(SyncResult {
169208
chain_update: full_scan_res.chain_update,
170209
graph_update: full_scan_res.graph_update,
@@ -374,7 +413,7 @@ fn populate_with_outpoints(
374413
client: &impl ElectrumApi,
375414
cps: &BTreeMap<u32, CheckPoint>,
376415
tx_cache: &mut TxCache,
377-
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
416+
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
378417
outpoints: impl IntoIterator<Item = OutPoint>,
379418
) -> Result<(), Error> {
380419
for outpoint in outpoints {
@@ -399,18 +438,18 @@ fn populate_with_outpoints(
399438
continue;
400439
}
401440
has_residing = true;
402-
if tx_graph.get_tx(res.tx_hash).is_none() {
403-
let _ = tx_graph.insert_tx(tx.clone());
441+
if graph_update.get_tx(res.tx_hash).is_none() {
442+
let _ = graph_update.insert_tx(tx.clone());
404443
}
405444
} else {
406445
if has_spending {
407446
continue;
408447
}
409-
let res_tx = match tx_graph.get_tx(res.tx_hash) {
448+
let res_tx = match graph_update.get_tx(res.tx_hash) {
410449
Some(tx) => tx,
411450
None => {
412451
let res_tx = fetch_tx(client, tx_cache, res.tx_hash)?;
413-
let _ = tx_graph.insert_tx(Arc::clone(&res_tx));
452+
let _ = graph_update.insert_tx(Arc::clone(&res_tx));
414453
res_tx
415454
}
416455
};
@@ -424,7 +463,7 @@ fn populate_with_outpoints(
424463
};
425464

426465
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
427-
let _ = tx_graph.insert_anchor(res.tx_hash, anchor);
466+
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
428467
}
429468
}
430469
}
@@ -484,6 +523,26 @@ fn fetch_tx<C: ElectrumApi>(
484523
})
485524
}
486525

526+
// Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
527+
// which we do not have by default. This data is needed to calculate the transaction fee.
528+
fn fetch_prev_txout<C: ElectrumApi>(
529+
client: &C,
530+
tx_cache: &mut TxCache,
531+
full_txs: Vec<Arc<Transaction>>,
532+
) -> Result<TxGraph<ConfirmationHeightAnchor>, Error> {
533+
let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
534+
for tx in full_txs {
535+
for vin in tx.input.clone() {
536+
let outpoint = vin.previous_output;
537+
let prev_tx = fetch_tx(client, tx_cache, outpoint.txid)?;
538+
for txout in prev_tx.output.clone() {
539+
let _ = graph_update.insert_txout(outpoint, txout);
540+
}
541+
}
542+
}
543+
Ok(graph_update)
544+
}
545+
487546
fn populate_with_spks<I: Ord + Clone>(
488547
client: &impl ElectrumApi,
489548
cps: &BTreeMap<u32, CheckPoint>,

crates/electrum/tests/test_electrum.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ fn scan_detects_confirmed_tx() -> Result<()> {
6666
SyncRequest::from_chain_tip(recv_chain.tip())
6767
.chain_spks(core::iter::once(spk_to_track)),
6868
5,
69+
true,
6970
)?
7071
.with_confirmation_time_height_anchor(&client)?;
7172

@@ -83,6 +84,29 @@ fn scan_detects_confirmed_tx() -> Result<()> {
8384
},
8485
);
8586

87+
for tx in recv_graph.graph().full_txs() {
88+
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
89+
// floating txouts available from the transaction's previous outputs.
90+
let fee = recv_graph
91+
.graph()
92+
.calculate_fee(&tx.tx)
93+
.expect("fee must exist");
94+
95+
// Retrieve the fee in the transaction data from `bitcoind`.
96+
let tx_fee = env
97+
.bitcoind
98+
.client
99+
.get_transaction(&tx.txid, None)
100+
.expect("Tx must exist")
101+
.fee
102+
.expect("Fee must exist")
103+
.abs()
104+
.to_sat() as u64;
105+
106+
// Check that the calculated fee matches the fee from the transaction data.
107+
assert_eq!(fee, tx_fee);
108+
}
109+
86110
Ok(())
87111
}
88112

@@ -132,6 +156,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
132156
.sync(
133157
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
134158
5,
159+
false,
135160
)?
136161
.with_confirmation_time_height_anchor(&client)?;
137162

@@ -162,6 +187,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
162187
.sync(
163188
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
164189
5,
190+
false,
165191
)?
166192
.with_confirmation_time_height_anchor(&client)?;
167193

example-crates/example_electrum/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ fn main() -> anyhow::Result<()> {
182182
};
183183

184184
let res = client
185-
.full_scan::<_>(request, stop_gap, scan_options.batch_size)
185+
.full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
186186
.context("scanning the blockchain")?
187187
.with_confirmation_height_anchor();
188188
(
@@ -303,7 +303,7 @@ fn main() -> anyhow::Result<()> {
303303
});
304304

305305
let res = client
306-
.sync(request, scan_options.batch_size)
306+
.sync(request, scan_options.batch_size, false)
307307
.context("scanning the blockchain")?
308308
.with_confirmation_height_anchor();
309309

example-crates/wallet_electrum/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ fn main() -> Result<(), anyhow::Error> {
5353
.inspect_spks_for_all_keychains(|_, _, _| std::io::stdout().flush().expect("must flush"));
5454

5555
let mut update = client
56-
.full_scan(request, STOP_GAP, BATCH_SIZE)?
56+
.full_scan(request, STOP_GAP, BATCH_SIZE, false)?
5757
.with_confirmation_time_height_anchor(&client)?;
5858

5959
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();

0 commit comments

Comments
 (0)