Skip to content

Commit 3162957

Browse files
committed
fix(esplora)!: Change sync, full_scan to take a timestamp parameter
This is used for setting the time a transaction was last seen in mempool. When doing a sync or full scan, the caller must specify the current time, for example as a UNIX timestamp.
1 parent a837cd3 commit 3162957

File tree

7 files changed

+78
-16
lines changed

7 files changed

+78
-16
lines changed

crates/esplora/src/async_ext.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ pub trait EsploraAsyncExt {
5151
///
5252
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
5353
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
54-
/// parallel.
54+
/// parallel. `time` is the current time, typically a UNIX timestamp, used only when setting
55+
/// the time a transaction was last seen unconfirmed.
5556
async fn full_scan<K: Ord + Clone + Send>(
5657
&self,
5758
keychain_spks: BTreeMap<
@@ -60,6 +61,7 @@ pub trait EsploraAsyncExt {
6061
>,
6162
stop_gap: usize,
6263
parallel_requests: usize,
64+
time: u64,
6365
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
6466

6567
/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
@@ -69,6 +71,7 @@ pub trait EsploraAsyncExt {
6971
/// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
7072
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
7173
/// want to include in the update
74+
/// * `time`: UNIX timestamp used to set the time a transaction was last seen unconfirmed
7275
///
7376
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
7477
/// may include scripts that have been used, use [`full_scan`] with the keychain.
@@ -80,6 +83,7 @@ pub trait EsploraAsyncExt {
8083
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
8184
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
8285
parallel_requests: usize,
86+
time: u64,
8387
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error>;
8488
}
8589

@@ -157,6 +161,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
157161
>,
158162
stop_gap: usize,
159163
parallel_requests: usize,
164+
time: u64,
160165
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
161166
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
162167
let parallel_requests = Ord::max(parallel_requests, 1);
@@ -204,6 +209,9 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
204209
if let Some(anchor) = anchor_from_status(&tx.status) {
205210
let _ = graph.insert_anchor(tx.txid, anchor);
206211
}
212+
if !tx.status.confirmed {
213+
let _ = graph.insert_seen_at(tx.txid, time);
214+
}
207215

208216
let previous_outputs = tx.vin.iter().filter_map(|vin| {
209217
let prevout = vin.prevout.as_ref()?;
@@ -250,6 +258,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
250258
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
251259
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
252260
parallel_requests: usize,
261+
time: u64,
253262
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
254263
let mut graph = self
255264
.full_scan(
@@ -263,6 +272,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
263272
.into(),
264273
usize::MAX,
265274
parallel_requests,
275+
time,
266276
)
267277
.await
268278
.map(|(g, _)| g)?;
@@ -287,10 +297,14 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
287297
if let Some(anchor) = anchor_from_status(&status) {
288298
let _ = graph.insert_anchor(txid, anchor);
289299
}
300+
if !status.confirmed {
301+
let _ = graph.insert_seen_at(txid, time);
302+
}
290303
}
291304
}
292305

293306
for op in outpoints.into_iter() {
307+
// get tx for this outpoint
294308
if graph.get_tx(op.txid).is_none() {
295309
if let Some(tx) = self.get_tx(&op.txid).await? {
296310
let _ = graph.insert_tx(tx);
@@ -299,8 +313,12 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
299313
if let Some(anchor) = anchor_from_status(&status) {
300314
let _ = graph.insert_anchor(op.txid, anchor);
301315
}
316+
if !status.confirmed {
317+
let _ = graph.insert_seen_at(op.txid, time);
318+
}
302319
}
303320

321+
// get spending status of this outpoint
304322
if let Some(op_status) = self.get_output_status(&op.txid, op.vout as _).await? {
305323
if let Some(txid) = op_status.txid {
306324
if graph.get_tx(txid).is_none() {
@@ -311,6 +329,9 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
311329
if let Some(anchor) = anchor_from_status(&status) {
312330
let _ = graph.insert_anchor(txid, anchor);
313331
}
332+
if !status.confirmed {
333+
let _ = graph.insert_seen_at(txid, time);
334+
}
314335
}
315336
}
316337
}

crates/esplora/src/blocking_ext.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@ pub trait EsploraExt {
4949
///
5050
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
5151
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
52-
/// parallel.
52+
/// parallel. `time` is the current time, typically a UNIX timestamp, used only when setting
53+
/// the time a transaction was last seen unconfirmed.
5354
fn full_scan<K: Ord + Clone>(
5455
&self,
5556
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
5657
stop_gap: usize,
5758
parallel_requests: usize,
59+
time: u64,
5860
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
5961

6062
/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
@@ -64,6 +66,7 @@ pub trait EsploraExt {
6466
/// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
6567
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
6668
/// want to include in the update
69+
/// * `time`: UNIX timestamp used to set the time a transaction was last seen unconfirmed
6770
///
6871
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
6972
/// may include scripts that have been used, use [`full_scan`] with the keychain.
@@ -75,6 +78,7 @@ pub trait EsploraExt {
7578
txids: impl IntoIterator<Item = Txid>,
7679
outpoints: impl IntoIterator<Item = OutPoint>,
7780
parallel_requests: usize,
81+
time: u64,
7882
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error>;
7983
}
8084

@@ -144,6 +148,7 @@ impl EsploraExt for esplora_client::BlockingClient {
144148
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
145149
stop_gap: usize,
146150
parallel_requests: usize,
151+
time: u64,
147152
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
148153
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
149154
let parallel_requests = Ord::max(parallel_requests, 1);
@@ -194,6 +199,9 @@ impl EsploraExt for esplora_client::BlockingClient {
194199
if let Some(anchor) = anchor_from_status(&tx.status) {
195200
let _ = graph.insert_anchor(tx.txid, anchor);
196201
}
202+
if !tx.status.confirmed {
203+
let _ = graph.insert_seen_at(tx.txid, time);
204+
}
197205

198206
let previous_outputs = tx.vin.iter().filter_map(|vin| {
199207
let prevout = vin.prevout.as_ref()?;
@@ -240,6 +248,7 @@ impl EsploraExt for esplora_client::BlockingClient {
240248
txids: impl IntoIterator<Item = Txid>,
241249
outpoints: impl IntoIterator<Item = OutPoint>,
242250
parallel_requests: usize,
251+
time: u64,
243252
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
244253
let mut graph = self
245254
.full_scan(
@@ -253,6 +262,7 @@ impl EsploraExt for esplora_client::BlockingClient {
253262
.into(),
254263
usize::MAX,
255264
parallel_requests,
265+
time,
256266
)
257267
.map(|(g, _)| g)?;
258268

@@ -284,10 +294,14 @@ impl EsploraExt for esplora_client::BlockingClient {
284294
if let Some(anchor) = anchor_from_status(&status) {
285295
let _ = graph.insert_anchor(txid, anchor);
286296
}
297+
if !status.confirmed {
298+
let _ = graph.insert_seen_at(txid, time);
299+
}
287300
}
288301
}
289302

290303
for op in outpoints {
304+
// get tx for this outpoint
291305
if graph.get_tx(op.txid).is_none() {
292306
if let Some(tx) = self.get_tx(&op.txid)? {
293307
let _ = graph.insert_tx(tx);
@@ -296,8 +310,12 @@ impl EsploraExt for esplora_client::BlockingClient {
296310
if let Some(anchor) = anchor_from_status(&status) {
297311
let _ = graph.insert_anchor(op.txid, anchor);
298312
}
313+
if !status.confirmed {
314+
let _ = graph.insert_seen_at(op.txid, time);
315+
}
299316
}
300317

318+
// get spending status of this outpoint
301319
if let Some(op_status) = self.get_output_status(&op.txid, op.vout as _)? {
302320
if let Some(txid) = op_status.txid {
303321
if graph.get_tx(txid).is_none() {
@@ -308,6 +326,9 @@ impl EsploraExt for esplora_client::BlockingClient {
308326
if let Some(anchor) = anchor_from_status(&status) {
309327
let _ = graph.insert_anchor(txid, anchor);
310328
}
329+
if !status.confirmed {
330+
let _ = graph.insert_seen_at(txid, time);
331+
}
311332
}
312333
}
313334
}

crates/esplora/tests/async_ext.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
106106
vec![].into_iter(),
107107
vec![].into_iter(),
108108
1,
109+
0,
109110
)
110111
.await?;
111112

@@ -188,10 +189,10 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
188189

189190
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
190191
// will.
191-
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1).await?;
192+
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1, 0).await?;
192193
assert!(graph_update.full_txs().next().is_none());
193194
assert!(active_indices.is_empty());
194-
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1).await?;
195+
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1, 0).await?;
195196
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
196197
assert_eq!(active_indices[&0], 3);
197198

@@ -213,12 +214,12 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
213214

214215
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
215216
// The last active indice won't be updated in the first case but will in the second one.
216-
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1).await?;
217+
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1, 0).await?;
217218
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
218219
assert_eq!(txs.len(), 1);
219220
assert!(txs.contains(&txid_4th_addr));
220221
assert_eq!(active_indices[&0], 3);
221-
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1).await?;
222+
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1, 0).await?;
222223
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
223224
assert_eq!(txs.len(), 2);
224225
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));

crates/esplora/tests/blocking_ext.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
134134
vec![].into_iter(),
135135
vec![].into_iter(),
136136
1,
137+
0,
137138
)?;
138139

139140
// Check to see if we have the floating txouts available from our two created transactions'
@@ -216,10 +217,10 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
216217

217218
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
218219
// will.
219-
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1)?;
220+
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1, 0)?;
220221
assert!(graph_update.full_txs().next().is_none());
221222
assert!(active_indices.is_empty());
222-
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1)?;
223+
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1, 0)?;
223224
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
224225
assert_eq!(active_indices[&0], 3);
225226

@@ -241,12 +242,12 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
241242

242243
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
243244
// The last active indice won't be updated in the first case but will in the second one.
244-
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1)?;
245+
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1, 0)?;
245246
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
246247
assert_eq!(txs.len(), 1);
247248
assert!(txs.contains(&txid_4th_addr));
248249
assert_eq!(active_indices[&0], 3);
249-
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1)?;
250+
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1, 0)?;
250251
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
251252
assert_eq!(txs.len(), 2);
252253
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));

