use std::fs; use bdk::{ bitcoin::{psbt::PartiallySignedTransaction, secp256k1::PublicKey, Network, Transaction}, blockchain::EsploraBlockchain, database::MemoryDatabase, keys::{bip39::Mnemonic, DerivableKey, ExtendedKey}, template::Bip84, wallet::AddressInfo, KeychainKind, SignOptions, SyncOptions, Wallet, }; use crate::utils; /// A client that uses Esplora to interact with the Bitcoin network. pub struct BitcoinClient { pub(crate) blockchain: bdk::blockchain::EsploraBlockchain, name: String, pub(crate) wallet: Wallet, pub(crate) external_public_key: PublicKey, } impl BitcoinClient { pub(crate) fn sync(&self) -> anyhow::Result<()> { self.wallet.sync(&self.blockchain, SyncOptions::default())?; Ok(()) } pub(crate) fn broadcast(&self, tx: &Transaction) -> anyhow::Result<()> { tracing::info!( "broadcasting transaction, output will be at https://mutinynet.com/tx/{}", tx.txid() ); let _ = self.blockchain.broadcast(&tx); Ok(()) } /// Builds and signs a send transaction to send coins between addresses. /// /// Does NOT send it, you must call `broadcast` to do that. /// /// We could split the creation and signing easily if needed. pub(crate) fn build_and_sign_send_tx( &mut self, recipient: AddressInfo, amount: u64, ) -> Result { let mut tx_builder = self.wallet.build_tx(); tx_builder .add_recipient(recipient.script_pubkey(), amount) .enable_rbf(); let (mut psbt, _) = tx_builder.finish()?; let tx = self.sign(&mut psbt, true)?.extract_tx(); Ok(tx) } pub(crate) fn sign( &self, psbt: &mut PartiallySignedTransaction, finalize: bool, ) -> Result { tracing::info!("{} signing PSBT", self.name); let options = SignOptions { try_finalize: finalize, ..Default::default() }; let finalized = self.wallet.sign(psbt, options)?; // make sure the PSBT is finalized if we asked for it if finalize { assert!(finalized) } Ok(psbt.to_owned()) } /// Creates a Bitcoin descriptor wallet with the mnemonic in the given user directory. pub(crate) fn create(name: &str, network: Network) -> anyhow::Result { let keys_dir = utils::home(name); let mnemonic_path = crate::utils::side_paths(keys_dir).1; // TODO: this tuple stinks let words = fs::read_to_string(mnemonic_path).expect("couldn't read bitcoin key file"); tracing::info!("Creating {name}'s wallet from mnemonic: {words}"); let xkey: ExtendedKey = Mnemonic::parse(words) .expect("couldn't parse mnemonic words") .into_extended_key() .expect("couldn't turn mnemonic into extended key"); let xprv = xkey .into_xprv(Network::Signet) .expect("couldn't turn xkey into xprv"); let secp = bdk::bitcoin::secp256k1::Secp256k1::new(); let external_descriptor1 = Bip84(xprv.clone(), KeychainKind::External); let external_descriptor2 = Bip84(xprv, KeychainKind::External); let internal_descriptor = Some(Bip84(xprv, KeychainKind::Internal)); let wallet = Wallet::new( external_descriptor1, internal_descriptor, network, MemoryDatabase::default(), )?; let blockchain = EsploraBlockchain::new("https://mutinynet.com/api", 20); let external_public_key = external_descriptor2.0.private_key.public_key(&secp); let esplora = BitcoinClient { name: name.to_string(), wallet, blockchain, external_public_key, }; Ok(esplora) } }