Skip to content

Commit a39a7e9

Browse files
committed
feat(electrum)!: use new sync/full-scan structs for ElectrumExt
`ElectrumResultExt` trait is also introduced that adds methods which can convert the `Anchor` type for the update `TxGraph`. Examples and tests are updated to use the new `ElectrumExt` API.
1 parent 7217a88 commit a39a7e9

File tree

5 files changed

+218
-294
lines changed

5 files changed

+218
-294
lines changed

crates/electrum/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ readme = "README.md"
1212
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1313

1414
[dependencies]
15-
bdk_chain = { path = "../chain", version = "0.13.0", default-features = false }
15+
bdk_chain = { path = "../chain", version = "0.13.0" }
1616
electrum-client = { version = "0.19" }
1717
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
1818

crates/electrum/src/electrum_ext.rs

Lines changed: 111 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -2,87 +2,17 @@ use bdk_chain::{
22
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
33
collections::{BTreeMap, HashMap, HashSet},
44
local_chain::CheckPoint,
5+
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult, TxCache},
56
tx_graph::TxGraph,
67
BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
78
};
8-
use core::{fmt::Debug, str::FromStr};
9+
use core::str::FromStr;
910
use electrum_client::{ElectrumApi, Error, HeaderNotification};
1011
use std::sync::Arc;
1112

1213
/// We include a chain suffix of a certain length for the purpose of robustness.
1314
const CHAIN_SUFFIX_LENGTH: u32 = 8;
1415

