[examples_redesign] Implemented example_electrum
This is a version of `keychain_tracker_electrum` that uses the redesigned structures instead.
This commit is contained in:
parent
f55974a64b
commit
6a1ac7f80a
@ -5,6 +5,7 @@ members = [
|
|||||||
"crates/file_store",
|
"crates/file_store",
|
||||||
"crates/electrum",
|
"crates/electrum",
|
||||||
"example-crates/example_cli",
|
"example-crates/example_cli",
|
||||||
|
"example-crates/example_electrum",
|
||||||
"example-crates/keychain_tracker_electrum",
|
"example-crates/keychain_tracker_electrum",
|
||||||
"example-crates/keychain_tracker_esplora",
|
"example-crates/keychain_tracker_esplora",
|
||||||
"example-crates/keychain_tracker_example_cli",
|
"example-crates/keychain_tracker_example_cli",
|
||||||
|
@ -54,7 +54,7 @@ impl<A: Anchor, X: Append> Append for ChangeSet<A, X> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_empty(&self) -> bool {
|
fn is_empty(&self) -> bool {
|
||||||
todo!()
|
self.indexed_additions.is_empty() && self.extension.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -666,7 +666,7 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
|
|||||||
pub fn handle_commands<C: clap::Subcommand, A: Anchor, O: ChainOracle, X>(
|
pub fn handle_commands<C: clap::Subcommand, A: Anchor, O: ChainOracle, X>(
|
||||||
graph: &Mutex<KeychainTxGraph<A>>,
|
graph: &Mutex<KeychainTxGraph<A>>,
|
||||||
db: &Mutex<Database<A, X>>,
|
db: &Mutex<Database<A, X>>,
|
||||||
chain: &O,
|
chain: &Mutex<O>,
|
||||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||||
network: Network,
|
network: Network,
|
||||||
broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>,
|
broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>,
|
||||||
@ -684,26 +684,31 @@ where
|
|||||||
}
|
}
|
||||||
Commands::Balance => {
|
Commands::Balance => {
|
||||||
let graph = &*graph.lock().unwrap();
|
let graph = &*graph.lock().unwrap();
|
||||||
|
let chain = &*chain.lock().unwrap();
|
||||||
run_balance_cmd(graph, chain).map_err(anyhow::Error::from)
|
run_balance_cmd(graph, chain).map_err(anyhow::Error::from)
|
||||||
}
|
}
|
||||||
Commands::TxOut { txout_cmd } => {
|
Commands::TxOut { txout_cmd } => {
|
||||||
let graph = &*graph.lock().unwrap();
|
let graph = &*graph.lock().unwrap();
|
||||||
|
let chain = &*chain.lock().unwrap();
|
||||||
run_txo_cmd(graph, chain, network, txout_cmd)
|
run_txo_cmd(graph, chain, network, txout_cmd)
|
||||||
}
|
}
|
||||||
Commands::Send {
|
Commands::Send {
|
||||||
value,
|
value,
|
||||||
address,
|
address,
|
||||||
coin_select,
|
coin_select,
|
||||||
} => run_send_cmd(
|
} => {
|
||||||
graph,
|
let chain = &*chain.lock().unwrap();
|
||||||
db,
|
run_send_cmd(
|
||||||
chain,
|
graph,
|
||||||
keymap,
|
db,
|
||||||
coin_select,
|
chain,
|
||||||
address,
|
keymap,
|
||||||
value,
|
coin_select,
|
||||||
broadcast,
|
address,
|
||||||
),
|
value,
|
||||||
|
broadcast,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
example-crates/example_electrum/Cargo.toml
Normal file
11
example-crates/example_electrum/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "example_electrum"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bdk_chain = { path = "../../crates/chain", features = ["serde"] }
|
||||||
|
bdk_electrum = { path = "../../crates/electrum" }
|
||||||
|
example_cli = { path = "../example_cli" }
|
315
example-crates/example_electrum/src/main.rs
Normal file
315
example-crates/example_electrum/src/main.rs
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
io::{self, Write},
|
||||||
|
sync::Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bdk_chain::{
|
||||||
|
bitcoin::{Address, BlockHash, Network, OutPoint, Txid},
|
||||||
|
indexed_tx_graph::IndexedAdditions,
|
||||||
|
local_chain::{self, LocalChain},
|
||||||
|
Append, ConfirmationHeightAnchor,
|
||||||
|
};
|
||||||
|
use bdk_electrum::{
|
||||||
|
electrum_client::{self, ElectrumApi},
|
||||||
|
v2::{ElectrumExt, ElectrumUpdate},
|
||||||
|
};
|
||||||
|
use example_cli::{
|
||||||
|
anyhow::{self, Context},
|
||||||
|
clap::{self, Parser, Subcommand},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DB_MAGIC: &[u8] = b"bdk_example_electrum";
|
||||||
|
const DB_PATH: &str = ".bdk_electrum_example.db";
|
||||||
|
const ASSUME_FINAL_DEPTH: usize = 10;
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug, Clone)]
|
||||||
|
enum ElectrumCommands {
|
||||||
|
/// Scans the addresses in the wallet using the esplora API.
|
||||||
|
Scan {
|
||||||
|
/// When a gap this large has been found for a keychain, it will stop.
|
||||||
|
#[clap(long, default_value = "5")]
|
||||||
|
stop_gap: usize,
|
||||||
|
#[clap(flatten)]
|
||||||
|
scan_options: ScanOptions,
|
||||||
|
},
|
||||||
|
/// Scans particular addresses using the esplora API.
|
||||||
|
Sync {
|
||||||
|
/// Scan all the unused addresses.
|
||||||
|
#[clap(long)]
|
||||||
|
unused_spks: bool,
|
||||||
|
/// Scan every address that you have derived.
|
||||||
|
#[clap(long)]
|
||||||
|
all_spks: bool,
|
||||||
|
/// Scan unspent outpoints for spends or changes to confirmation status of residing tx.
|
||||||
|
#[clap(long)]
|
||||||
|
utxos: bool,
|
||||||
|
/// Scan unconfirmed transactions for updates.
|
||||||
|
#[clap(long)]
|
||||||
|
unconfirmed: bool,
|
||||||
|
#[clap(flatten)]
|
||||||
|
scan_options: ScanOptions,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone, PartialEq)]
|
||||||
|
pub struct ScanOptions {
|
||||||
|
/// Set batch size for each script_history call to electrum client.
|
||||||
|
#[clap(long, default_value = "25")]
|
||||||
|
pub batch_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let (args, keymap, graph, db, chain_changeset) =
|
||||||
|
example_cli::init::<ElectrumCommands, ConfirmationHeightAnchor, local_chain::ChangeSet>(
|
||||||
|
DB_MAGIC, DB_PATH,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let chain = Mutex::new({
|
||||||
|
let mut chain = LocalChain::default();
|
||||||
|
chain.apply_changeset(chain_changeset);
|
||||||
|
chain
|
||||||
|
});
|
||||||
|
|
||||||
|
let electrum_url = match args.network {
|
||||||
|
Network::Bitcoin => "ssl://electrum.blockstream.info:50002",
|
||||||
|
Network::Testnet => "ssl://electrum.blockstream.info:60002",
|
||||||
|
Network::Regtest => "tcp://localhost:60401",
|
||||||
|
Network::Signet => "tcp://signet-electrumx.wakiyamap.dev:50001",
|
||||||
|
};
|
||||||
|
let config = electrum_client::Config::builder()
|
||||||
|
.validate_domain(matches!(args.network, Network::Bitcoin))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let client = electrum_client::Client::from_config(electrum_url, config)?;
|
||||||
|
|
||||||
|
let electrum_cmd = match &args.command {
|
||||||
|
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
|
||||||
|
general_cmd => {
|
||||||
|
let res = example_cli::handle_commands(
|
||||||
|
&graph,
|
||||||
|
&db,
|
||||||
|
&chain,
|
||||||
|
&keymap,
|
||||||
|
args.network,
|
||||||
|
|tx| {
|
||||||
|
client
|
||||||
|
.transaction_broadcast(tx)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(anyhow::Error::from)
|
||||||
|
},
|
||||||
|
general_cmd.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
db.lock().unwrap().commit()?;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = match electrum_cmd.clone() {
|
||||||
|
ElectrumCommands::Scan {
|
||||||
|
stop_gap,
|
||||||
|
scan_options,
|
||||||
|
} => {
|
||||||
|
let (keychain_spks, c) = {
|
||||||
|
let graph = &*graph.lock().unwrap();
|
||||||
|
let chain = &*chain.lock().unwrap();
|
||||||
|
|
||||||
|
let keychain_spks = graph
|
||||||
|
.index
|
||||||
|
.spks_of_all_keychains()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(keychain, iter)| {
|
||||||
|
let mut first = true;
|
||||||
|
let spk_iter = iter.inspect(move |(i, _)| {
|
||||||
|
if first {
|
||||||
|
eprint!("\nscanning {}: ", keychain);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
eprint!("{} ", i);
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
});
|
||||||
|
(keychain, spk_iter)
|
||||||
|
})
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
|
||||||
|
let c = chain
|
||||||
|
.blocks()
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.take(ASSUME_FINAL_DEPTH)
|
||||||
|
.map(|(k, v)| (*k, *v))
|
||||||
|
.collect::<BTreeMap<u32, BlockHash>>();
|
||||||
|
|
||||||
|
(keychain_spks, c)
|
||||||
|
};
|
||||||
|
|
||||||
|
client
|
||||||
|
.scan(
|
||||||
|
&c,
|
||||||
|
keychain_spks,
|
||||||
|
core::iter::empty(),
|
||||||
|
core::iter::empty(),
|
||||||
|
stop_gap,
|
||||||
|
scan_options.batch_size,
|
||||||
|
)
|
||||||
|
.context("scanning the blockchain")?
|
||||||
|
}
|
||||||
|
ElectrumCommands::Sync {
|
||||||
|
mut unused_spks,
|
||||||
|
all_spks,
|
||||||
|
mut utxos,
|
||||||
|
mut unconfirmed,
|
||||||
|
scan_options,
|
||||||
|
} => {
|
||||||
|
// Get a short lock on the tracker to get the spks we're interested in
|
||||||
|
let graph = graph.lock().unwrap();
|
||||||
|
let chain = chain.lock().unwrap();
|
||||||
|
let chain_tip = chain.tip().unwrap_or_default();
|
||||||
|
|
||||||
|
if !(all_spks || unused_spks || utxos || unconfirmed) {
|
||||||
|
unused_spks = true;
|
||||||
|
unconfirmed = true;
|
||||||
|
utxos = true;
|
||||||
|
} else if all_spks {
|
||||||
|
unused_spks = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::Script>> =
|
||||||
|
Box::new(core::iter::empty());
|
||||||
|
if all_spks {
|
||||||
|
let all_spks = graph
|
||||||
|
.index
|
||||||
|
.all_spks()
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (*k, v.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| {
|
||||||
|
eprintln!("scanning {:?}", index);
|
||||||
|
script
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
if unused_spks {
|
||||||
|
let unused_spks = graph
|
||||||
|
.index
|
||||||
|
.unused_spks(..)
|
||||||
|
.map(|(k, v)| (*k, v.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| {
|
||||||
|
eprintln!(
|
||||||
|
"Checking if address {} {:?} has been used",
|
||||||
|
Address::from_script(&script, args.network).unwrap(),
|
||||||
|
index
|
||||||
|
);
|
||||||
|
|
||||||
|
script
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
|
||||||
|
|
||||||
|
if utxos {
|
||||||
|
let init_outpoints = graph.index.outpoints().iter().cloned();
|
||||||
|
|
||||||
|
let utxos = graph
|
||||||
|
.graph()
|
||||||
|
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
|
||||||
|
.map(|(_, utxo)| utxo)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
outpoints = Box::new(
|
||||||
|
utxos
|
||||||
|
.into_iter()
|
||||||
|
.inspect(|utxo| {
|
||||||
|
eprintln!(
|
||||||
|
"Checking if outpoint {} (value: {}) has been spent",
|
||||||
|
utxo.outpoint, utxo.txout.value
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(|utxo| utxo.outpoint),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
|
||||||
|
|
||||||
|
if unconfirmed {
|
||||||
|
let unconfirmed_txids = graph
|
||||||
|
.graph()
|
||||||
|
.list_chain_txs(&*chain, chain_tip)
|
||||||
|
.filter(|canonical_tx| !canonical_tx.observed_as.is_confirmed())
|
||||||
|
.map(|canonical_tx| canonical_tx.node.txid)
|
||||||
|
.collect::<Vec<Txid>>();
|
||||||
|
|
||||||
|
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
|
||||||
|
eprintln!("Checking if {} is confirmed yet", txid);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let c = chain
|
||||||
|
.blocks()
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.take(ASSUME_FINAL_DEPTH)
|
||||||
|
.map(|(k, v)| (*k, *v))
|
||||||
|
.collect::<BTreeMap<u32, BlockHash>>();
|
||||||
|
|
||||||
|
// drop lock on graph and chain
|
||||||
|
drop((graph, chain));
|
||||||
|
|
||||||
|
let update = client
|
||||||
|
.scan_without_keychain(&c, spks, txids, outpoints, scan_options.batch_size)
|
||||||
|
.context("scanning the blockchain")?;
|
||||||
|
ElectrumUpdate {
|
||||||
|
graph_update: update.graph_update,
|
||||||
|
chain_update: update.chain_update,
|
||||||
|
keychain_update: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let missing_txids = {
|
||||||
|
let graph = &*graph.lock().unwrap();
|
||||||
|
response
|
||||||
|
.missing_full_txs(graph.graph())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_txs = client
|
||||||
|
.batch_transaction_get(&missing_txids)
|
||||||
|
.context("fetching full transactions")?;
|
||||||
|
let now = std::time::UNIX_EPOCH
|
||||||
|
.elapsed()
|
||||||
|
.expect("must get time")
|
||||||
|
.as_secs();
|
||||||
|
let final_update = response.finalize(Some(now), new_txs);
|
||||||
|
|
||||||
|
let db_changeset = {
|
||||||
|
let mut chain = chain.lock().unwrap();
|
||||||
|
let mut graph = graph.lock().unwrap();
|
||||||
|
|
||||||
|
let chain_changeset = chain.apply_update(final_update.chain)?;
|
||||||
|
|
||||||
|
let indexed_additions = {
|
||||||
|
let mut additions = IndexedAdditions::<ConfirmationHeightAnchor, _>::default();
|
||||||
|
let (_, index_additions) = graph.index.reveal_to_target_multi(&final_update.keychain);
|
||||||
|
additions.append(IndexedAdditions {
|
||||||
|
index_additions,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
additions.append(graph.apply_update(final_update.graph));
|
||||||
|
additions
|
||||||
|
};
|
||||||
|
|
||||||
|
example_cli::ChangeSet {
|
||||||
|
indexed_additions,
|
||||||
|
extension: chain_changeset,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut db = db.lock().unwrap();
|
||||||
|
db.stage(db_changeset);
|
||||||
|
db.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user