Skip to content

Commit 5ebedaf

Browse files
committed
[examples_redesign] Implement tracker_electrum_example
This is `keychain_tracker_electrum_example` using the redesigned structures.
1 parent e87d78b commit 5ebedaf

File tree

6 files changed

+362
-18
lines changed

6 files changed

+362
-18
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ members = [
77
"example-crates/keychain_tracker_electrum",
88
"example-crates/keychain_tracker_esplora",
99
"example-crates/keychain_tracker_example_cli",
10+
"example-crates/tracker_electrum",
1011
"example-crates/tracker_example_cli",
1112
"example-crates/wallet_electrum",
1213
"example-crates/wallet_esplora",

crates/electrum/src/v2.rs

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
use bdk_chain::{
22
bitcoin::{hashes::hex::FromHex, BlockHash, OutPoint, Script, Transaction, Txid},
3-
indexed_tx_graph::{IndexedAdditions, IndexedTxGraph, Indexer},
3+
indexed_tx_graph::{IndexedAdditions, IndexedTxGraph},
4+
keychain::{DerivationAdditions, KeychainTxOutIndex},
45
local_chain::{self, LocalChain, UpdateNotConnectedError},
56
tx_graph::TxGraph,
67
Anchor, Append, BlockId, ConfirmationHeightAnchor,
78
};
89
use electrum_client::{Client, ElectrumApi, Error};
9-
use std::collections::{BTreeMap, BTreeSet, HashMap};
10+
use std::{
11+
collections::{BTreeMap, BTreeSet, HashMap},
12+
fmt::Debug,
13+
};
1014

1115
use crate::InternalError;
1216

17+
#[derive(Debug)]
1318
pub struct ElectrumUpdate<G, K> {
1419
pub graph_update: G,
1520
pub chain_update: LocalChain,
@@ -29,10 +34,10 @@ impl<G: Default, K> Default for ElectrumUpdate<G, K> {
2934
pub type IntermediaryElectrumUpdate<A, K> = ElectrumUpdate<HashMap<Txid, BTreeSet<A>>, K>;
3035

3136
impl<'a, A: Anchor, K> IntermediaryElectrumUpdate<A, K> {
32-
pub fn missing_full_txs<G>(&'a self, graph: G) -> impl Iterator<Item = &'a Txid> + 'a
33-
where
34-
G: AsRef<TxGraph> + 'a,
35-
{
37+
pub fn missing_full_txs(
38+
&'a self,
39+
graph: &'a TxGraph<A>,
40+
) -> impl Iterator<Item = &'a Txid> + 'a {
3641
self.graph_update
3742
.keys()
3843
.filter(move |&&txid| graph.as_ref().get_tx(txid).is_none())
@@ -61,17 +66,30 @@ impl<'a, A: Anchor, K> IntermediaryElectrumUpdate<A, K> {
6166

6267
pub type FinalElectrumUpdate<A, K> = ElectrumUpdate<TxGraph<A>, K>;
6368

64-
impl<A: Anchor, K> FinalElectrumUpdate<A, K> {
65-
pub fn apply<I: Indexer>(
69+
impl<A: Anchor, K: Ord + Clone + Debug> FinalElectrumUpdate<A, K> {
70+
pub fn apply(
6671
self,
67-
indexed_graph: &mut IndexedTxGraph<A, I>,
72+
indexed_graph: &mut IndexedTxGraph<A, KeychainTxOutIndex<K>>,
6873
chain: &mut LocalChain,
69-
) -> Result<(IndexedAdditions<A, I::Additions>, local_chain::ChangeSet), UpdateNotConnectedError>
70-
where
71-
I::Additions: Default + Append,
72-
{
73-
let additions = indexed_graph.apply_update(self.graph_update);
74+
) -> Result<
75+
(
76+
IndexedAdditions<A, DerivationAdditions<K>>,
77+
local_chain::ChangeSet,
78+
),
79+
UpdateNotConnectedError,
80+
> {
81+
let (_, derivation_additions) = indexed_graph
82+
.index
83+
.reveal_to_target_multi(&self.keychain_update);
84+
85+
let additions = {
86+
let mut additions = indexed_graph.apply_update(self.graph_update);
87+
additions.index_additions.append(derivation_additions);
88+
additions
89+
};
90+
7491
let changeset = chain.apply_update(self.chain_update)?;
92+
7593
Ok((additions, changeset))
7694
}
7795
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "tracker_electrum_example"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
bdk_chain = { path = "../../crates/chain", features = ["serde"] }
10+
bdk_electrum = { path = "../../crates/electrum" }
11+
tracker_example_cli = { path = "../tracker_example_cli" }
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
use std::{
2+
collections::BTreeMap,
3+
io::{self, Write},
4+
time::UNIX_EPOCH,
5+
};
6+
7+
use bdk_chain::{
8+
bitcoin::{Address, BlockHash, Network, OutPoint, Txid},
9+
Append, ConfirmationHeightAnchor,
10+
};
11+
use bdk_electrum::{
12+
electrum_client::{self, ElectrumApi},
13+
v2::{ElectrumExt, ElectrumUpdate},
14+
};
15+
use tracker_example_cli::{
16+
self as cli,
17+
anyhow::{self, Context},
18+
clap::{self, Parser, Subcommand},
19+
};
20+
21+
const DB_MAGIC: &[u8] = b"bdk_example_electrum";
22+
23+
#[derive(Subcommand, Debug, Clone)]
24+
enum ElectrumCommands {
25+
/// Scans the addresses in the wallet using the esplora API.
26+
Scan {
27+
/// When a gap this large has been found for a keychain, it will stop.
28+
#[clap(long, default_value = "5")]
29+
stop_gap: usize,
30+
#[clap(flatten)]
31+
scan_options: ScanOptions,
32+
},
33+
/// Scans particular addresses using the esplora API.
34+
Sync {
35+
/// Scan all the unused addresses.
36+
#[clap(long)]
37+
unused_spks: bool,
38+
/// Scan every address that you have derived.
39+
#[clap(long)]
40+
all_spks: bool,
41+
/// Scan unspent outpoints for spends or changes to confirmation status of residing tx.
42+
#[clap(long)]
43+
utxos: bool,
44+
/// Scan unconfirmed transactions for updates.
45+
#[clap(long)]
46+
unconfirmed: bool,
47+
#[clap(flatten)]
48+
scan_options: ScanOptions,
49+
},
50+
}
51+
52+
#[derive(Parser, Debug, Clone, PartialEq)]
53+
pub struct ScanOptions {
54+
/// Set batch size for each script_history call to electrum client.
55+
#[clap(long, default_value = "25")]
56+
pub batch_size: usize,
57+
}
58+
59+
fn main() -> anyhow::Result<()> {
60+
let (args, keymap, tracker, db) = cli::init::<ElectrumCommands, ConfirmationHeightAnchor, _>(
61+
DB_MAGIC,
62+
cli::Tracker::new_local(),
63+
)?;
64+
65+
let electrum_url = match args.network {
66+
Network::Bitcoin => "ssl://electrum.blockstream.info:50002",
67+
Network::Testnet => "ssl://electrum.blockstream.info:60002",
68+
Network::Regtest => "tcp://localhost:60401",
69+
Network::Signet => "tcp://signet-electrumx.wakiyamap.dev:50001",
70+
};
71+
let config = electrum_client::Config::builder()
72+
.validate_domain(matches!(args.network, Network::Bitcoin))
73+
.build();
74+
75+
let client = electrum_client::Client::from_config(electrum_url, config)?;
76+
77+
// [TODO]: Use genesis block based on network!
78+
let chain_tip = tracker.lock().unwrap().chain.tip().unwrap_or_default();
79+
80+
let electrum_cmd = match args.command.clone() {
81+
cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
82+
general_command => {
83+
return cli::handle_commands(
84+
general_command,
85+
|transaction| {
86+
let _txid = client.transaction_broadcast(transaction)?;
87+
Ok(())
88+
},
89+
&tracker,
90+
&db,
91+
chain_tip,
92+
args.network,
93+
&keymap,
94+
)
95+
}
96+
};
97+
98+
let response = match electrum_cmd {
99+
ElectrumCommands::Scan {
100+
stop_gap,
101+
scan_options,
102+
} => {
103+
let (spk_iters, local_chain) = {
104+
let tracker = &*tracker.lock().unwrap();
105+
let spk_iters = tracker
106+
.indexed_graph
107+
.index
108+
.spks_of_all_keychains()
109+
.into_iter()
110+
.map(|(keychain, iter)| {
111+
let mut first = true;
112+
let spk_iter = iter.inspect(move |(i, _)| {
113+
if first {
114+
eprint!("\nscanning {}: ", keychain);
115+
first = false;
116+
}
117+
118+
eprint!("{} ", i);
119+
let _ = io::stdout().flush();
120+
});
121+
(keychain, spk_iter)
122+
})
123+
.collect::<BTreeMap<_, _>>();
124+
let local_chain: BTreeMap<u32, BlockHash> = tracker.chain.clone().into();
125+
(spk_iters, local_chain)
126+
};
127+
128+
client.scan(
129+
&local_chain,
130+
spk_iters,
131+
core::iter::empty(),
132+
core::iter::empty(),
133+
stop_gap,
134+
scan_options.batch_size,
135+
)?
136+
}
137+
ElectrumCommands::Sync {
138+
mut unused_spks,
139+
all_spks,
140+
mut utxos,
141+
mut unconfirmed,
142+
scan_options,
143+
} => {
144+
// Get a short lock on the tracker to get the spks we're interested in
145+
let tracker = tracker.lock().unwrap();
146+
147+
if !(all_spks || unused_spks || utxos || unconfirmed) {
148+
unused_spks = true;
149+
unconfirmed = true;
150+
utxos = true;
151+
} else if all_spks {
152+
unused_spks = false;
153+
}
154+
155+
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::Script>> =
156+
Box::new(core::iter::empty());
157+
if all_spks {
158+
let index = &tracker.indexed_graph.index;
159+
let all_spks = index
160+
.all_spks()
161+
.iter()
162+
.map(|(k, v)| (*k, v.clone()))
163+
.collect::<Vec<_>>();
164+
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
165+
eprintln!("scanning {:?}", index);
166+
script
167+
})));
168+
}
169+
if unused_spks {
170+
let index = &tracker.indexed_graph.index;
171+
let unused_spks = index
172+
.unused_spks(..)
173+
.map(|(k, v)| (*k, v.clone()))
174+
.collect::<Vec<_>>();
175+
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
176+
eprintln!(
177+
"Checking if address {} {:?} has been used",
178+
Address::from_script(&script, args.network).unwrap(),
179+
index
180+
);
181+
182+
script
183+
})));
184+
}
185+
186+
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
187+
188+
if utxos {
189+
let utxos = tracker
190+
.list_owned_unspents(chain_tip)
191+
.map(|(_, utxo)| utxo)
192+
.collect::<Vec<_>>();
193+
outpoints = Box::new(
194+
utxos
195+
.into_iter()
196+
.inspect(|utxo| {
197+
eprintln!(
198+
"Checking if outpoint {} (value: {}) has been spent",
199+
utxo.outpoint, utxo.txout.value
200+
);
201+
})
202+
.map(|utxo| utxo.outpoint),
203+
);
204+
};
205+
206+
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
207+
208+
if unconfirmed {
209+
let unconfirmed_txids = tracker
210+
.list_txs(chain_tip)
211+
.filter(|ctx| !ctx.observed_as.is_confirmed())
212+
.map(|ctx| ctx.node.txid)
213+
.collect::<Vec<_>>();
214+
215+
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
216+
eprintln!("Checking if {} is confirmed yet", txid);
217+
}));
218+
}
219+
220+
let local_chain: BTreeMap<u32, BlockHash> = tracker.chain.clone().into();
221+
drop(tracker);
222+
223+
let update = client.scan_without_keychain(
224+
&local_chain,
225+
spks,
226+
txids,
227+
outpoints,
228+
scan_options.batch_size,
229+
)?;
230+
ElectrumUpdate {
231+
graph_update: update.graph_update,
232+
chain_update: update.chain_update,
233+
keychain_update: BTreeMap::new(),
234+
}
235+
}
236+
};
237+
println!();
238+
239+
let missing_txids = {
240+
let tracker = &*tracker.lock().unwrap();
241+
response
242+
.missing_full_txs(tracker.indexed_graph.graph())
243+
.cloned()
244+
.collect::<Vec<_>>()
245+
};
246+
247+
let update = response.finalize(
248+
UNIX_EPOCH.elapsed().map(|d| d.as_secs()).ok(),
249+
client
250+
.batch_transaction_get(&missing_txids)
251+
.context("fetching full transactions")?,
252+
);
253+
254+
{
255+
use bdk_chain::PersistBackend;
256+
let tracker = &mut *tracker.lock().unwrap();
257+
let db = &mut *db.lock().unwrap();
258+
259+
let (additions, changeset) =
260+
update.apply(&mut tracker.indexed_graph, &mut tracker.chain)?;
261+
262+
let mut tracker_changeset = cli::ChangeSet::default();
263+
tracker_changeset.append(additions.into());
264+
tracker_changeset.append(changeset.into());
265+
266+
// [TODO] How do we check if changeset is empty?
267+
// [TODO] When should we flush?
268+
db.write_changes(&tracker_changeset)?;
269+
}
270+
271+
Ok(())
272+
}

0 commit comments

Comments
 (0)