example-crates/example_esplora/src/main.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::{
22
collections::{BTreeMap, BTreeSet},
33
io::{self, Write},
44
sync::Mutex,
5+
time,
56
};
67

78
use bdk_chain::{
@@ -189,8 +190,16 @@ fn main() -> anyhow::Result<()> {
189190
// is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
190191
// represents the last active spk derivation indices of keychains
191192
// (`keychain_indices_update`).
193+
let now = time::SystemTime::now()
194+
.duration_since(time::UNIX_EPOCH)?
195+
.as_secs();
192196
let (graph_update, last_active_indices) = client
193-
.full_scan(keychain_spks, *stop_gap, scan_options.parallel_requests)
197+
.full_scan(
198+
keychain_spks,
199+
*stop_gap,
200+
scan_options.parallel_requests,
201+
now,
202+
)
194203
.context("scanning for transactions")?;
195204

196205
let mut graph = graph.lock().expect("mutex must not be poisoned");
@@ -307,8 +316,11 @@ fn main() -> anyhow::Result<()> {
307316
}
308317
}
309318

319+
let now = time::SystemTime::now()
320+
.duration_since(time::UNIX_EPOCH)?
321+
.as_secs();
310322
let graph_update =
311-
client.sync(spks, txids, outpoints, scan_options.parallel_requests)?;
323+
client.sync(spks, txids, outpoints, scan_options.parallel_requests, now)?;
312324

313325
graph.lock().unwrap().apply_update(graph_update)
314326
}

