Skip to content

Commit da8cfd3

Browse files
evanlinjinremix7531LLFourndanielabrozzoni
committed
feat: add cli example for esplora
Co-authored-by: remix <[email protected]> Co-authored-by: LLFourn <[email protected]> Co-authored-by: Daniela Brozzoni <[email protected]>
1 parent 93e8eaf commit da8cfd3

File tree

4 files changed

+376
-0
lines changed

4 files changed

+376
-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_blocking",
1213
"example-crates/wallet_esplora_async",

crates/chain/src/tx_graph.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,45 @@ impl<A> ChangeSet<A> {
10661066
})
10671067
.chain(self.txouts.iter().map(|(op, txout)| (*op, txout)))
10681068
}
1069+
1070+
/// Iterates over the heights of that the new transaction anchors in this changeset.
1071+
///
1072+
/// This is useful if you want to find which heights you need to fetch data about in order to
1073+
/// confirm or exclude these anchors.
1074+
///
1075+
/// See also: [`TxGraph::missing_heights`]
1076+
pub fn anchor_heights(&self) -> impl Iterator<Item = u32> + '_
1077+
where
1078+
A: Anchor,
1079+
{
1080+
let mut dedup = None;
1081+
self.anchors
1082+
.iter()
1083+
.map(|(a, _)| a.anchor_block().height)
1084+
.filter(move |height| {
1085+
let duplicate = dedup == Some(*height);
1086+
dedup = Some(*height);
1087+
!duplicate
1088+
})
1089+
}
1090+
1091+
/// Returns an iterator for the [`anchor_heights`] in this changeset that are not included in
1092+
/// `local_chain`. This tells you which heights you need to include in `local_chain` in order
1093+
/// for it to conclusively act as a [`ChainOracle`] for the transaction anchors this changeset
1094+
/// will add.
1095+
///
1096+
/// [`ChainOracle`]: crate::ChainOracle
1097+
/// [`anchor_heights`]: Self::anchor_heights
1098+
pub fn missing_heights_from<'a>(
1099+
&'a self,
1100+
local_chain: &'a LocalChain,
1101+
) -> impl Iterator<Item = u32> + 'a
1102+
where
1103+
A: Anchor,
1104+
{
1105+
self.anchor_heights()
1106+
.filter(move |height| !local_chain.blocks().contains_key(height))
1107+
}
10691108
}
10701109

