Skip to content

Commit 3dd481f

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

File tree

3 files changed

+339
-0
lines changed

3 files changed

+339
-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: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
use std::{
2+
collections::BTreeMap,
3+
io::{self, Write},
4+
sync::Mutex,
5+
};
6+
7+
use bdk_chain::{
8+
bitcoin::{Address, Network, OutPoint, ScriptBuf, 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 using 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+
/// Scan for particular addresses and unconfirmed transactions 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+
// Contruct `IndexedTxGraph` and `LocalChain` with our initial changeset. They are wrapped in
69+
// `Mutex` to display how they can be used in a multithreaded context. Technically the mutexes
70+
// aren't strictly needed here.
71+
let graph = Mutex::new({
72+
let mut graph = IndexedTxGraph::new(index);
73+
graph.apply_additions(init_changeset.indexed_additions);
74+
graph
75+
});
76+
let chain = Mutex::new({
77+
let mut chain = LocalChain::default();
78+
chain.apply_changeset(&init_changeset.chain_changeset);
79+
chain
80+
});
81+
82+
let esplora_url = match args.network {
83+
Network::Bitcoin => "https://blockstream.info/api",
84+
Network::Testnet => "https://blockstream.info/testnet/api",
85+
Network::Regtest => "http://localhost:3002",
86+
Network::Signet => "https://mempool.space/signet/api",
87+
_ => panic!("unsuported network"),
88+
};
89+
90+
let client = esplora_client::Builder::new(esplora_url).build_blocking()?;
91+
92+
let esplora_cmd = match &args.command {
93+
// These are commands that are handled by this example (sync, scan).
94+
example_cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd,
95+
// These are general commands handled by example_cli. Execute the cmd and return.
96+
general_cmd => {
97+
let res = example_cli::handle_commands(
98+
&graph,
99+
&db,
100+
&chain,
101+
&keymap,
102+
args.network,
103+
|tx| {
104+
client
105+
.broadcast(tx)
106+
.map(|_| ())
107+
.map_err(anyhow::Error::from)
108+
},
109+
general_cmd.clone(),
110+
);
111+
112+
db.lock().unwrap().commit()?;
113+
return res;
114+
}
115+
};
116+
117+
// This is where we will accumulate changes of `IndexedTxGraph` and `LocalChain` and persist
118+
// these changes as a batch at the end.
119+
let mut changeset = LocalChangeSet::<Keychain, ConfirmationTimeAnchor>::default();
120+
121+
// Prepare the `IndexedTxGraph` update based on whether we are scanning or syncing.
122+
// Scanning: We are iterating through spks of all keychains and scanning for transactions for
123+
// each spk. We start with the lowest derivation index spk and stop scanning after `stop_gap`
124+
// number of consecutive spks have no transaction history.
125+
// Syncing: We only check for specified spks, utxos and txids to update their confirmation
126+
// status or fetch missing transactions.
127+
let graph_update = match &esplora_cmd {
128+
EsploraCommands::Scan {
129+
stop_gap,
130+
scan_options,
131+
} => {
132+
let keychain_spks = graph
133+
.lock()
134+
.expect("mutex must not be poisoned")
135+
.index
136+
.spks_of_all_keychains()
137+
.into_iter()
138+
// This `map` is purely for printing to stdout.
139+
.map(|(keychain, iter)| {
140+
let mut first = true;
141+
let spk_iter = iter.inspect(move |(i, _)| {
142+
if first {
143+
eprint!("\nscanning {}: ", keychain);
144+
first = false;
145+
}
146+
eprint!("{} ", i);
147+
// Flush early to ensure we print at every iteration.
148+
let _ = io::stdout().flush();
149+
});
150+
(keychain, spk_iter)
151+
})
152+
.collect::<BTreeMap<_, _>>();
153+
154+
// The client scans keychain spks for transaction histories, stopping after `stop_gap`
155+
// is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
156+
// represents the last active spk derivation indices of keychains
157+
// (`keychain_indices_update`).
158+
let (graph_update, keychain_indices_update) = client
159+
.update_tx_graph(
160+
keychain_spks,
161+
core::iter::empty(),
162+
core::iter::empty(),
163+
*stop_gap,
164+
scan_options.parallel_requests,
165+
)
166+
.context("scanning for transactions")?;
167+
168+
// Update the index in `IndexedTxGraph` with `keychain_indices_update`. The resultant
169+
// changes are appended to `changeset`.
170+
changeset.append({
171+
let (_, index_additions) = graph
172+
.lock()
173+
.expect("mutex must not be poisoned")
174+
.index
175+
.reveal_to_target_multi(&keychain_indices_update);
176+
LocalChangeSet::from(IndexedAdditions::from(index_additions))
177+
});
178+
179+
graph_update
180+
}
181+
EsploraCommands::Sync {
182+
mut unused_spks,
183+
all_spks,
184+
mut utxos,
185+
mut unconfirmed,
186+
scan_options,
187+
} => {
188+
if !(*all_spks || unused_spks || utxos || unconfirmed) {
189+
// If nothing is specifically selected, we select everything (except all spks).
190+
unused_spks = true;
191+
unconfirmed = true;
192+
utxos = true;
193+
} else if *all_spks {
194+
// If all spks is selected, we don't need to also select unused spks (as unused spks
195+
// is a subset of all spks).
196+
unused_spks = false;
197+
}
198+
199+
// Spks, outpoints and txids we want updates on will be accumulated here.
200+
let mut spks: Box<dyn Iterator<Item = ScriptBuf>> = Box::new(core::iter::empty());
201+
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
202+
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
203+
204+
// Get a short lock on the structures to get spks, utxos, and txs that we are interested
205+
// in.
206+
{
207+
let graph = graph.lock().unwrap();
208+
let chain = chain.lock().unwrap();
209+
let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
210+
211+
if *all_spks {
212+
let all_spks = graph
213+
.index
214+
.all_spks()
215+
.iter()
216+
.map(|(k, v)| (*k, v.clone()))
217+
.collect::<Vec<_>>();
218+
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
219+
eprintln!("scanning {:?}", index);
220+
script
221+
})));
222+
}
223+
if unused_spks {
224+
let unused_spks = graph
225+
.index
226+
.unused_spks(..)
227+
.map(|(k, v)| (*k, v.to_owned()))
228+
.collect::<Vec<_>>();
229+
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
230+
eprintln!(
231+
"Checking if address {} {:?} has been used",
232+
Address::from_script(&script, args.network).unwrap(),
233+
index
234+
);
235+
script
236+
})));
237+
}
238+
if utxos {
239+
// We want to search for whether the UTXO is spent, and spent by which
240+
// transaction. We provide the outpoint of the UTXO to
241+
// `EsploraExt::update_tx_graph_without_keychain`.
242+
let init_outpoints = graph.index.outpoints().iter().cloned();
243+
let utxos = graph
244+
.graph()
245+
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
246+
.map(|(_, utxo)| utxo)
247+
.collect::<Vec<_>>();
248+
outpoints = Box::new(
249+
utxos
250+
.into_iter()
251+
.inspect(|utxo| {
252+
eprintln!(
253+
"Checking if outpoint {} (value: {}) has been spent",
254+
utxo.outpoint, utxo.txout.value
255+
);
256+
})
257+
.map(|utxo| utxo.outpoint),
258+
);
259+
};
260+
if unconfirmed {
261+
// We want to search for whether the unconfirmed transaction is now confirmed.
262+
// We provide the unconfirmed txids to
263+
// `EsploraExt::update_tx_graph_without_keychain`.
264+
let unconfirmed_txids = graph
265+
.graph()
266+
.list_chain_txs(&*chain, chain_tip)
267+
.filter(|canonical_tx| !canonical_tx.observed_as.is_confirmed())
268+
.map(|canonical_tx| canonical_tx.node.txid)
269+
.collect::<Vec<Txid>>();
270+
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
271+
eprintln!("Checking if {} is confirmed yet", txid);
272+
}));
273+
}
274+
}
275+
276+
client.update_tx_graph_without_keychain(
277+
spks,
278+
txids,
279+
outpoints,
280+
scan_options.parallel_requests,
281+
)?
282+
}
283+
};
284+
285+
println!();
286+
287+
// Up to this point, we have only created a `TxGraph` update, but not an update for our
288+
// `ChainOracle` implementation (`LocalChain`). The `TxGraph` update may contain chain anchors,
289+
// so we need the corresponding blocks to exist in our `LocalChain`. Here, we find the heights
290+
// of missing blocks in `LocalChain`.
291+
//
292+
// Getting the local chain tip is only for printing to stdout.
293+
let (missing_block_heights, tip) = {
294+
let chain = &*chain.lock().unwrap();
295+
let heights_to_fetch = graph_update.missing_heights(chain).collect::<Vec<_>>();
296+
let tip = chain.tip();
297+
(heights_to_fetch, tip)
298+
};
299+
300+
println!("prev tip: {}", tip.as_ref().map_or(0, CheckPoint::height));
301+
println!("missing block heights: {:?}", missing_block_heights);
302+
303+
// Here, we actually fetch the missing blocks and create a `local_chain::Update`.
304+
let chain_update = client
305+
.update_local_chain(tip, missing_block_heights)
306+
.context("scanning for blocks")?;
307+
308+
println!("new tip: {}", chain_update.tip.height());
309+
310+
// We apply the `LocalChain` and `TxGraph` updates, and append the resultant changes to
311+
// `changeset` (for persistance).
312+
changeset.append({
313+
let chain_additions = chain.lock().unwrap().apply_update(chain_update)?;
314+
LocalChangeSet::from(chain_additions)
315+
});
316+
changeset.append({
317+
let indexed_graph_additions = graph.lock().unwrap().apply_update(graph_update);
318+
LocalChangeSet::from(indexed_graph_additions)
319+
});
320+
321+
// We persist `changeset`.
322+
let mut db = db.lock().unwrap();
323+
db.stage(changeset);
324+
db.commit()?;
325+
Ok(())
326+
}

0 commit comments

Comments
 (0)