15-
/// Type that maintains a cache of [`Arc`]-wrapped transactions.
16-
pub type TxCache = HashMap<Txid, Arc<Transaction>>;
17-
18-
/// Combination of chain and transactions updates from electrum
19-
///
20-
/// We have to update the chain and the txids at the same time since we anchor the txids to
21-
/// the same chain tip that we check before and after we gather the txids.
22-
#[derive(Debug)]
23-
pub struct ElectrumUpdate<A = ConfirmationHeightAnchor> {
24-
/// Chain update
25-
pub chain_update: CheckPoint,
26-
/// Tracks electrum updates in TxGraph
27-
pub graph_update: TxGraph<A>,
28-
}
29-
30-
impl<A: Clone + Ord> ElectrumUpdate<A> {
31-
/// Transform the [`ElectrumUpdate`] to have [`bdk_chain::Anchor`]s of another type.
32-
///
33-
/// Refer to [`TxGraph::map_anchors`].
34-
pub fn map_anchors<A2: Clone + Ord, F>(self, f: F) -> ElectrumUpdate<A2>
35-
where
36-
F: FnMut(A) -> A2,
37-
{
38-
ElectrumUpdate {
39-
chain_update: self.chain_update,
40-
graph_update: self.graph_update.map_anchors(f),
41-
}
42-
}
43-
}
44-
45-
impl ElectrumUpdate {
46-
/// Transforms the [`TxGraph`]'s [`bdk_chain::Anchor`] type to [`ConfirmationTimeHeightAnchor`].
47-
pub fn into_confirmation_time_update(
48-
self,
49-
client: &impl ElectrumApi,
50-
) -> Result<ElectrumUpdate<ConfirmationTimeHeightAnchor>, Error> {
51-
let relevant_heights = self
52-
.graph_update
53-
.all_anchors()
54-
.iter()
55-
.map(|(a, _)| a.confirmation_height)
56-
.collect::<HashSet<_>>();
57-
58-
let height_to_time = relevant_heights
59-
.clone()
60-
.into_iter()
61-
.zip(
62-
client
63-
.batch_block_header(relevant_heights)?
64-
.into_iter()
65-
.map(|bh| bh.time as u64),
66-
)
67-
.collect::<HashMap<u32, u64>>();
68-
69-
let chain_update = self.chain_update;
70-
let graph_update =
71-
self.graph_update
72-
.clone()
73-
.map_anchors(|a| ConfirmationTimeHeightAnchor {
74-
anchor_block: a.anchor_block,
75-
confirmation_height: a.confirmation_height,
76-
confirmation_time: height_to_time[&a.confirmation_height],
77-
});
78-
79-
Ok(ElectrumUpdate {
80-
chain_update,
81-
graph_update,
82-
})
83-
}
84-
}
85-
8616
/// Trait to extend [`electrum_client::Client`] functionality.
8717
pub trait ElectrumExt {
8818
/// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
@@ -97,12 +27,10 @@ pub trait ElectrumExt {
9727
/// single batch request.
9828
fn full_scan<K: Ord + Clone>(
9929
&self,
100-
tx_cache: &mut TxCache,
101-
prev_tip: CheckPoint,
102-
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
30+
request: FullScanRequest<K>,
10331
stop_gap: usize,
10432
batch_size: usize,
105-
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error>;
33+
) -> Result<FullScanResult<K, ConfirmationHeightAnchor>, Error>;
10634

10735
/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
10836
/// and returns updates for [`bdk_chain`] data structures.
@@ -123,28 +51,19 @@ pub trait ElectrumExt {
12351
/// [`full_scan`]: ElectrumExt::full_scan
12452
fn sync(
12553
&self,
126-
tx_cache: &mut TxCache,
127-
prev_tip: CheckPoint,
128-
misc_spks: impl IntoIterator<Item = ScriptBuf>,
129-
txids: impl IntoIterator<Item = Txid>,
130-
outpoints: impl IntoIterator<Item = OutPoint>,
54+
request: SyncRequest,
13155
batch_size: usize,
132-
) -> Result<ElectrumUpdate, Error>;
56+
) -> Result<SyncResult<ConfirmationHeightAnchor>, Error>;
13357
}
13458

13559
impl<E: ElectrumApi> ElectrumExt for E {
13660
fn full_scan<K: Ord + Clone>(
13761
&self,
138-
tx_cache: &mut TxCache,
139-
prev_tip: CheckPoint,
140-
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
62+
mut request: FullScanRequest<K>,
14163
stop_gap: usize,
14264
batch_size: usize,
143-
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
144-
let mut request_spks = keychain_spks
145-
.into_iter()
146-
.map(|(k, s)| (k, s.into_iter()))
147-
.collect::<BTreeMap<K, _>>();
65+
) -> Result<FullScanResult<K, ConfirmationHeightAnchor>, Error> {
66+
let mut request_spks = request.spks_by_keychain;
14867

14968
// We keep track of already-scanned spks just in case a reorg happens and we need to do a
15069
// rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so
@@ -154,8 +73,8 @@ impl<E: ElectrumApi> ElectrumExt for E {
15473
// * val: (script_pubkey, has_tx_history).
15574
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
15675

157-
let (electrum_update, keychain_update) = loop {
158-
let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
76+
let update = loop {
77+
let (tip, _) = construct_update_tip(self, request.chain_tip.clone())?;
15978
let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
16079
let cps = tip
16180
.iter()
@@ -168,7 +87,7 @@ impl<E: ElectrumApi> ElectrumExt for E {
16887
scanned_spks.append(&mut populate_with_spks(
16988
self,
17089
&cps,
171-
tx_cache,
90+
&mut request.tx_cache,
17291
&mut graph_update,
17392
&mut scanned_spks
17493
.iter()
@@ -182,7 +101,7 @@ impl<E: ElectrumApi> ElectrumExt for E {
182101
populate_with_spks(
183102
self,
184103
&cps,
185-
tx_cache,
104+
&mut request.tx_cache,
186105
&mut graph_update,
187106
keychain_spks,
188107
stop_gap,
@@ -213,55 +132,118 @@ impl<E: ElectrumApi> ElectrumExt for E {
213132
})
214133
.collect::<BTreeMap<_, _>>();
215134

216-
break (
217-
ElectrumUpdate {
218-
chain_update,
219-
graph_update,
220-
},
221-
keychain_update,
222-
);
135+
break FullScanResult {
136+
graph_update,
137+
chain_update,
138+
last_active_indices: keychain_update,
139+
};
223140
};
224141

225-
Ok((electrum_update, keychain_update))
142+
Ok(update)
226143
}
227144

228145
fn sync(
229146
&self,
230-
tx_cache: &mut TxCache,
231-
prev_tip: CheckPoint,
232-
misc_spks: impl IntoIterator<Item = ScriptBuf>,
233-
txids: impl IntoIterator<Item = Txid>,
234-
outpoints: impl IntoIterator<Item = OutPoint>,
147+
request: SyncRequest,
235148
batch_size: usize,
236-
) -> Result<ElectrumUpdate, Error> {
237-
let spk_iter = misc_spks
238-
.into_iter()
239-
.enumerate()
240-
.map(|(i, spk)| (i as u32, spk));
241-
242-
let (electrum_update, _) = self.full_scan(
243-
tx_cache,
244-
prev_tip.clone(),
245-
[((), spk_iter)].into(),
246-
usize::MAX,
247-
batch_size,
248-
)?;
249-
250-
let (tip, _) = construct_update_tip(self, prev_tip)?;
149+
) -> Result<SyncResult<ConfirmationHeightAnchor>, Error> {
150+
let mut tx_cache = request.tx_cache.clone();
151+
152+
let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
153+
.cache_txs(request.tx_cache)
154+
.set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
155+
let full_scan_res = self.full_scan(full_scan_req, usize::MAX, batch_size)?;
156+
157+
let (tip, _) = construct_update_tip(self, request.chain_tip)?;
251158
let cps = tip
252159
.iter()
253160
.take(10)
254161
.map(|cp| (cp.height(), cp))
255162
.collect::<BTreeMap<u32, CheckPoint>>();
256163

257164
let mut tx_graph = TxGraph::<ConfirmationHeightAnchor>::default();
258-
populate_with_txids(self, &cps, tx_cache, &mut tx_graph, txids)?;
259-
populate_with_outpoints(self, &cps, &mut tx_graph, outpoints)?;
165+
populate_with_txids(self, &cps, &mut tx_cache, &mut tx_graph, request.txids)?;
166+
populate_with_outpoints(self, &cps, &mut tx_cache, &mut tx_graph, request.outpoints)?;
167+
168+
Ok(SyncResult {
169+
chain_update: full_scan_res.chain_update,
170+
graph_update: full_scan_res.graph_update,
171+
})
172+
}
173+
}
174+
175+
/// Trait that extends [`SyncResult`] and [`FullScanResult`] functionality.
176+
///
177+
/// Currently, only a single method exists that converts the update [`TxGraph`] to have an anchor
178+
/// type of [`ConfirmationTimeHeightAnchor`].
179+
pub trait ElectrumResultExt {
180+
/// New result type with a [`TxGraph`] that contains the [`ConfirmationTimeHeightAnchor`].
181+
type NewResult;
182+
183+
/// Convert result type to have an update [`TxGraph`] that contains the [`ConfirmationTimeHeightAnchor`] .
184+
fn try_into_confirmation_time_result(
185+
self,
186+
client: &impl ElectrumApi,
187+
) -> Result<Self::NewResult, Error>;
188+
}
189+
190+
impl<K> ElectrumResultExt for FullScanResult<K, ConfirmationHeightAnchor> {
191+
type NewResult = FullScanResult<K, ConfirmationTimeHeightAnchor>;
192+
193+
fn try_into_confirmation_time_result(
194+
self,
195+
client: &impl ElectrumApi,
196+
) -> Result<Self::NewResult, Error> {
197+
Ok(FullScanResult::<K, ConfirmationTimeHeightAnchor> {
198+
graph_update: try_into_confirmation_time_result(self.graph_update, client)?,
199+
chain_update: self.chain_update,
200+
last_active_indices: self.last_active_indices,
201+
})
202+
}
203+
}
204+
205+
impl ElectrumResultExt for SyncResult<ConfirmationHeightAnchor> {
206+
type NewResult = SyncResult<ConfirmationTimeHeightAnchor>;
260207

261-
Ok(electrum_update)
208+
fn try_into_confirmation_time_result(
209+
self,
210+
client: &impl ElectrumApi,
211+
) -> Result<Self::NewResult, Error> {
212+
Ok(SyncResult {
213+
graph_update: try_into_confirmation_time_result(self.graph_update, client)?,
214+
chain_update: self.chain_update,
215+
})
262216
}
263217
}
264218

219+
fn try_into_confirmation_time_result(
220+
graph_update: TxGraph<ConfirmationHeightAnchor>,
221+
client: &impl ElectrumApi,
222+
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
223+
let relevant_heights = graph_update
224+
.all_anchors()
225+
.iter()
226+
.map(|(a, _)| a.confirmation_height)
227+
.collect::<HashSet<_>>();
228+
229+
let height_to_time = relevant_heights
230+
.clone()
231+
.into_iter()
232+
.zip(
233+
client
234+
.batch_block_header(relevant_heights)?
235+
.into_iter()
236+
.map(|bh| bh.time as u64),
237+
)
238+
.collect::<HashMap<u32, u64>>();
239+
240+
Ok(graph_update.map_anchors(|a| ConfirmationTimeHeightAnchor {
241+
anchor_block: a.anchor_block,
242+
confirmation_height: a.confirmation_height,
243+
confirmation_time: height_to_time[&a.confirmation_height],
244+
}))
245+
}
246+
265247
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
266248
fn construct_update_tip(
267249
client: &impl ElectrumApi,
@@ -380,6 +362,7 @@ fn determine_tx_anchor(
380362
fn populate_with_outpoints(
381363
client: &impl ElectrumApi,
382364
cps: &BTreeMap<u32, CheckPoint>,
365+
tx_cache: &mut TxCache,
383366
tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
384367
outpoints: impl IntoIterator<Item = OutPoint>,
385368
) -> Result<(), Error> {
@@ -415,9 +398,9 @@ fn populate_with_outpoints(
415398
let res_tx = match tx_graph.get_tx(res.tx_hash) {
416399
Some(tx) => tx,
417400
None => {
418-
let res_tx = client.transaction_get(&res.tx_hash)?;
419-
let _ = tx_graph.insert_tx(res_tx);
420-
tx_graph.get_tx(res.tx_hash).expect("just inserted")
401+
let res_tx = fetch_tx(client, tx_cache, res.tx_hash)?;
402+
let _ = tx_graph.insert_tx(Arc::clone(&res_tx));
403+
res_tx
421404
}
422405
};
423406
has_spending = res_tx

0 commit comments

Comments
 (0)