2023-05-12 17:43:05 +08:00
|
|
|
use std::{
|
|
|
|
io::{self, Write},
|
|
|
|
sync::Mutex,
|
|
|
|
};
|
|
|
|
|
|
|
|
use bdk_chain::{
|
2024-04-30 14:50:21 +08:00
|
|
|
bitcoin::{constants::genesis_block, Address, Network, Txid},
|
|
|
|
collections::BTreeSet,
|
2023-08-07 17:43:17 +02:00
|
|
|
indexed_tx_graph::{self, IndexedTxGraph},
|
2023-08-21 15:18:16 +03:00
|
|
|
keychain,
|
2023-08-21 11:20:38 +03:00
|
|
|
local_chain::{self, LocalChain},
|
2024-04-30 14:50:21 +08:00
|
|
|
spk_client::{FullScanRequest, SyncRequest},
|
2023-05-12 17:43:05 +08:00
|
|
|
Append, ConfirmationHeightAnchor,
|
|
|
|
};
|
|
|
|
use bdk_electrum::{
|
2023-10-04 16:45:57 +08:00
|
|
|
electrum_client::{self, Client, ElectrumApi},
|
2024-04-30 14:50:21 +08:00
|
|
|
ElectrumExt,
|
2023-05-12 17:43:05 +08:00
|
|
|
};
|
|
|
|
use example_cli::{
|
|
|
|
anyhow::{self, Context},
|
|
|
|
clap::{self, Parser, Subcommand},
|
2023-05-13 23:28:03 +08:00
|
|
|
Keychain,
|
2023-05-12 17:43:05 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
const DB_MAGIC: &[u8] = b"bdk_example_electrum";
|
2023-07-19 17:42:52 +08:00
|
|
|
const DB_PATH: &str = ".bdk_example_electrum.db";
|
2023-05-12 17:43:05 +08:00
|
|
|
|
|
|
|
#[derive(Subcommand, Debug, Clone)]
|
|
|
|
enum ElectrumCommands {
|
2023-05-17 11:48:35 +08:00
|
|
|
/// Scans the addresses in the wallet using the electrum API.
|
2023-05-12 17:43:05 +08:00
|
|
|
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,
|
2023-10-04 16:45:57 +08:00
|
|
|
#[clap(flatten)]
|
|
|
|
electrum_args: ElectrumArgs,
|
2023-05-12 17:43:05 +08:00
|
|
|
},
|
2023-05-17 11:48:35 +08:00
|
|
|
/// Scans particular addresses using the electrum API.
|
2023-05-12 17:43:05 +08:00
|
|
|
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,
|
2023-10-04 16:45:57 +08:00
|
|
|
#[clap(flatten)]
|
|
|
|
electrum_args: ElectrumArgs,
|
2023-05-12 17:43:05 +08:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-10-04 16:45:57 +08:00
|
|
|
impl ElectrumCommands {
|
|
|
|
fn electrum_args(&self) -> ElectrumArgs {
|
|
|
|
match self {
|
|
|
|
ElectrumCommands::Scan { electrum_args, .. } => electrum_args.clone(),
|
|
|
|
ElectrumCommands::Sync { electrum_args, .. } => electrum_args.clone(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(clap::Args, Debug, Clone)]
|
|
|
|
pub struct ElectrumArgs {
|
|
|
|
/// The electrum url to use to connect to. If not provided it will use a default electrum server
|
|
|
|
/// for your chosen network.
|
|
|
|
electrum_url: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ElectrumArgs {
|
|
|
|
pub fn client(&self, network: Network) -> anyhow::Result<Client> {
|
|
|
|
let electrum_url = self.electrum_url.as_deref().unwrap_or(match 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",
|
|
|
|
_ => panic!("Unknown network"),
|
|
|
|
});
|
|
|
|
let config = electrum_client::Config::builder()
|
|
|
|
.validate_domain(matches!(network, Network::Bitcoin))
|
|
|
|
.build();
|
|
|
|
|
|
|
|
Ok(electrum_client::Client::from_config(electrum_url, config)?)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-12 17:43:05 +08:00
|
|
|
#[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,
|
|
|
|
}
|
|
|
|
|
2023-08-21 15:18:16 +03:00
|
|
|
type ChangeSet = (
|
|
|
|
local_chain::ChangeSet,
|
|
|
|
indexed_tx_graph::ChangeSet<ConfirmationHeightAnchor, keychain::ChangeSet<Keychain>>,
|
|
|
|
);
|
2023-05-13 23:28:03 +08:00
|
|
|
|
2023-05-12 17:43:05 +08:00
|
|
|
fn main() -> anyhow::Result<()> {
|
2024-01-30 17:56:51 -05:00
|
|
|
let example_cli::Init {
|
|
|
|
args,
|
|
|
|
keymap,
|
|
|
|
index,
|
|
|
|
db,
|
|
|
|
init_changeset,
|
|
|
|
} = example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
|
|
|
|
|
|
|
let (disk_local_chain, disk_tx_graph) = init_changeset;
|
2023-05-13 23:28:03 +08:00
|
|
|
|
|
|
|
let graph = Mutex::new({
|
|
|
|
let mut graph = IndexedTxGraph::new(index);
|
2023-09-06 09:47:45 +03:00
|
|
|
graph.apply_changeset(disk_tx_graph);
|
2023-05-13 23:28:03 +08:00
|
|
|
graph
|
|
|
|
});
|
2023-05-12 17:43:05 +08:00
|
|
|
|
2023-11-28 13:23:05 -05:00
|
|
|
let chain = Mutex::new({
|
|
|
|
let genesis_hash = genesis_block(args.network).block_hash();
|
|
|
|
let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
|
|
|
|
chain.apply_changeset(&disk_local_chain)?;
|
|
|
|
chain
|
|
|
|
});
|
2023-05-12 17:43:05 +08:00
|
|
|
|
|
|
|
let electrum_cmd = match &args.command {
|
|
|
|
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
|
|
|
|
general_cmd => {
|
2024-01-19 11:23:46 +11:00
|
|
|
return example_cli::handle_commands(
|
2023-05-12 17:43:05 +08:00
|
|
|
&graph,
|
|
|
|
&db,
|
|
|
|
&chain,
|
|
|
|
&keymap,
|
|
|
|
args.network,
|
2023-10-04 16:45:57 +08:00
|
|
|
|electrum_args, tx| {
|
|
|
|
let client = electrum_args.client(args.network)?;
|
|
|
|
client.transaction_broadcast(tx)?;
|
|
|
|
Ok(())
|
2023-05-12 17:43:05 +08:00
|
|
|
},
|
|
|
|
general_cmd.clone(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-10-04 16:45:57 +08:00
|
|
|
let client = electrum_cmd.electrum_args().client(args.network)?;
|
|
|
|
|
2024-04-30 14:50:21 +08:00
|
|
|
let (chain_update, mut graph_update, keychain_update) = match electrum_cmd.clone() {
|
2023-05-12 17:43:05 +08:00
|
|
|
ElectrumCommands::Scan {
|
|
|
|
stop_gap,
|
|
|
|
scan_options,
|
2023-10-04 16:45:57 +08:00
|
|
|
..
|
2023-05-12 17:43:05 +08:00
|
|
|
} => {
|
2024-04-30 14:50:21 +08:00
|
|
|
let request = {
|
2023-05-12 17:43:05 +08:00
|
|
|
let graph = &*graph.lock().unwrap();
|
|
|
|
let chain = &*chain.lock().unwrap();
|
|
|
|
|
2024-04-30 14:50:21 +08:00
|
|
|
FullScanRequest::from_chain_tip(chain.tip())
|
|
|
|
.cache_graph_txs(graph.graph())
|
|
|
|
.set_spks_for_keychain(
|
|
|
|
Keychain::External,
|
|
|
|
graph
|
|
|
|
.index
|
|
|
|
.unbounded_spk_iter(&Keychain::External)
|
|
|
|
.into_iter()
|
|
|
|
.flatten(),
|
|
|
|
)
|
|
|
|
.set_spks_for_keychain(
|
|
|
|
Keychain::Internal,
|
|
|
|
graph
|
|
|
|
.index
|
|
|
|
.unbounded_spk_iter(&Keychain::Internal)
|
|
|
|
.into_iter()
|
|
|
|
.flatten(),
|
|
|
|
)
|
|
|
|
.inspect_spks_for_all_keychains({
|
|
|
|
let mut once = BTreeSet::new();
|
|
|
|
move |k, spk_i, _| {
|
|
|
|
if once.insert(k) {
|
|
|
|
eprint!("\nScanning {}: ", k);
|
|
|
|
} else {
|
|
|
|
eprint!("{} ", spk_i);
|
2023-05-12 17:43:05 +08:00
|
|
|
}
|
|
|
|
let _ = io::stdout().flush();
|
2024-04-30 14:50:21 +08:00
|
|
|
}
|
2023-05-12 17:43:05 +08:00
|
|
|
})
|
|
|
|
};
|
|
|
|
|
2024-04-30 14:50:21 +08:00
|
|
|
let res = client
|
|
|
|
.full_scan::<_>(request, stop_gap, scan_options.batch_size)
|
|
|
|
.context("scanning the blockchain")?;
|
|
|
|
(
|
|
|
|
res.chain_update,
|
|
|
|
res.graph_update,
|
|
|
|
Some(res.last_active_indices),
|
|
|
|
)
|
2023-05-12 17:43:05 +08:00
|
|
|
}
|
|
|
|
ElectrumCommands::Sync {
|
|
|
|
mut unused_spks,
|
|
|
|
all_spks,
|
|
|
|
mut utxos,
|
|
|
|
mut unconfirmed,
|
|
|
|
scan_options,
|
2023-10-04 16:45:57 +08:00
|
|
|
..
|
2023-05-12 17:43:05 +08:00
|
|
|
} => {
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
if !(all_spks || unused_spks || utxos || unconfirmed) {
|
|
|
|
unused_spks = true;
|
|
|
|
unconfirmed = true;
|
|
|
|
utxos = true;
|
|
|
|
} else if all_spks {
|
|
|
|
unused_spks = false;
|
|
|
|
}
|
|
|
|
|
2024-04-30 14:50:21 +08:00
|
|
|
let chain_tip = chain.tip();
|
|
|
|
let mut request =
|
|
|
|
SyncRequest::from_chain_tip(chain_tip.clone()).cache_graph_txs(graph.graph());
|
|
|
|
|
2023-05-12 17:43:05 +08:00
|
|
|
if all_spks {
|
|
|
|
let all_spks = graph
|
|
|
|
.index
|
2024-02-06 17:31:22 +11:00
|
|
|
.revealed_spks(..)
|
|
|
|
.map(|(k, i, spk)| (k.to_owned(), i, spk.to_owned()))
|
2023-05-12 17:43:05 +08:00
|
|
|
.collect::<Vec<_>>();
|
2024-04-30 14:50:21 +08:00
|
|
|
request = request.chain_spks(all_spks.into_iter().map(|(k, spk_i, spk)| {
|
|
|
|
eprintln!("scanning {}: {}", k, spk_i);
|
2024-01-13 20:04:49 +08:00
|
|
|
spk
|
2024-04-30 14:50:21 +08:00
|
|
|
}));
|
2023-05-12 17:43:05 +08:00
|
|
|
}
|
|
|
|
if unused_spks {
|
|
|
|
let unused_spks = graph
|
|
|
|
.index
|
2024-01-13 20:04:49 +08:00
|
|
|
.unused_spks()
|
|
|
|
.map(|(k, i, spk)| (k, i, spk.to_owned()))
|
2023-05-12 17:43:05 +08:00
|
|
|
.collect::<Vec<_>>();
|
2024-04-30 14:50:21 +08:00
|
|
|
request =
|
|
|
|
request.chain_spks(unused_spks.into_iter().map(move |(k, spk_i, spk)| {
|
|
|
|
eprintln!(
|
|
|
|
"Checking if address {} {}:{} has been used",
|
|
|
|
Address::from_script(&spk, args.network).unwrap(),
|
|
|
|
k,
|
|
|
|
spk_i,
|
|
|
|
);
|
|
|
|
spk
|
|
|
|
}));
|
2023-05-12 17:43:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if utxos {
|
2024-01-15 18:52:03 +01:00
|
|
|
let init_outpoints = graph.index.outpoints();
|
2023-05-12 17:43:05 +08:00
|
|
|
|
|
|
|
let utxos = graph
|
|
|
|
.graph()
|
2024-04-30 14:50:21 +08:00
|
|
|
.filter_chain_unspents(&*chain, chain_tip.block_id(), init_outpoints)
|
2023-05-12 17:43:05 +08:00
|
|
|
.map(|(_, utxo)| utxo)
|
|
|
|
.collect::<Vec<_>>();
|
2024-04-30 14:50:21 +08:00
|
|
|
request = request.chain_outpoints(utxos.into_iter().map(|utxo| {
|
|
|
|
eprintln!(
|
|
|
|
"Checking if outpoint {} (value: {}) has been spent",
|
|
|
|
utxo.outpoint, utxo.txout.value
|
|
|
|
);
|
|
|
|
utxo.outpoint
|
|
|
|
}));
|
2023-05-12 17:43:05 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
if unconfirmed {
|
|
|
|
let unconfirmed_txids = graph
|
|
|
|
.graph()
|
2024-04-30 14:50:21 +08:00
|
|
|
.list_chain_txs(&*chain, chain_tip.block_id())
|
2023-08-07 17:43:17 +02:00
|
|
|
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
|
|
|
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
2023-05-12 17:43:05 +08:00
|
|
|
.collect::<Vec<Txid>>();
|
|
|
|
|
2024-04-30 14:50:21 +08:00
|
|
|
request = request.chain_txids(
|
|
|
|
unconfirmed_txids
|
|
|
|
.into_iter()
|
|
|
|
.inspect(|txid| eprintln!("Checking if {} is confirmed yet", txid)),
|
|
|
|
);
|
2023-05-12 17:43:05 +08:00
|
|
|
}
|
|
|
|
|
2024-04-30 14:50:21 +08:00
|
|
|
let res = client
|
|
|
|
.sync(request, scan_options.batch_size)
|
2024-04-11 17:57:14 -04:00
|
|
|
.context("scanning the blockchain")?;
|
2023-05-12 17:43:05 +08:00
|
|
|
|
|
|
|
// drop lock on graph and chain
|
|
|
|
drop((graph, chain));
|
|
|
|
|
2024-04-30 14:50:21 +08:00
|
|
|
(res.chain_update, res.graph_update, None)
|
2023-05-12 17:43:05 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let now = std::time::UNIX_EPOCH
|
|
|
|
.elapsed()
|
|
|
|
.expect("must get time")
|
|
|
|
.as_secs();
|
2024-04-02 10:19:56 -04:00
|
|
|
let _ = graph_update.update_last_seen_unconfirmed(now);
|
2023-05-12 17:43:05 +08:00
|
|
|
|
|
|
|
let db_changeset = {
|
|
|
|
let mut chain = chain.lock().unwrap();
|
|
|
|
let mut graph = graph.lock().unwrap();
|
|
|
|
|
2024-04-30 14:50:21 +08:00
|
|
|
let chain_changeset = chain.apply_update(chain_update)?;
|
|
|
|
|
|
|
|
let mut indexed_tx_graph_changeset =
|
|
|
|
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
|
|
|
|
if let Some(keychain_update) = keychain_update {
|
|
|
|
let (_, keychain_changeset) = graph.index.reveal_to_target_multi(&keychain_update);
|
|
|
|
indexed_tx_graph_changeset.append(keychain_changeset.into());
|
|
|
|
}
|
|
|
|
indexed_tx_graph_changeset.append(graph.apply_update(graph_update));
|
|
|
|
|
|
|
|
(chain_changeset, indexed_tx_graph_changeset)
|
2023-05-12 17:43:05 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
let mut db = db.lock().unwrap();
|
|
|
|
db.stage(db_changeset);
|
|
|
|
db.commit()?;
|
|
|
|
Ok(())
|
|
|
|
}
|