example-crates/wallet_esplora_async/src/main.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{io::Write, str::FromStr};
1+
use std::{io::Write, str::FromStr, time};
22

33
use bdk::{
44
bitcoin::{Address, Network},
@@ -53,8 +53,11 @@ async fn main() -> Result<(), anyhow::Error> {
5353
(k, k_spks)
5454
})
5555
.collect();
56+
let now = time::SystemTime::now()
57+
.duration_since(time::UNIX_EPOCH)?
58+
.as_secs();
5659
let (update_graph, last_active_indices) = client
57-
.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)
60+
.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS, now)
5861
.await?;
5962
let missing_heights = update_graph.missing_heights(wallet.local_chain());
6063
let chain_update = client.update_local_chain(prev_tip, missing_heights).await?;

example-crates/wallet_esplora_blocking/src/main.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const SEND_AMOUNT: u64 = 1000;
33
const STOP_GAP: usize = 5;
44
const PARALLEL_REQUESTS: usize = 1;
55

6-
use std::{io::Write, str::FromStr};
6+
use std::{io::Write, str::FromStr, time};
77

88
use bdk::{
99
bitcoin::{Address, Network},
@@ -53,8 +53,11 @@ fn main() -> Result<(), anyhow::Error> {
5353
})
5454
.collect();
5555

56+
let now = time::SystemTime::now()
57+
.duration_since(time::UNIX_EPOCH)?
58+
.as_secs();
5659
let (update_graph, last_active_indices) =
57-
client.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)?;
60+
client.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS, now)?;
5861
let missing_heights = update_graph.missing_heights(wallet.local_chain());
5962
let chain_update = client.update_local_chain(prev_tip, missing_heights)?;
6063
let update = Update {

0 commit comments

Comments
 (0)