]> Untitled Git - bdk/commitdiff
feat(example_cli): allow chain specific args in examples
author志宇 <hello@evanlinjin.me>
Wed, 4 Oct 2023 08:45:57 +0000 (16:45 +0800)
committer志宇 <hello@evanlinjin.me>
Mon, 9 Oct 2023 14:14:02 +0000 (22:14 +0800)
So you can pass in the esplora/electrum/bitcoind_rpc server details in
the example.

Co-authored-by: LLFourn <lloyd.fourn@gmail.com>
example-crates/example_cli/src/lib.rs
example-crates/example_electrum/src/main.rs
example-crates/example_esplora/src/main.rs

index c9459c353f9796db8e7cb05effcdcba4b292e3db..1982c30c659cb44c32075487f1117b33cac981fd 100644 (file)
@@ -34,7 +34,7 @@ pub type Database<'m, C> = Persist<Store<'m, C>, C>;
 #[derive(Parser)]
 #[clap(author, version, about, long_about = None)]
 #[clap(propagate_version = true)]
-pub struct Args<S: clap::Subcommand> {
+pub struct Args<CS: clap::Subcommand, S: clap::Args> {
     #[clap(env = "DESCRIPTOR")]
     pub descriptor: String,
     #[clap(env = "CHANGE_DESCRIPTOR")]
@@ -50,14 +50,14 @@ pub struct Args<S: clap::Subcommand> {
     pub cp_limit: usize,
 
     #[clap(subcommand)]
-    pub command: Commands<S>,
+    pub command: Commands<CS, S>,
 }
 
 #[allow(clippy::almost_swapped)]
 #[derive(Subcommand, Debug, Clone)]
-pub enum Commands<S: clap::Subcommand> {
+pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
     #[clap(flatten)]
-    ChainSpecific(S),
+    ChainSpecific(CS),
     /// Address generation and inspection.
     Address {
         #[clap(subcommand)]
@@ -77,6 +77,8 @@ pub enum Commands<S: clap::Subcommand> {
         address: Address<address::NetworkUnchecked>,
         #[clap(short, default_value = "bnb")]
         coin_select: CoinSelectionAlgo,
+        #[clap(flatten)]
+        chain_specfic: S,
     },
 }
 
@@ -183,225 +185,6 @@ impl core::fmt::Display for Keychain {
     }
 }
 
-pub fn run_address_cmd<A, C>(
-    graph: &mut KeychainTxGraph<A>,
-    db: &Mutex<Database<C>>,
-    network: Network,
-    cmd: AddressCmd,
-) -> anyhow::Result<()>
-where
-    C: Default + Append + DeserializeOwned + Serialize + From<KeychainChangeSet<A>>,
-{
-    let index = &mut graph.index;
-
-    match cmd {
-        AddressCmd::Next | AddressCmd::New => {
-            let spk_chooser = match cmd {
-                AddressCmd::Next => KeychainTxOutIndex::next_unused_spk,
-                AddressCmd::New => KeychainTxOutIndex::reveal_next_spk,
-                _ => unreachable!("only these two variants exist in match arm"),
-            };
-
-            let ((spk_i, spk), index_changeset) = spk_chooser(index, &Keychain::External);
-            let db = &mut *db.lock().unwrap();
-            db.stage(C::from((
-                local_chain::ChangeSet::default(),
-                indexed_tx_graph::ChangeSet::from(index_changeset),
-            )));
-            db.commit()?;
-            let addr = Address::from_script(spk, network).context("failed to derive address")?;
-            println!("[address @ {}] {}", spk_i, addr);
-            Ok(())
-        }
-        AddressCmd::Index => {
-            for (keychain, derivation_index) in index.last_revealed_indices() {
-                println!("{:?}: {}", keychain, derivation_index);
-            }
-            Ok(())
-        }
-        AddressCmd::List { change } => {
-            let target_keychain = match change {
-                true => Keychain::Internal,
-                false => Keychain::External,
-            };
-            for (spk_i, spk) in index.revealed_spks_of_keychain(&target_keychain) {
-                let address = Address::from_script(spk, network)
-                    .expect("should always be able to derive address");
-                println!(
-                    "{:?} {} used:{}",
-                    spk_i,
-                    address,
-                    index.is_used(&(target_keychain, spk_i))
-                );
-            }
-            Ok(())
-        }
-    }
-}
-
-pub fn run_balance_cmd<A: Anchor, O: ChainOracle>(
-    graph: &KeychainTxGraph<A>,
-    chain: &O,
-) -> Result<(), O::Error> {
-    fn print_balances<'a>(title_str: &'a str, items: impl IntoIterator<Item = (&'a str, u64)>) {
-        println!("{}:", title_str);
-        for (name, amount) in items.into_iter() {
-            println!("    {:<10} {:>12} sats", name, amount)
-        }
-    }
-
-    let balance = graph.graph().try_balance(
-        chain,
-        chain.get_chain_tip()?.unwrap_or_default(),
-        graph.index.outpoints().iter().cloned(),
-        |(k, _), _| k == &Keychain::Internal,
-    )?;
-
-    let confirmed_total = balance.confirmed + balance.immature;
-    let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;
-
-    print_balances(
-        "confirmed",
-        [
-            ("total", confirmed_total),
-            ("spendable", balance.confirmed),
-            ("immature", balance.immature),
-        ],
-    );
-    print_balances(
-        "unconfirmed",
-        [
-            ("total", unconfirmed_total),
-            ("trusted", balance.trusted_pending),
-            ("untrusted", balance.untrusted_pending),
-        ],
-    );
-
-    Ok(())
-}
-
-pub fn run_txo_cmd<A: Anchor, O: ChainOracle>(
-    graph: &KeychainTxGraph<A>,
-    chain: &O,
-    network: Network,
-    cmd: TxOutCmd,
-) -> anyhow::Result<()>
-where
-    O::Error: std::error::Error + Send + Sync + 'static,
-{
-    let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
-    let outpoints = graph.index.outpoints().iter().cloned();
-
-    match cmd {
-        TxOutCmd::List {
-            spent,
-            unspent,
-            confirmed,
-            unconfirmed,
-        } => {
-            let txouts = graph
-                .graph()
-                .try_filter_chain_txouts(chain, chain_tip, outpoints)
-                .filter(|r| match r {
-                    Ok((_, full_txo)) => match (spent, unspent) {
-                        (true, false) => full_txo.spent_by.is_some(),
-                        (false, true) => full_txo.spent_by.is_none(),
-                        _ => true,
-                    },
-                    // always keep errored items
-                    Err(_) => true,
-                })
-                .filter(|r| match r {
-                    Ok((_, full_txo)) => match (confirmed, unconfirmed) {
-                        (true, false) => full_txo.chain_position.is_confirmed(),
-                        (false, true) => !full_txo.chain_position.is_confirmed(),
-                        _ => true,
-                    },
-                    // always keep errored items
-                    Err(_) => true,
-                })
-                .collect::<Result<Vec<_>, _>>()?;
-
-            for (spk_i, full_txo) in txouts {
-                let addr = Address::from_script(&full_txo.txout.script_pubkey, network)?;
-                println!(
-                    "{:?} {} {} {} spent:{:?}",
-                    spk_i, full_txo.txout.value, full_txo.outpoint, addr, full_txo.spent_by
-                )
-            }
-            Ok(())
-        }
-    }
-}
-
-#[allow(clippy::too_many_arguments)]
-pub fn run_send_cmd<A: Anchor, O: ChainOracle, C>(
-    graph: &Mutex<KeychainTxGraph<A>>,
-    db: &Mutex<Database<'_, C>>,
-    chain: &O,
-    keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
-    cs_algorithm: CoinSelectionAlgo,
-    address: Address,
-    value: u64,
-    broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>,
-) -> anyhow::Result<()>
-where
-    O::Error: std::error::Error + Send + Sync + 'static,
-    C: Default + Append + DeserializeOwned + Serialize + From<KeychainChangeSet<A>>,
-{
-    let (transaction, change_index) = {
-        let graph = &mut *graph.lock().unwrap();
-        // take mutable ref to construct tx -- it is only open for a short time while building it.
-        let (tx, change_info) = create_tx(graph, chain, keymap, cs_algorithm, address, value)?;
-
-        if let Some((index_changeset, (change_keychain, index))) = change_info {
-            // We must first persist to disk the fact that we've got a new address from the
-            // change keychain so future scans will find the tx we're about to broadcast.
-            // If we're unable to persist this, then we don't want to broadcast.
-            {
-                let db = &mut *db.lock().unwrap();
-                db.stage(C::from((
-                    local_chain::ChangeSet::default(),
-                    indexed_tx_graph::ChangeSet::from(index_changeset),
-                )));
-                db.commit()?;
-            }
-
-            // We don't want other callers/threads to use this address while we're using it
-            // but we also don't want to scan the tx we just created because it's not
-            // technically in the blockchain yet.
-            graph.index.mark_used(&change_keychain, index);
-            (tx, Some((change_keychain, index)))
-        } else {
-            (tx, None)
-        }
-    };
-
-    match (broadcast)(&transaction) {
-        Ok(_) => {
-            println!("Broadcasted Tx : {}", transaction.txid());
-
-            let keychain_changeset = graph.lock().unwrap().insert_tx(&transaction, None, None);
-
-            // We know the tx is at least unconfirmed now. Note if persisting here fails,
-            // it's not a big deal since we can always find it again form
-            // blockchain.
-            db.lock().unwrap().stage(C::from((
-                local_chain::ChangeSet::default(),
-                keychain_changeset,
-            )));
-            Ok(())
-        }
-        Err(e) => {
-            if let Some((keychain, index)) = change_index {
-                // We failed to broadcast, so allow our change address to be used in the future
-                graph.lock().unwrap().index.unmark_used(&keychain, index);
-            }
-            Err(e)
-        }
-    }
-}
-
 #[allow(clippy::type_complexity)]
 pub fn create_tx<A: Anchor, O: ChainOracle>(
     graph: &mut KeychainTxGraph<A>,
@@ -647,14 +430,14 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
         .collect()
 }
 
-pub fn handle_commands<S: clap::Subcommand, A: Anchor, O: ChainOracle, C>(
+pub fn handle_commands<CS: clap::Subcommand, S: clap::Args, A: Anchor, O: ChainOracle, C>(
     graph: &Mutex<KeychainTxGraph<A>>,
     db: &Mutex<Database<C>>,
     chain: &Mutex<O>,
     keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
     network: Network,
-    broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>,
-    cmd: Commands<S>,
+    broadcast: impl FnOnce(S, &Transaction) -> anyhow::Result<()>,
+    cmd: Commands<CS, S>,
 ) -> anyhow::Result<()>
 where
     O::Error: std::error::Error + Send + Sync + 'static,
@@ -664,45 +447,213 @@ where
         Commands::ChainSpecific(_) => unreachable!("example code should handle this!"),
         Commands::Address { addr_cmd } => {
             let graph = &mut *graph.lock().unwrap();
-            run_address_cmd(graph, db, network, addr_cmd)
+            let index = &mut graph.index;
+
+            match addr_cmd {
+                AddressCmd::Next | AddressCmd::New => {
+                    let spk_chooser = match addr_cmd {
+                        AddressCmd::Next => KeychainTxOutIndex::next_unused_spk,
+                        AddressCmd::New => KeychainTxOutIndex::reveal_next_spk,
+                        _ => unreachable!("only these two variants exist in match arm"),
+                    };
+
+                    let ((spk_i, spk), index_changeset) = spk_chooser(index, &Keychain::External);
+                    let db = &mut *db.lock().unwrap();
+                    db.stage(C::from((
+                        local_chain::ChangeSet::default(),
+                        indexed_tx_graph::ChangeSet::from(index_changeset),
+                    )));
+                    db.commit()?;
+                    let addr =
+                        Address::from_script(spk, network).context("failed to derive address")?;
+                    println!("[address @ {}] {}", spk_i, addr);
+                    Ok(())
+                }
+                AddressCmd::Index => {
+                    for (keychain, derivation_index) in index.last_revealed_indices() {
+                        println!("{:?}: {}", keychain, derivation_index);
+                    }
+                    Ok(())
+                }
+                AddressCmd::List { change } => {
+                    let target_keychain = match change {
+                        true => Keychain::Internal,
+                        false => Keychain::External,
+                    };
+                    for (spk_i, spk) in index.revealed_spks_of_keychain(&target_keychain) {
+                        let address = Address::from_script(spk, network)
+                            .expect("should always be able to derive address");
+                        println!(
+                            "{:?} {} used:{}",
+                            spk_i,
+                            address,
+                            index.is_used(&(target_keychain, spk_i))
+                        );
+                    }
+                    Ok(())
+                }
+            }
         }
         Commands::Balance => {
             let graph = &*graph.lock().unwrap();
             let chain = &*chain.lock().unwrap();
-            run_balance_cmd(graph, chain).map_err(anyhow::Error::from)
+            fn print_balances<'a>(
+                title_str: &'a str,
+                items: impl IntoIterator<Item = (&'a str, u64)>,
+            ) {
+                println!("{}:", title_str);
+                for (name, amount) in items.into_iter() {
+                    println!("    {:<10} {:>12} sats", name, amount)
+                }
+            }
+
+            let balance = graph.graph().try_balance(
+                chain,
+                chain.get_chain_tip()?.unwrap_or_default(),
+                graph.index.outpoints().iter().cloned(),
+                |(k, _), _| k == &Keychain::Internal,
+            )?;
+
+            let confirmed_total = balance.confirmed + balance.immature;
+            let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;
+
+            print_balances(
+                "confirmed",
+                [
+                    ("total", confirmed_total),
+                    ("spendable", balance.confirmed),
+                    ("immature", balance.immature),
+                ],
+            );
+            print_balances(
+                "unconfirmed",
+                [
+                    ("total", unconfirmed_total),
+                    ("trusted", balance.trusted_pending),
+                    ("untrusted", balance.untrusted_pending),
+                ],
+            );
+
+            Ok(())
         }
         Commands::TxOut { txout_cmd } => {
             let graph = &*graph.lock().unwrap();
             let chain = &*chain.lock().unwrap();
-            run_txo_cmd(graph, chain, network, txout_cmd)
+            let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
+            let outpoints = graph.index.outpoints().iter().cloned();
+
+            match txout_cmd {
+                TxOutCmd::List {
+                    spent,
+                    unspent,
+                    confirmed,
+                    unconfirmed,
+                } => {
+                    let txouts = graph
+                        .graph()
+                        .try_filter_chain_txouts(chain, chain_tip, outpoints)
+                        .filter(|r| match r {
+                            Ok((_, full_txo)) => match (spent, unspent) {
+                                (true, false) => full_txo.spent_by.is_some(),
+                                (false, true) => full_txo.spent_by.is_none(),
+                                _ => true,
+                            },
+                            // always keep errored items
+                            Err(_) => true,
+                        })
+                        .filter(|r| match r {
+                            Ok((_, full_txo)) => match (confirmed, unconfirmed) {
+                                (true, false) => full_txo.chain_position.is_confirmed(),
+                                (false, true) => !full_txo.chain_position.is_confirmed(),
+                                _ => true,
+                            },
+                            // always keep errored items
+                            Err(_) => true,
+                        })
+                        .collect::<Result<Vec<_>, _>>()?;
+
+                    for (spk_i, full_txo) in txouts {
+                        let addr = Address::from_script(&full_txo.txout.script_pubkey, network)?;
+                        println!(
+                            "{:?} {} {} {} spent:{:?}",
+                            spk_i, full_txo.txout.value, full_txo.outpoint, addr, full_txo.spent_by
+                        )
+                    }
+                    Ok(())
+                }
+            }
         }
         Commands::Send {
             value,
             address,
             coin_select,
+            chain_specfic,
         } => {
             let chain = &*chain.lock().unwrap();
             let address = address.require_network(network)?;
-            run_send_cmd(
-                graph,
-                db,
-                chain,
-                keymap,
-                coin_select,
-                address,
-                value,
-                broadcast,
-            )
+            let (transaction, change_index) = {
+                let graph = &mut *graph.lock().unwrap();
+                // take mutable ref to construct tx -- it is only open for a short time while building it.
+                let (tx, change_info) =
+                    create_tx(graph, chain, keymap, coin_select, address, value)?;
+
+                if let Some((index_changeset, (change_keychain, index))) = change_info {
+                    // We must first persist to disk the fact that we've got a new address from the
+                    // change keychain so future scans will find the tx we're about to broadcast.
+                    // If we're unable to persist this, then we don't want to broadcast.
+                    {
+                        let db = &mut *db.lock().unwrap();
+                        db.stage(C::from((
+                            local_chain::ChangeSet::default(),
+                            indexed_tx_graph::ChangeSet::from(index_changeset),
+                        )));
+                        db.commit()?;
+                    }
+
+                    // We don't want other callers/threads to use this address while we're using it
+                    // but we also don't want to scan the tx we just created because it's not
+                    // technically in the blockchain yet.
+                    graph.index.mark_used(&change_keychain, index);
+                    (tx, Some((change_keychain, index)))
+                } else {
+                    (tx, None)
+                }
+            };
+
+            match (broadcast)(chain_specfic, &transaction) {
+                Ok(_) => {
+                    println!("Broadcasted Tx : {}", transaction.txid());
+
+                    let keychain_changeset =
+                        graph.lock().unwrap().insert_tx(&transaction, None, None);
+
+                    // We know the tx is at least unconfirmed now. Note if persisting here fails,
+                    // it's not a big deal since we can always find it again form
+                    // blockchain.
+                    db.lock().unwrap().stage(C::from((
+                        local_chain::ChangeSet::default(),
+                        keychain_changeset,
+                    )));
+                    Ok(())
+                }
+                Err(e) => {
+                    if let Some((keychain, index)) = change_index {
+                        // We failed to broadcast, so allow our change address to be used in the future
+                        graph.lock().unwrap().index.unmark_used(&keychain, index);
+                    }
+                    Err(e)
+                }
+            }
         }
     }
 }
 
 #[allow(clippy::type_complexity)]
-pub fn init<'m, S: clap::Subcommand, C>(
+pub fn init<'m, CS: clap::Subcommand, S: clap::Args, C>(
     db_magic: &'m [u8],
     db_default_path: &str,
 ) -> anyhow::Result<(
-    Args<S>,
+    Args<CS, S>,
     KeyMap,
     KeychainTxOutIndex<Keychain>,
     Mutex<Database<'m, C>>,
@@ -714,7 +665,7 @@ where
     if std::env::var("BDK_DB_PATH").is_err() {
         std::env::set_var("BDK_DB_PATH", db_default_path);
     }
-    let args = Args::<S>::parse();
+    let args = Args::<CS, S>::parse();
     let secp = Secp256k1::default();
 
     let mut index = KeychainTxOutIndex::<Keychain>::default();
index a05e85c5741199ff1a8706aaa0e09aa54e4613a9..be5ffc7bc86dadb2c6db0ea27989cfc526118000 100644 (file)
@@ -12,7 +12,7 @@ use bdk_chain::{
     Append, ConfirmationHeightAnchor,
 };
 use bdk_electrum::{
-    electrum_client::{self, ElectrumApi},
+    electrum_client::{self, Client, ElectrumApi},
     ElectrumExt, ElectrumUpdate,
 };
 use example_cli::{
@@ -33,6 +33,8 @@ enum ElectrumCommands {
         stop_gap: usize,
         #[clap(flatten)]
         scan_options: ScanOptions,
+        #[clap(flatten)]
+        electrum_args: ElectrumArgs,
     },
     /// Scans particular addresses using the electrum API.
     Sync {
@@ -50,9 +52,44 @@ enum ElectrumCommands {
         unconfirmed: bool,
         #[clap(flatten)]
         scan_options: ScanOptions,
+        #[clap(flatten)]
+        electrum_args: ElectrumArgs,
     },
 }
 
+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)?)
+    }
+}
+
 #[derive(Parser, Debug, Clone, PartialEq)]
 pub struct ScanOptions {
     /// Set batch size for each script_history call to electrum client.
@@ -67,7 +104,7 @@ type ChangeSet = (
 
 fn main() -> anyhow::Result<()> {
     let (args, keymap, index, db, (disk_local_chain, disk_tx_graph)) =
-        example_cli::init::<ElectrumCommands, ChangeSet>(DB_MAGIC, DB_PATH)?;
+        example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
 
     let graph = Mutex::new({
         let mut graph = IndexedTxGraph::new(index);
@@ -77,19 +114,6 @@ fn main() -> anyhow::Result<()> {
 
     let chain = Mutex::new(LocalChain::from_changeset(disk_local_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",
-        _ => panic!("Unknown network"),
-    };
-    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 => {
@@ -99,11 +123,10 @@ fn main() -> anyhow::Result<()> {
                 &chain,
                 &keymap,
                 args.network,
-                |tx| {
-                    client
-                        .transaction_broadcast(tx)
-                        .map(|_| ())
-                        .map_err(anyhow::Error::from)
+                |electrum_args, tx| {
+                    let client = electrum_args.client(args.network)?;
+                    client.transaction_broadcast(tx)?;
+                    Ok(())
                 },
                 general_cmd.clone(),
             );
@@ -113,10 +136,13 @@ fn main() -> anyhow::Result<()> {
         }
     };
 
+    let client = electrum_cmd.electrum_args().client(args.network)?;
+
     let response = match electrum_cmd.clone() {
         ElectrumCommands::Scan {
             stop_gap,
             scan_options,
+            ..
         } => {
             let (keychain_spks, tip) = {
                 let graph = &*graph.lock().unwrap();
@@ -162,6 +188,7 @@ fn main() -> anyhow::Result<()> {
             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();
index 5791fe61a5c759f5ec84b43ae80fe3900a2da66f..d2ba62d0bb1eea667c7dfb46429960eb7628edd6 100644 (file)
@@ -37,6 +37,8 @@ enum EsploraCommands {
         stop_gap: usize,
         #[clap(flatten)]
         scan_options: ScanOptions,
+        #[clap(flatten)]
+        esplora_args: EsploraArgs,
     },
     /// Scan for particular addresses and unconfirmed transactions using the esplora API.
     Sync {
@@ -54,8 +56,40 @@ enum EsploraCommands {
         unconfirmed: bool,
         #[clap(flatten)]
         scan_options: ScanOptions,
+        #[clap(flatten)]
+        esplora_args: EsploraArgs,
     },
 }
+impl EsploraCommands {
+    fn esplora_args(&self) -> EsploraArgs {
+        match self {
+            EsploraCommands::Scan { esplora_args, .. } => esplora_args.clone(),
+            EsploraCommands::Sync { esplora_args, .. } => esplora_args.clone(),
+        }
+    }
+}
+
+#[derive(clap::Args, Debug, Clone)]
+pub struct EsploraArgs {
+    /// The esplora url endpoint to connect to e.g. `<https://blockstream.info/api>`
+    /// If not provided it'll be set to a default for the network provided
+    esplora_url: Option<String>,
+}
+
+impl EsploraArgs {
+    pub fn client(&self, network: Network) -> anyhow::Result<esplora_client::BlockingClient> {
+        let esplora_url = self.esplora_url.as_deref().unwrap_or(match network {
+            Network::Bitcoin => "https://blockstream.info/api",
+            Network::Testnet => "https://blockstream.info/testnet/api",
+            Network::Regtest => "http://localhost:3002",
+            Network::Signet => "https://mempool.space/signet/api",
+            _ => panic!("unsupported network"),
+        });
+
+        let client = esplora_client::Builder::new(esplora_url).build_blocking()?;
+        Ok(client)
+    }
+}
 
 #[derive(Parser, Debug, Clone, PartialEq)]
 pub struct ScanOptions {
@@ -66,7 +100,7 @@ pub struct ScanOptions {
 
 fn main() -> anyhow::Result<()> {
     let (args, keymap, index, db, init_changeset) =
-        example_cli::init::<EsploraCommands, ChangeSet>(DB_MAGIC, DB_PATH)?;
+        example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
 
     let (init_chain_changeset, init_indexed_tx_graph_changeset) = init_changeset;
 
@@ -84,16 +118,6 @@ fn main() -> anyhow::Result<()> {
         chain
     });
 
-    let esplora_url = match args.network {
-        Network::Bitcoin => "https://blockstream.info/api",
-        Network::Testnet => "https://blockstream.info/testnet/api",
-        Network::Regtest => "http://localhost:3002",
-        Network::Signet => "https://mempool.space/signet/api",
-        _ => panic!("unsupported network"),
-    };
-
-    let client = esplora_client::Builder::new(esplora_url).build_blocking()?;
-
     let esplora_cmd = match &args.command {
         // These are commands that are handled by this example (sync, scan).
         example_cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd,
@@ -105,7 +129,8 @@ fn main() -> anyhow::Result<()> {
                 &chain,
                 &keymap,
                 args.network,
-                |tx| {
+                |esplora_args, tx| {
+                    let client = esplora_args.client(args.network)?;
                     client
                         .broadcast(tx)
                         .map(|_| ())
@@ -119,6 +144,7 @@ fn main() -> anyhow::Result<()> {
         }
     };
 
+    let client = esplora_cmd.esplora_args().client(args.network)?;
     // Prepare the `IndexedTxGraph` update based on whether we are scanning or syncing.
     // Scanning: We are iterating through spks of all keychains and scanning for transactions for
     //   each spk. We start with the lowest derivation index spk and stop scanning after `stop_gap`
@@ -131,6 +157,7 @@ fn main() -> anyhow::Result<()> {
         EsploraCommands::Scan {
             stop_gap,
             scan_options,
+            ..
         } => {
             let keychain_spks = graph
                 .lock()
@@ -184,6 +211,7 @@ fn main() -> anyhow::Result<()> {
             mut utxos,
             mut unconfirmed,
             scan_options,
+            ..
         } => {
             if !(*all_spks || unused_spks || utxos || unconfirmed) {
                 // If nothing is specifically selected, we select everything (except all spks).