|
| 1 | +use std::{ |
| 2 | + collections::BTreeMap, |
| 3 | + io::{self, Write}, |
| 4 | + time::UNIX_EPOCH, |
| 5 | +}; |
| 6 | + |
| 7 | +use bdk_chain::{ |
| 8 | + bitcoin::{Address, BlockHash, Network, OutPoint, Txid}, |
| 9 | + Append, ConfirmationHeightAnchor, |
| 10 | +}; |
| 11 | +use bdk_electrum::{ |
| 12 | + electrum_client::{self, ElectrumApi}, |
| 13 | + v2::{ElectrumExt, ElectrumUpdate}, |
| 14 | +}; |
| 15 | +use tracker_example_cli::{ |
| 16 | + self as cli, |
| 17 | + anyhow::{self, Context}, |
| 18 | + clap::{self, Parser, Subcommand}, |
| 19 | +}; |
| 20 | + |
| 21 | +const DB_MAGIC: &[u8] = b"bdk_example_electrum"; |
| 22 | + |
| 23 | +#[derive(Subcommand, Debug, Clone)] |
| 24 | +enum ElectrumCommands { |
| 25 | + /// Scans the addresses in the wallet using the esplora API. |
| 26 | + Scan { |
| 27 | + /// When a gap this large has been found for a keychain, it will stop. |
| 28 | + #[clap(long, default_value = "5")] |
| 29 | + stop_gap: usize, |
| 30 | + #[clap(flatten)] |
| 31 | + scan_options: ScanOptions, |
| 32 | + }, |
| 33 | + /// Scans particular addresses using the esplora API. |
| 34 | + Sync { |
| 35 | + /// Scan all the unused addresses. |
| 36 | + #[clap(long)] |
| 37 | + unused_spks: bool, |
| 38 | + /// Scan every address that you have derived. |
| 39 | + #[clap(long)] |
| 40 | + all_spks: bool, |
| 41 | + /// Scan unspent outpoints for spends or changes to confirmation status of residing tx. |
| 42 | + #[clap(long)] |
| 43 | + utxos: bool, |
| 44 | + /// Scan unconfirmed transactions for updates. |
| 45 | + #[clap(long)] |
| 46 | + unconfirmed: bool, |
| 47 | + #[clap(flatten)] |
| 48 | + scan_options: ScanOptions, |
| 49 | + }, |
| 50 | +} |
| 51 | + |
| 52 | +#[derive(Parser, Debug, Clone, PartialEq)] |
| 53 | +pub struct ScanOptions { |
| 54 | + /// Set batch size for each script_history call to electrum client. |
| 55 | + #[clap(long, default_value = "25")] |
| 56 | + pub batch_size: usize, |
| 57 | +} |
| 58 | + |
| 59 | +fn main() -> anyhow::Result<()> { |
| 60 | + let (args, keymap, tracker, db) = cli::init::<ElectrumCommands, ConfirmationHeightAnchor, _>( |
| 61 | + DB_MAGIC, |
| 62 | + cli::Tracker::new_local(), |
| 63 | + )?; |
| 64 | + |
| 65 | + let electrum_url = match args.network { |
| 66 | + Network::Bitcoin => "ssl://electrum.blockstream.info:50002", |
| 67 | + Network::Testnet => "ssl://electrum.blockstream.info:60002", |
| 68 | + Network::Regtest => "tcp://localhost:60401", |
| 69 | + Network::Signet => "tcp://signet-electrumx.wakiyamap.dev:50001", |
| 70 | + }; |
| 71 | + let config = electrum_client::Config::builder() |
| 72 | + .validate_domain(matches!(args.network, Network::Bitcoin)) |
| 73 | + .build(); |
| 74 | + |
| 75 | + let client = electrum_client::Client::from_config(electrum_url, config)?; |
| 76 | + |
| 77 | + // [TODO]: Use genesis block based on network! |
| 78 | + let chain_tip = tracker.lock().unwrap().chain.tip().unwrap_or_default(); |
| 79 | + |
| 80 | + let electrum_cmd = match args.command.clone() { |
| 81 | + cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd, |
| 82 | + general_command => { |
| 83 | + return cli::handle_commands( |
| 84 | + general_command, |
| 85 | + |transaction| { |
| 86 | + let _txid = client.transaction_broadcast(transaction)?; |
| 87 | + Ok(()) |
| 88 | + }, |
| 89 | + &tracker, |
| 90 | + &db, |
| 91 | + chain_tip, |
| 92 | + args.network, |
| 93 | + &keymap, |
| 94 | + ) |
| 95 | + } |
| 96 | + }; |
| 97 | + |
| 98 | + let response = match electrum_cmd { |
| 99 | + ElectrumCommands::Scan { |
| 100 | + stop_gap, |
| 101 | + scan_options, |
| 102 | + } => { |
| 103 | + let (spk_iters, local_chain) = { |
| 104 | + let tracker = &*tracker.lock().unwrap(); |
| 105 | + let spk_iters = tracker |
| 106 | + .indexed_graph |
| 107 | + .index |
| 108 | + .spks_of_all_keychains() |
| 109 | + .into_iter() |
| 110 | + .map(|(keychain, iter)| { |
| 111 | + let mut first = true; |
| 112 | + let spk_iter = iter.inspect(move |(i, _)| { |
| 113 | + if first { |
| 114 | + eprint!("\nscanning {}: ", keychain); |
| 115 | + first = false; |
| 116 | + } |
| 117 | + |
| 118 | + eprint!("{} ", i); |
| 119 | + let _ = io::stdout().flush(); |
| 120 | + }); |
| 121 | + (keychain, spk_iter) |
| 122 | + }) |
| 123 | + .collect::<BTreeMap<_, _>>(); |
| 124 | + let local_chain: BTreeMap<u32, BlockHash> = tracker.chain.clone().into(); |
| 125 | + (spk_iters, local_chain) |
| 126 | + }; |
| 127 | + |
| 128 | + client.scan( |
| 129 | + &local_chain, |
| 130 | + spk_iters, |
| 131 | + core::iter::empty(), |
| 132 | + core::iter::empty(), |
| 133 | + stop_gap, |
| 134 | + scan_options.batch_size, |
| 135 | + )? |
| 136 | + } |
| 137 | + ElectrumCommands::Sync { |
| 138 | + mut unused_spks, |
| 139 | + all_spks, |
| 140 | + mut utxos, |
| 141 | + mut unconfirmed, |
| 142 | + scan_options, |
| 143 | + } => { |
| 144 | + // Get a short lock on the tracker to get the spks we're interested in |
| 145 | + let tracker = tracker.lock().unwrap(); |
| 146 | + |
| 147 | + if !(all_spks || unused_spks || utxos || unconfirmed) { |
| 148 | + unused_spks = true; |
| 149 | + unconfirmed = true; |
| 150 | + utxos = true; |
| 151 | + } else if all_spks { |
| 152 | + unused_spks = false; |
| 153 | + } |
| 154 | + |
| 155 | + let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::Script>> = |
| 156 | + Box::new(core::iter::empty()); |
| 157 | + if all_spks { |
| 158 | + let index = &tracker.indexed_graph.index; |
| 159 | + let all_spks = index |
| 160 | + .all_spks() |
| 161 | + .iter() |
| 162 | + .map(|(k, v)| (*k, v.clone())) |
| 163 | + .collect::<Vec<_>>(); |
| 164 | + spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| { |
| 165 | + eprintln!("scanning {:?}", index); |
| 166 | + script |
| 167 | + }))); |
| 168 | + } |
| 169 | + if unused_spks { |
| 170 | + let index = &tracker.indexed_graph.index; |
| 171 | + let unused_spks = index |
| 172 | + .unused_spks(..) |
| 173 | + .map(|(k, v)| (*k, v.clone())) |
| 174 | + .collect::<Vec<_>>(); |
| 175 | + spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| { |
| 176 | + eprintln!( |
| 177 | + "Checking if address {} {:?} has been used", |
| 178 | + Address::from_script(&script, args.network).unwrap(), |
| 179 | + index |
| 180 | + ); |
| 181 | + |
| 182 | + script |
| 183 | + }))); |
| 184 | + } |
| 185 | + |
| 186 | + let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty()); |
| 187 | + |
| 188 | + if utxos { |
| 189 | + let utxos = tracker |
| 190 | + .list_owned_unspents(chain_tip) |
| 191 | + .map(|(_, utxo)| utxo) |
| 192 | + .collect::<Vec<_>>(); |
| 193 | + outpoints = Box::new( |
| 194 | + utxos |
| 195 | + .into_iter() |
| 196 | + .inspect(|utxo| { |
| 197 | + eprintln!( |
| 198 | + "Checking if outpoint {} (value: {}) has been spent", |
| 199 | + utxo.outpoint, utxo.txout.value |
| 200 | + ); |
| 201 | + }) |
| 202 | + .map(|utxo| utxo.outpoint), |
| 203 | + ); |
| 204 | + }; |
| 205 | + |
| 206 | + let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty()); |
| 207 | + |
| 208 | + if unconfirmed { |
| 209 | + let unconfirmed_txids = tracker |
| 210 | + .list_txs(chain_tip) |
| 211 | + .filter(|ctx| !ctx.observed_as.is_confirmed()) |
| 212 | + .map(|ctx| ctx.node.txid) |
| 213 | + .collect::<Vec<_>>(); |
| 214 | + |
| 215 | + txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| { |
| 216 | + eprintln!("Checking if {} is confirmed yet", txid); |
| 217 | + })); |
| 218 | + } |
| 219 | + |
| 220 | + let local_chain: BTreeMap<u32, BlockHash> = tracker.chain.clone().into(); |
| 221 | + drop(tracker); |
| 222 | + |
| 223 | + let update = client.scan_without_keychain( |
| 224 | + &local_chain, |
| 225 | + spks, |
| 226 | + txids, |
| 227 | + outpoints, |
| 228 | + scan_options.batch_size, |
| 229 | + )?; |
| 230 | + ElectrumUpdate { |
| 231 | + graph_update: update.graph_update, |
| 232 | + chain_update: update.chain_update, |
| 233 | + keychain_update: BTreeMap::new(), |
| 234 | + } |
| 235 | + } |
| 236 | + }; |
| 237 | + println!(); |
| 238 | + |
| 239 | + let missing_txids = { |
| 240 | + let tracker = &*tracker.lock().unwrap(); |
| 241 | + response |
| 242 | + .missing_full_txs(tracker.indexed_graph.graph()) |
| 243 | + .cloned() |
| 244 | + .collect::<Vec<_>>() |
| 245 | + }; |
| 246 | + |
| 247 | + let update = response.finalize( |
| 248 | + UNIX_EPOCH.elapsed().map(|d| d.as_secs()).ok(), |
| 249 | + client |
| 250 | + .batch_transaction_get(&missing_txids) |
| 251 | + .context("fetching full transactions")?, |
| 252 | + ); |
| 253 | + |
| 254 | + { |
| 255 | + use bdk_chain::PersistBackend; |
| 256 | + let tracker = &mut *tracker.lock().unwrap(); |
| 257 | + let db = &mut *db.lock().unwrap(); |
| 258 | + |
| 259 | + let (additions, changeset) = |
| 260 | + update.apply(&mut tracker.indexed_graph, &mut tracker.chain)?; |
| 261 | + |
| 262 | + let mut tracker_changeset = cli::ChangeSet::default(); |
| 263 | + tracker_changeset.append(additions.into()); |
| 264 | + tracker_changeset.append(changeset.into()); |
| 265 | + |
| 266 | + // [TODO] How do we check if changeset is empty? |
| 267 | + // [TODO] When should we flush? |
| 268 | + db.write_changes(&tracker_changeset)?; |
| 269 | + } |
| 270 | + |
| 271 | + Ok(()) |
| 272 | +} |
0 commit comments