Skip to content

Commit ffce704

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 should specify the current time, for example as a UNIX timestamp.
1 parent a837cd3 commit ffce704

File tree

7 files changed

+103
-17
lines changed

7 files changed

+103
-17
lines changed

crates/esplora/src/async_ext.rs

Lines changed: 30 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: Option<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: Option<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: Option<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,11 @@ 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+
if let Some(seen_at) = time {
214+
let _ = graph.insert_seen_at(tx.txid, seen_at);
215+
}
216+
}
207217

208218
let previous_outputs = tx.vin.iter().filter_map(|vin| {
209219
let prevout = vin.prevout.as_ref()?;
@@ -250,6 +260,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
250260
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
251261
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
252262
parallel_requests: usize,
263+
time: Option<u64>,
253264
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
254265
let mut graph = self
255266
.full_scan(
@@ -263,6 +274,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
263274
.into(),
264275
usize::MAX,
265276
parallel_requests,
277+
time,
266278
)
267279
.await
268280
.map(|(g, _)| g)?;
@@ -287,10 +299,16 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
287299
if let Some(anchor) = anchor_from_status(&status) {
288300
let _ = graph.insert_anchor(txid, anchor);
289301
}
302+
if !status.confirmed {
303+
if let Some(seen_at) = time {
304+
let _ = graph.insert_seen_at(txid, seen_at);
305+
}
306+
}
290307
}
291308
}
292309

293310
for op in outpoints.into_iter() {
311+
// get tx for this outpoint
294312
if graph.get_tx(op.txid).is_none() {
295313
if let Some(tx) = self.get_tx(&op.txid).await? {
296314
let _ = graph.insert_tx(tx);
@@ -299,8 +317,14 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
299317
if let Some(anchor) = anchor_from_status(&status) {
300318
let _ = graph.insert_anchor(op.txid, anchor);
301319
}
320+
if !status.confirmed {
321+
if let Some(seen_at) = time {
322+
let _ = graph.insert_seen_at(op.txid, seen_at);
323+
}
324+
}
302325
}
303326

327+
// get spending status of this outpoint
304328
if let Some(op_status) = self.get_output_status(&op.txid, op.vout as _).await? {
305329
if let Some(txid) = op_status.txid {
306330
if graph.get_tx(txid).is_none() {
@@ -311,6 +335,11 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
311335
if let Some(anchor) = anchor_from_status(&status) {
312336
let _ = graph.insert_anchor(txid, anchor);
313337
}
338+
if !status.confirmed {
339+
if let Some(seen_at) = time {
340+
let _ = graph.insert_seen_at(txid, seen_at);
341+
}
342+
}
314343
}
315344
}
316345
}

crates/esplora/src/blocking_ext.rs

Lines changed: 30 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 time, 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: Option<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: Option<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: Option<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,11 @@ 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+
if let Some(seen_at) = time {
204+
let _ = graph.insert_seen_at(tx.txid, seen_at);
205+
}
206+
}
197207

198208
let previous_outputs = tx.vin.iter().filter_map(|vin| {
199209
let prevout = vin.prevout.as_ref()?;
@@ -240,6 +250,7 @@ impl EsploraExt for esplora_client::BlockingClient {
240250
txids: impl IntoIterator<Item = Txid>,
241251
outpoints: impl IntoIterator<Item = OutPoint>,
242252
parallel_requests: usize,
253+
time: Option<u64>,
243254
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
244255
let mut graph = self
245256
.full_scan(
@@ -253,6 +264,7 @@ impl EsploraExt for esplora_client::BlockingClient {
253264
.into(),
254265
usize::MAX,
255266
parallel_requests,
267+
time,
256268
)
257269
.map(|(g, _)| g)?;
258270

@@ -284,10 +296,16 @@ impl EsploraExt for esplora_client::BlockingClient {
284296
if let Some(anchor) = anchor_from_status(&status) {
285297
let _ = graph.insert_anchor(txid, anchor);
286298
}
299+
if !status.confirmed {
300+
if let Some(seen_at) = time {
301+
let _ = graph.insert_seen_at(txid, seen_at);
302+
}
303+
}
287304
}
288305
}
289306

290307
for op in outpoints {
308+
// get tx for this outpoint
291309
if graph.get_tx(op.txid).is_none() {
292310
if let Some(tx) = self.get_tx(&op.txid)? {
293311
let _ = graph.insert_tx(tx);
@@ -296,8 +314,14 @@ impl EsploraExt for esplora_client::BlockingClient {
296314
if let Some(anchor) = anchor_from_status(&status) {
297315
let _ = graph.insert_anchor(op.txid, anchor);
298316
}
317+
if !status.confirmed {
318+
if let Some(seen_at) = time {
319+
let _ = graph.insert_seen_at(op.txid, seen_at);
320+
}
321+
}
299322
}
300323

324+
// get spending status of this outpoint
301325
if let Some(op_status) = self.get_output_status(&op.txid, op.vout as _)? {
302326
if let Some(txid) = op_status.txid {
303327
if graph.get_tx(txid).is_none() {
@@ -308,6 +332,11 @@ impl EsploraExt for esplora_client::BlockingClient {
308332
if let Some(anchor) = anchor_from_status(&status) {
309333
let _ = graph.insert_anchor(txid, anchor);
310334
}
335+
if !status.confirmed {
336+
if let Some(seen_at) = time {
337+
let _ = graph.insert_seen_at(txid, seen_at);
338+
}
339+
}
311340
}
312341
}
313342
}

crates/esplora/tests/async_ext.rs

Lines changed: 8 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+
None,
109110
)
110111
.await?;
111112

@@ -188,10 +189,12 @@ 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) =
193+
env.client.full_scan(keychains.clone(), 2, 1, None).await?;
192194
assert!(graph_update.full_txs().next().is_none());
193195
assert!(active_indices.is_empty());
194-
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1).await?;
196+
let (graph_update, active_indices) =
197+
env.client.full_scan(keychains.clone(), 3, 1, None).await?;
195198
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
196199
assert_eq!(active_indices[&0], 3);
197200

@@ -213,12 +216,13 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
213216

214217
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
215218
// 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?;
219+
let (graph_update, active_indices) =
220+
env.client.full_scan(keychains.clone(), 4, 1, None).await?;
217221
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
218222
assert_eq!(txs.len(), 1);
219223
assert!(txs.contains(&txid_4th_addr));
220224
assert_eq!(active_indices[&0], 3);
221-
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1).await?;
225+
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1, None).await?;
222226
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
223227
assert_eq!(txs.len(), 2);
224228
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+
None,
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, None)?;
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, None)?;
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, None)?;
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, None)?;
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: 20 additions & 3 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+
Some(now),
202+
)
194203
.context("scanning for transactions")?;
195204

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

310-
let graph_update =
311-
client.sync(spks, txids, outpoints, scan_options.parallel_requests)?;
319+
let now = time::SystemTime::now()
320+
.duration_since(time::UNIX_EPOCH)?
321+
.as_secs();
322+
let graph_update = client.sync(
323+
spks,
324+
txids,
325+
outpoints,
326+
scan_options.parallel_requests,
327+
Some(now),
328+
)?;
312329

313330
graph.lock().unwrap().apply_update(graph_update)
314331
}

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, Some(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, Some(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)