10711110
impl<A: Ord> Append for ChangeSet<A> {
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: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
use std::{
2+
collections::{BTreeMap, BTreeSet},
3+
io::{self, Write},
4+
sync::Mutex,
5+
};
6+
7+
use bdk_chain::{
8+
bitcoin::{Address, Network, OutPoint, ScriptBuf, Txid},
9+
indexed_tx_graph::IndexedTxGraph,
10+
keychain::WalletChangeSet,
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+
WalletChangeSet<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_changeset(init_changeset.index_tx_graph);
74+
graph
75+
});
76+
let chain = Mutex::new({
77+
let mut chain = LocalChain::default();
78+
chain.apply_changeset(&init_changeset.chain);
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!("unsupported 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+
// Prepare the `IndexedTxGraph` update based on whether we are scanning or syncing.
118+
// Scanning: We are iterating through spks of all keychains and scanning for transactions for
119+
// each spk. We start with the lowest derivation index spk and stop scanning after `stop_gap`
120+
// number of consecutive spks have no transaction history. A Scan is done in situations of
121+
// wallet restoration. It is a special case. Applications should use "sync" style updates
122+
// after an initial scan.
123+
// Syncing: We only check for specified spks, utxos and txids to update their confirmation
124+
// status or fetch missing transactions.
125+
let indexed_tx_graph_changeset = match &esplora_cmd {
126+
EsploraCommands::Scan {
127+
stop_gap,
128+
scan_options,
129+
} => {
130+
let keychain_spks = graph
131+
.lock()
132+
.expect("mutex must not be poisoned")
133+
.index
134+
.spks_of_all_keychains()
135+
.into_iter()
136+
// This `map` is purely for logging.
137+
.map(|(keychain, iter)| {
138+
let mut first = true;
139+
let spk_iter = iter.inspect(move |(i, _)| {
140+
if first {
141+
eprint!("\nscanning {}: ", keychain);
142+
first = false;
143+
}
144+
eprint!("{} ", i);
145+
// Flush early to ensure we print at every iteration.
146+
let _ = io::stderr().flush();
147+
});
148+
(keychain, spk_iter)
149+
})
150+
.collect::<BTreeMap<_, _>>();
151+
152+
// The client scans keychain spks for transaction histories, stopping after `stop_gap`
153+
// is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
154+
// represents the last active spk derivation indices of keychains
155+
// (`keychain_indices_update`).
156+
let (graph_update, last_active_indices) = client
157+
.update_tx_graph(
158+
keychain_spks,
159+
core::iter::empty(),
160+
core::iter::empty(),
161+
*stop_gap,
162+
scan_options.parallel_requests,
163+
)
164+
.context("scanning for transactions")?;
165+
166+
let mut graph = graph.lock().expect("mutex must not be poisoned");
167+
// Because we did a stop gap based scan we are likely to have some updates to our
168+
// deriviation indices. Usually before a scan you are on a fresh wallet with no
169+
// addresses derived so we need to derive up to last active addresses the scan found
170+
// before adding the transactions.
171+
let (_, index_changeset) = graph.index.reveal_to_target_multi(&last_active_indices);
172+
let mut indexed_tx_graph_changeset = graph.apply_update(graph_update);
173+
indexed_tx_graph_changeset.append(index_changeset.into());
174+
indexed_tx_graph_changeset
175+
}
176+
EsploraCommands::Sync {
177+
mut unused_spks,
178+
all_spks,
179+
mut utxos,
180+
mut unconfirmed,
181+
scan_options,
182+
} => {
183+
if !(*all_spks || unused_spks || utxos || unconfirmed) {
184+
// If nothing is specifically selected, we select everything (except all spks).
185+
unused_spks = true;
186+
unconfirmed = true;
187+
utxos = true;
188+
} else if *all_spks {
189+
// If all spks is selected, we don't need to also select unused spks (as unused spks
190+
// is a subset of all spks).
191+
unused_spks = false;
192+
}
193+
194+
// Spks, outpoints and txids we want updates on will be accumulated here.
195+
let mut spks: Box<dyn Iterator<Item = ScriptBuf>> = Box::new(core::iter::empty());
196+
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
197+
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
198+
199+
// Get a short lock on the structures to get spks, utxos, and txs that we are interested
200+
// in.
201+
{
202+
let graph = graph.lock().unwrap();
203+
let chain = chain.lock().unwrap();
204+
let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default();
205+
206+
if *all_spks {
207+
let all_spks = graph
208+
.index
209+
.all_spks()
210+
.iter()
211+
.map(|(k, v)| (*k, v.clone()))
212+
.collect::<Vec<_>>();
213+
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
214+
eprintln!("scanning {:?}", index);
215+
// Flush early to ensure we print at every iteration.
216+
let _ = io::stderr().flush();
217+
script
218+
})));
219+
}
220+
if unused_spks {
221+
let unused_spks = graph
222+
.index
223+
.unused_spks(..)
224+
.map(|(k, v)| (*k, v.to_owned()))
225+
.collect::<Vec<_>>();
226+
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
227+
eprintln!(
228+
"Checking if address {} {:?} has been used",
229+
Address::from_script(&script, args.network).unwrap(),
230+
index
231+
);
232+
// Flush early to ensure we print at every iteration.
233+
let _ = io::stderr().flush();
234+
script
235+
})));
236+
}
237+
if utxos {
238+
// We want to search for whether the UTXO is spent, and spent by which
239+
// transaction. We provide the outpoint of the UTXO to
240+
// `EsploraExt::update_tx_graph_without_keychain`.
241+
let init_outpoints = graph.index.outpoints().iter().cloned();
242+
let utxos = graph
243+
.graph()
244+
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
245+
.map(|(_, utxo)| utxo)
246+
.collect::<Vec<_>>();
247+
outpoints = Box::new(
248+
utxos
249+
.into_iter()
250+
.inspect(|utxo| {
251+
eprintln!(
252+
"Checking if outpoint {} (value: {}) has been spent",
253+
utxo.outpoint, utxo.txout.value
254+
);
255+
// Flush early to ensure we print at every iteration.
256+
let _ = io::stderr().flush();
257+
})
258+
.map(|utxo| utxo.outpoint),
259+
);
260+
};
261+
if unconfirmed {
262+
// We want to search for whether the unconfirmed transaction is now confirmed.
263+
// We provide the unconfirmed txids to
264+
// `EsploraExt::update_tx_graph_without_keychain`.
265+
let unconfirmed_txids = graph
266+
.graph()
267+
.list_chain_txs(&*chain, chain_tip)
268+
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
269+
.map(|canonical_tx| canonical_tx.tx_node.txid)
270+
.collect::<Vec<Txid>>();
271+
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
272+
eprintln!("Checking if {} is confirmed yet", txid);
273+
// Flush early to ensure we print at every iteration.
274+
let _ = io::stderr().flush();
275+
}));
276+
}
277+
}
278+
279+
let graph_update = client.update_tx_graph_without_keychain(
280+
spks,
281+
txids,
282+
outpoints,
283+
scan_options.parallel_requests,
284+
)?;
285+
286+
graph.lock().unwrap().apply_update(graph_update)
287+
}
288+
};
289+
290+
println!();
291+
292+
// Now that we're done updating the `IndexedTxGraph`, it's time to update the `LocalChain`! We
293+
// want the `LocalChain` to have data about all the anchors in the `TxGraph` - for this reason,
294+
// we want retrieve the blocks at the heights of the newly added anchors that are missing from
295+
// our view of the chain.
296+
let (missing_block_heights, tip) = {
297+
let chain = &*chain.lock().unwrap();
298+
let missing_block_heights = indexed_tx_graph_changeset
299+
.graph
300+
.missing_heights_from(chain)
301+
.collect::<BTreeSet<_>>();
302+
let tip = chain.tip();
303+
(missing_block_heights, tip)
304+
};
305+
306+
println!("prev tip: {}", tip.as_ref().map_or(0, CheckPoint::height));
307+
println!("missing block heights: {:?}", missing_block_heights);
308+
309+
// Here, we actually fetch the missing blocks and create a `local_chain::Update`.
310+
let chain_update = client
311+
.update_local_chain(tip, missing_block_heights)
312+
.context("scanning for blocks")?;
313+
314+
println!("new tip: {}", chain_update.tip.height());
315+
316+
// We persist the changes
317+
let mut db = db.lock().unwrap();
318+
db.stage(WalletChangeSet {
319+
chain: chain.lock().unwrap().apply_update(chain_update)?,
320+
indexed_tx_graph: indexed_tx_graph_changeset,
321+
});
322+
db.commit()?;
323+
Ok(())
324+
}

0 commit comments

Comments
 (0)