diff --git a/side-node/src/bitcoin/clients/esplora.rs b/side-node/src/bitcoin/clients/esplora.rs index 3bb329d..8b8ae82 100644 --- a/side-node/src/bitcoin/clients/esplora.rs +++ b/side-node/src/bitcoin/clients/esplora.rs @@ -4,7 +4,9 @@ use bdk::keys::bip39::Mnemonic; use bdk_esplora::{esplora_client, EsploraAsyncExt}; use bdk_wallet::{ bitcoin::{Address, Amount, Network, Script}, + chain::ConfirmationTimeHeightAnchor, keys::{DerivableKey, ExtendedKey}, + wallet::AddressInfo, KeychainKind, SignOptions, Wallet, }; @@ -23,55 +25,59 @@ const PARALLEL_REQUESTS: usize = 5; /// Also, it very nadily works with the mutinynet.com esplora server, which is configured /// with 30 second block times. pub async fn run() -> Result<(), anyhow::Error> { - let dave_dir = utils::home(&"dave".to_string()); + let (mut wallet, mut db) = create_wallet("dave")?; - let mnemonic_path = crate::utils::side_paths(dave_dir).1; // TODO: this tuple stinks - let mnemonic_words = fs::read_to_string(mnemonic_path).expect("couldn't read bitcoin key file"); - - println!("Creating wallet from mnemonic: {mnemonic_words}"); - let mnemonic = Mnemonic::parse(mnemonic_words).unwrap(); - - // Generate the extended key - let xkey: ExtendedKey = mnemonic - .into_extended_key() - .expect("couldn't turn mnemonic into xkey"); - - let xprv = xkey - .into_xprv(Network::Signet) - .expect("problem converting xkey to xprv") - .to_string(); - - println!("Setting up esplora database"); - - let db_path = "/tmp/bdk-esplora-async-example.sqlite"; - let conn = Connection::open(db_path)?; - let mut db = Store::new(conn)?; - let external_descriptor = format!("wpkh({xprv}/84'/1'/0'/0/*)"); - let internal_descriptor = format!("wpkh({xprv}/84'/1'/0'/1/*)"); - let changeset = db.read().expect("couldn't read esplora database"); - - let mut wallet = Wallet::new_or_load( - &external_descriptor, - &internal_descriptor, - changeset, - Network::Signet, - ) - .expect("problem setting up wallet"); - - let address = wallet.next_unused_address(KeychainKind::External); - if let Some(changeset) = wallet.take_staged() { - db.write(&changeset)?; - } - println!("Generated Address: {}", address); + let _next_address = next_unused_address(&mut wallet, &mut db)?; let balance = wallet.balance(); println!("Wallet balance before syncing: {} sats", balance.total()); + let client = sync(&mut wallet, &mut db).await?; + + let balance = wallet.balance(); + println!("Wallet balance after syncing: {} sats", balance.total()); + + if balance.total() < SEND_AMOUNT { + println!( + "Please send at least {} sats to the receiving address", + SEND_AMOUNT + ); + std::process::exit(0); + } + + let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")? + .require_network(Network::Signet)?; + + let tx = build_send_tx(wallet, faucet_address)?; + client.broadcast(&tx).await?; + println!("Tx broadcasted! Txid: {}", tx.compute_txid()); + + Ok(()) +} + +fn build_send_tx( + mut wallet: Wallet, + faucet_address: Address, +) -> Result { + let mut tx_builder = wallet.build_tx(); + tx_builder + .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) + .enable_rbf(); + let mut psbt = tx_builder.finish()?; + let finalized = wallet.sign(&mut psbt, SignOptions::default())?; + assert!(finalized); + let tx = psbt.extract_tx()?; + Ok(tx) +} + +async fn sync( + wallet: &mut Wallet, + db: &mut Store, +) -> Result { print!("Syncing..."); let client = esplora_client::Builder::new("https://mutinynet.com/api") .build_async() .expect("couldn't build esplora client"); - fn generate_inspect(kind: KeychainKind) -> impl FnMut(u32, &Script) + Send + Sync + 'static { let mut once = Some(()); let mut stdout = std::io::stdout(); @@ -103,45 +109,68 @@ pub async fn run() -> Result<(), anyhow::Error> { KeychainKind::Internal, generate_inspect(KeychainKind::Internal), ); - let mut update = client .full_scan(request, STOP_GAP, PARALLEL_REQUESTS) .await?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); let _ = update.graph_update.update_last_seen_unconfirmed(now); - wallet.apply_update(update)?; if let Some(changeset) = wallet.take_staged() { db.write(&changeset)?; } println!(); - - let balance = wallet.balance(); - println!("Wallet balance after syncing: {} sats", balance.total()); - - if balance.total() < SEND_AMOUNT { - println!( - "Please send at least {} sats to the receiving address", - SEND_AMOUNT - ); - std::process::exit(0); - } - - let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")? - .require_network(Network::Signet)?; - - let mut tx_builder = wallet.build_tx(); - tx_builder - .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) - .enable_rbf(); - - let mut psbt = tx_builder.finish()?; - let finalized = wallet.sign(&mut psbt, SignOptions::default())?; - assert!(finalized); - - let tx = psbt.extract_tx()?; - client.broadcast(&tx).await?; - println!("Tx broadcasted! Txid: {}", tx.compute_txid()); - - Ok(()) + Ok(client) +} + +fn next_unused_address( + wallet: &mut Wallet, + db: &mut Store, +) -> Result { + let address = wallet.next_unused_address(KeychainKind::External); + if let Some(changeset) = wallet.take_staged() { + db.write(&changeset)?; + } + println!("Generated Address: {}", address); + Ok(address) +} + +fn create_wallet( + name: &str, +) -> anyhow::Result<(Wallet, Store)> { + let keys_dir = utils::home(name); + + let mnemonic_path = crate::utils::side_paths(keys_dir).1; // TODO: this tuple stinks + let mnemonic_words = fs::read_to_string(mnemonic_path).expect("couldn't read bitcoin key file"); + + println!("Creating wallet from mnemonic: {mnemonic_words}"); + let mnemonic = Mnemonic::parse(mnemonic_words).unwrap(); + + // Generate the extended key + let xkey: ExtendedKey = mnemonic + .into_extended_key() + .expect("couldn't turn mnemonic into xkey"); + + let xprv = xkey + .into_xprv(Network::Signet) + .expect("problem converting xkey to xprv") + .to_string(); + + println!("Setting up esplora database for {name}"); + + let db_path = format!("/tmp/{name}-bdk-esplora-async-example.sqlite"); + let conn = Connection::open(db_path)?; + let mut db = Store::new(conn)?; + let external_descriptor = format!("wpkh({xprv}/84'/1'/0'/0/*)"); + let internal_descriptor = format!("wpkh({xprv}/84'/1'/0'/1/*)"); + let changeset = db.read().expect("couldn't read esplora database"); + + let wallet = Wallet::new_or_load( + &external_descriptor, + &internal_descriptor, + changeset, + Network::Signet, + ) + .expect("problem setting up wallet"); + + Ok((wallet, db)) } diff --git a/side-node/src/utils.rs b/side-node/src/utils.rs index 9aeec88..6ed33b8 100644 --- a/side-node/src/utils.rs +++ b/side-node/src/utils.rs @@ -21,7 +21,7 @@ pub(crate) fn side_paths(prefix: PathBuf) -> (PathBuf, PathBuf, PathBuf) { } /// Returns the path to the home directory for this host OS and the given node name -pub(crate) fn home(name: &String) -> std::path::PathBuf { +pub(crate) fn home(name: &str) -> std::path::PathBuf { let mut path = dirs::home_dir().unwrap(); path.push(".side"); path.push(name);