Skip to content

Commit 6a5b769

Browse files
evanlinjinremix7531
andcommitted
feat: add cli example for esplora
Co-authored-by: remix <[email protected]>
1 parent feafaac commit 6a5b769

File tree

3 files changed

+354
-0
lines changed

3 files changed

+354
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ members = [
77
"crates/esplora",
88
"example-crates/example_cli",
99
"example-crates/example_electrum",
10+
"example-crates/example_esplora",
1011
"example-crates/wallet_electrum",
1112
"example-crates/wallet_esplora",
1213
"example-crates/wallet_esplora_async",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "example_esplora"
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_esplora = { path = "../../crates/esplora", features = ["blocking"] }
11+
example_cli = { path = "../example_cli" }
12+
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
use std::{
2+
collections::BTreeMap,
3+
io::{self, Write},
4+
sync::Mutex,
5+
};
6+
7+
use bdk_chain::{
8+
bitcoin::{Address, Network, OutPoint, Txid},
9+
indexed_tx_graph::{IndexedAdditions, IndexedTxGraph},
10+
keychain::LocalChangeSet,
11+
local_chain::{CheckPoint, LocalChain},
12+
Append, ConfirmationTimeAnchor,
13+
};
14+
15+
use bdk_esplora::{esplora_client, EsploraExt};
16+
17+
use example_cli::{
18+
anyhow::{self, Context},
19+
clap::{self, Parser, Subcommand},
20+
Keychain,
21+
};
22+
23+
const DB_MAGIC: &[u8] = b"bdk_example_esplora";
24+
const DB_PATH: &str = ".bdk_esplora_example.db";
25+
26+
#[derive(Subcommand, Debug, Clone)]
27+
enum EsploraCommands {
28+
/// Scans the addresses in the wallet sing the esplora API.
29+
Scan {
30+
/// When a gap this large has been found for a keychain, it will stop.
31+
#[clap(long, default_value = "5")]
32+
stop_gap: usize,
33+
#[clap(flatten)]
34+
scan_options: ScanOptions,
35+
},
36+
/// Scans particular addresses using the esplora API.
37+
Sync {
38+
/// Scan all the unused addresses.
39+
#[clap(long)]
40+
unused_spks: bool,
41+
/// Scan every address that you have derived.
42+
#[clap(long)]
43+
all_spks: bool,
44+
/// Scan unspent outpoints for spends or changes to confirmation status of residing tx.
45+
#[clap(long)]
46+
utxos: bool,
47+
/// Scan unconfirmed transactions for updates.
48+
#[clap(long)]
49+
unconfirmed: bool,
50+
#[clap(flatten)]
51+
scan_options: ScanOptions,
52+
},
53+
}
54+
55+
#[derive(Parser, Debug, Clone, PartialEq)]
56+
pub struct ScanOptions {
57+
/// Max number of concurrent esplora server requests.
58+
#[clap(long, default_value = "1")]
59+
pub parallel_requests: usize,
60+
}
61+
62+
fn main() -> anyhow::Result<()> {
63+
let (args, keymap, index, db, init_changeset) = example_cli::init::<
64+
EsploraCommands,
65+
LocalChangeSet<Keychain, ConfirmationTimeAnchor>,
66+
>(DB_MAGIC, DB_PATH)?;
67+
68+
let graph = Mutex::new({
69+
let mut graph = IndexedTxGraph::new(index);
70+
graph.apply_additions(init_changeset.indexed_additions);
71+
graph
72+
});
73+
74+
let chain = Mutex::new({
75+
let mut chain = LocalChain::default();
76+
chain.apply_changeset(&init_changeset.chain_changeset);
77+
chain
78+
});
79+
80+
let esplora_url = match args.network {
81+
Network::Bitcoin => "https://blockstream.info/api",
82+
Network::Testnet => "https://blockstream.info/testnet/api",
83+
Network::Regtest => "http://localhost:3002",
84+
Network::Signet => "https://mempool.space/signet/api",
85+
_ => panic!("unsuported network"),
86+
};
87+
88+
let client = esplora_client::Builder::new(esplora_url).build_blocking()?;
89+
90+
// Match the given command. Exectute and return if command is provided by example_cli
91+
let esplora_cmd = match &args.command {
92+
// Command that are handled by the specify example
93+
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
94+
// General commands handled by example_cli. Execute the cmd and return.
95+
general_cmd => {
96+
let res = example_cli::handle_commands(
97+
&graph,
98+
&db,
99+
&chain,
100+
&keymap,
101+
args.network,
102+
|tx| {
103+
client
104+
.broadcast(tx)
105+
.map(|_| ())
106+
.map_err(anyhow::Error::from)
107+
},
108+
general_cmd.clone(),
109+
);
110+
111+
db.lock().unwrap().commit()?;
112+
return res;
113+
}
114+
};
115+
116+
let (update_graph, update_keychain_indices) = match &esplora_cmd {
117+
EsploraCommands::Scan {
118+
stop_gap,
119+
scan_options,
120+
} => {
121+
let graph = graph.lock().unwrap();
122+
123+
let keychain_spks = graph
124+
.index
125+
.spks_of_all_keychains()
126+
.into_iter()
127+
.map(|(keychain, iter)| {
128+
let mut first = true;
129+
let spk_iter = iter.inspect(move |(i, _)| {
130+
if first {
131+
eprint!("\nscanning {}: ", keychain);
132+
first = false;
133+
}
134+
eprint!("{} ", i);
135+
let _ = io::stdout().flush();
136+
});
137+
(keychain, spk_iter)
138+
})
139+
.collect::<BTreeMap<_, _>>();
140+
141+
drop(graph);
142+
143+
client
144+
.update_tx_graph(
145+
keychain_spks,
146+
core::iter::empty(),
147+
core::iter::empty(),
148+
*stop_gap,
149+
scan_options.parallel_requests,
150+
)
151+
.context("scanning for transactions")?
152+
}
153+
EsploraCommands::Sync {
154+
mut unused_spks,
155+
all_spks,
156+
mut utxos,
157+
mut unconfirmed,
158+
scan_options,
159+
} => {
160+
// Get a short lock on the tracker to get the spks we're interested in
161+
let graph = graph.lock().unwrap();
162+
let chain = chain.lock().unwrap();
163+
let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
164+
165+
if !(*all_spks || unused_spks || utxos || unconfirmed) {
166+
unused_spks = true;
167+
unconfirmed = true;
168+
utxos = true;
169+
} else if *all_spks {
170+
unused_spks = false;
171+
}
172+
173+
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::ScriptBuf>> =
174+
Box::new(core::iter::empty());
175+
if *all_spks {
176+
let all_spks = graph
177+
.index
178+
.all_spks()
179+
.iter()
180+
.map(|(k, v)| (*k, v.clone()))
181+
.collect::<Vec<_>>();
182+
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
183+
eprintln!("scanning {:?}", index);
184+
script
185+
})));
186+
}
187+
if unused_spks {
188+
let unused_spks = graph
189+
.index
190+
.unused_spks(..)
191+
.map(|(k, v)| (*k, v.to_owned()))
192+
.collect::<Vec<_>>();
193+
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
194+
eprintln!(
195+
"Checking if address {} {:?} has been used",
196+
Address::from_script(&script, args.network).unwrap(),
197+
index
198+
);
199+
script
200+
})));
201+
}
202+
203+
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
204+
205+
if utxos {
206+
let init_outpoints = graph.index.outpoints().iter().cloned();
207+
208+
let utxos = graph
209+
.graph()
210+
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
211+
.map(|(_, utxo)| utxo)
212+
.collect::<Vec<_>>();
213+
214+
outpoints = Box::new(
215+
utxos
216+
.into_iter()
217+
.inspect(|utxo| {
218+
eprintln!(
219+
"Checking if outpoint {} (value: {}) has been spent",
220+
utxo.outpoint, utxo.txout.value
221+
);
222+
})
223+
.map(|utxo| utxo.outpoint),
224+
);
225+
};
226+
227+
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
228+
229+
if unconfirmed {
230+
let unconfirmed_txids = graph
231+
.graph()
232+
.list_chain_txs(&*chain, chain_tip)
233+
.filter(|canonical_tx| !canonical_tx.observed_as.is_confirmed())
234+
.map(|canonical_tx| canonical_tx.node.txid)
235+
.collect::<Vec<Txid>>();
236+
237+
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
238+
eprintln!("Checking if {} is confirmed yet", txid);
239+
}));
240+
}
241+
242+
// drop lock on graph and chain
243+
drop((graph, chain));
244+
245+
(
246+
client
247+
.update_tx_graph_without_keychain(
248+
spks,
249+
txids,
250+
outpoints,
251+
scan_options.parallel_requests,
252+
)
253+
.context("syncing transaction updates")?,
254+
Default::default(),
255+
)
256+
}
257+
};
258+
259+
println!();
260+
261+
let (heights_to_fetch, tip) = {
262+
let chain = &*chain.lock().unwrap();
263+
let heights_to_fetch = update_graph.missing_heights(chain).collect::<Vec<_>>();
264+
let tip = chain.tip();
265+
(heights_to_fetch, tip)
266+
};
267+
268+
#[cfg(debug_assertions)]
269+
println!(
270+
"old chain: {:?}",
271+
tip.iter()
272+
.flat_map(CheckPoint::iter)
273+
.map(|cp| cp.height())
274+
.collect::<Vec<_>>()
275+
);
276+
println!("prev tip: {}", tip.as_ref().map_or(0, CheckPoint::height));
277+
println!("missing blocks: {:?}", heights_to_fetch);
278+
279+
let update = client
280+
.update_local_chain(tip, heights_to_fetch)
281+
.context("scanning for blocks")?;
282+
283+
#[cfg(debug_assertions)]
284+
println!(
285+
"new chain: {:?}",
286+
update.tip.iter().map(|cp| cp.height()).collect::<Vec<_>>()
287+
);
288+
println!("new tip: {}", update.tip.height());
289+
290+
// check that all anchors are part of the new tip's history
291+
#[cfg(debug_assertions)]
292+
{
293+
use bdk_chain::bitcoin::BlockHash;
294+
use bdk_chain::collections::HashMap;
295+
let chain_heights = update
296+
.tip
297+
.iter()
298+
.map(|cp| (cp.height(), cp.hash()))
299+
.collect::<HashMap<u32, BlockHash>>();
300+
for (anchor, _) in update_graph.all_anchors() {
301+
assert_eq!(anchor.anchor_block.height, anchor.confirmation_height);
302+
assert!(chain_heights.contains_key(&anchor.anchor_block.height));
303+
304+
let remote_hash = chain_heights
305+
.get(&anchor.confirmation_height)
306+
.expect("must have block");
307+
308+
// inform about mismatched blocks
309+
if remote_hash != &anchor.anchor_block.hash {
310+
println!("mismatched block @ {}!", anchor.confirmation_height);
311+
println!("\t- anchor_block: {}", anchor.anchor_block.hash);
312+
println!("\t- from_chain: {}", remote_hash);
313+
}
314+
}
315+
}
316+
317+
let db_changeset: LocalChangeSet<Keychain, ConfirmationTimeAnchor> = {
318+
let mut chain = chain.lock().unwrap();
319+
let mut graph = graph.lock().unwrap();
320+
321+
let chain_changeset = chain.apply_update(update)?;
322+
323+
let indexed_additions = {
324+
let mut additions = IndexedAdditions::default();
325+
let (_, index_additions) = graph.index.reveal_to_target_multi(&update_keychain_indices);
326+
additions.append(IndexedAdditions::from(index_additions));
327+
additions.append(graph.apply_update(update_graph));
328+
additions
329+
};
330+
331+
LocalChangeSet {
332+
chain_changeset,
333+
indexed_additions,
334+
}
335+
};
336+
337+
let mut db = db.lock().unwrap();
338+
db.stage(db_changeset);
339+
db.commit()?;
340+
Ok(())
341+
}

0 commit comments

Comments
 (0)