diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index d2d5e7ca..edbcbd85 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -21,7 +21,7 @@ impl TxLock { pub async fn new(wallet: &Wallet, amount: Amount, A: PublicKey, B: PublicKey) -> Result { let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0); let address = lock_output_descriptor - .address(wallet.get_network().await) + .address(wallet.get_network()) .expect("can derive address from descriptor"); let psbt = wallet.send_to_address(address, amount).await?; diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index e4ba57d4..0bda71cb 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -5,11 +5,12 @@ use ::bitcoin::util::psbt::PartiallySignedTransaction; use ::bitcoin::Txid; use anyhow::{bail, Context, Result}; use bdk::blockchain::{noop_progress, Blockchain, ElectrumBlockchain}; +use bdk::database::BatchDatabase; use bdk::descriptor::Segwitv0; use bdk::electrum_client::{ElectrumApi, GetHistoryRes}; use bdk::keys::DerivableKey; use bdk::{FeeRate, KeychainKind}; -use bitcoin::Script; +use bitcoin::{Network, Script}; use reqwest::Url; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; @@ -21,10 +22,11 @@ use tokio::sync::{watch, Mutex}; const SLED_TREE_NAME: &str = "default_tree"; -pub struct Wallet { - client: Arc>, - wallet: Arc>>, +pub struct Wallet { + client: Arc>, + wallet: Arc>>, finality_confirmations: u32, + network: Network, } impl Wallet { @@ -39,7 +41,7 @@ impl Wallet { let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?; - let bdk_wallet = bdk::Wallet::new( + let wallet = bdk::Wallet::new( bdk::template::BIP84(key.clone(), KeychainKind::External), Some(bdk::template::BIP84(key, KeychainKind::Internal)), env_config.bitcoin_network, @@ -50,108 +52,19 @@ impl Wallet { let electrum = bdk::electrum_client::Client::new(electrum_rpc_url.as_str()) .context("Failed to initialize Electrum RPC client")?; + let network = wallet.network(); + Ok(Self { - wallet: Arc::new(Mutex::new(bdk_wallet)), client: Arc::new(Mutex::new(Client::new( electrum, env_config.bitcoin_sync_interval(), )?)), + wallet: Arc::new(Mutex::new(wallet)), finality_confirmations: env_config.bitcoin_finality_confirmations, + network, }) } - pub async fn balance(&self) -> Result { - let balance = self - .wallet - .lock() - .await - .get_balance() - .context("Failed to calculate Bitcoin balance")?; - - Ok(Amount::from_sat(balance)) - } - - pub async fn new_address(&self) -> Result
{ - let address = self - .wallet - .lock() - .await - .get_new_address() - .context("Failed to get new Bitcoin address")?; - - Ok(address) - } - - pub async fn get_tx(&self, txid: Txid) -> Result> { - let tx = self.wallet.lock().await.client().get_tx(&txid)?; - - Ok(tx) - } - - pub async fn transaction_fee(&self, txid: Txid) -> Result { - let fees = self - .wallet - .lock() - .await - .list_transactions(true)? - .iter() - .find(|tx| tx.txid == txid) - .context("Could not find tx in bdk wallet when trying to determine fees")? - .fees; - - Ok(Amount::from_sat(fees)) - } - - pub async fn sync(&self) -> Result<()> { - self.wallet - .lock() - .await - .sync(noop_progress(), None) - .context("Failed to sync balance of Bitcoin wallet")?; - - Ok(()) - } - - pub async fn send_to_address( - &self, - address: Address, - amount: Amount, - ) -> Result { - let wallet = self.wallet.lock().await; - - let mut tx_builder = wallet.build_tx(); - tx_builder.add_recipient(address.script_pubkey(), amount.as_sat()); - tx_builder.fee_rate(self.select_feerate()); - let (psbt, _details) = tx_builder.finish()?; - - Ok(psbt) - } - - /// Calculates the maximum "giveable" amount of this wallet. - /// - /// We define this as the maximum amount we can pay to a single output, - /// already accounting for the fees we need to spend to get the - /// transaction confirmed. - pub async fn max_giveable(&self, locking_script_size: usize) -> Result { - let wallet = self.wallet.lock().await; - - let mut tx_builder = wallet.build_tx(); - - let dummy_script = Script::from(vec![0u8; locking_script_size]); - tx_builder.set_single_recipient(dummy_script); - tx_builder.drain_wallet(); - tx_builder.fee_rate(self.select_feerate()); - let (_, details) = tx_builder.finish().context("Failed to build transaction")?; - - let max_giveable = details.sent - details.fees; - - Ok(Amount::from_sat(max_giveable)) - } - - pub async fn get_network(&self) -> bitcoin::Network { - self.wallet.lock().await.network() - } - /// Broadcast the given transaction to the network and emit a log statement /// if done so successfully. /// @@ -260,13 +173,6 @@ impl Wallet { sub } - - /// Selects an appropriate [`FeeRate`] to be used for getting transactions - /// confirmed within a reasonable amount of time. - fn select_feerate(&self) -> FeeRate { - // TODO: This should obviously not be a const :) - FeeRate::from_sat_per_vb(5.0) - } } /// Represents a subscription to the status of a given transaction. @@ -329,6 +235,119 @@ impl Subscription { } } +impl Wallet +where + D: BatchDatabase, +{ + pub async fn balance(&self) -> Result { + let balance = self + .wallet + .lock() + .await + .get_balance() + .context("Failed to calculate Bitcoin balance")?; + + Ok(Amount::from_sat(balance)) + } + + pub async fn new_address(&self) -> Result
{ + let address = self + .wallet + .lock() + .await + .get_new_address() + .context("Failed to get new Bitcoin address")?; + + Ok(address) + } + + pub async fn transaction_fee(&self, txid: Txid) -> Result { + let fees = self + .wallet + .lock() + .await + .list_transactions(true)? + .iter() + .find(|tx| tx.txid == txid) + .context("Could not find tx in bdk wallet when trying to determine fees")? + .fees; + + Ok(Amount::from_sat(fees)) + } + + pub async fn send_to_address( + &self, + address: Address, + amount: Amount, + ) -> Result { + let wallet = self.wallet.lock().await; + + let mut tx_builder = wallet.build_tx(); + tx_builder.add_recipient(address.script_pubkey(), amount.as_sat()); + tx_builder.fee_rate(self.select_feerate()); + let (psbt, _details) = tx_builder.finish()?; + + Ok(psbt) + } + + /// Calculates the maximum "giveable" amount of this wallet. + /// + /// We define this as the maximum amount we can pay to a single output, + /// already accounting for the fees we need to spend to get the + /// transaction confirmed. + pub async fn max_giveable(&self, locking_script_size: usize) -> Result { + let wallet = self.wallet.lock().await; + + let mut tx_builder = wallet.build_tx(); + + let dummy_script = Script::from(vec![0u8; locking_script_size]); + tx_builder.set_single_recipient(dummy_script); + tx_builder.drain_wallet(); + tx_builder.fee_rate(self.select_feerate()); + let (_, details) = tx_builder.finish().context("Failed to build transaction")?; + + let max_giveable = details.sent - details.fees; + + Ok(Amount::from_sat(max_giveable)) + } +} + +impl Wallet +where + B: Blockchain, + D: BatchDatabase, +{ + pub async fn get_tx(&self, txid: Txid) -> Result> { + let tx = self.wallet.lock().await.client().get_tx(&txid)?; + + Ok(tx) + } + + pub async fn sync(&self) -> Result<()> { + self.wallet + .lock() + .await + .sync(noop_progress(), None) + .context("Failed to sync balance of Bitcoin wallet")?; + + Ok(()) + } +} + +impl Wallet { + // TODO: Get rid of this by changing bounds on bdk::Wallet + pub fn get_network(&self) -> bitcoin::Network { + self.network + } + + /// Selects an appropriate [`FeeRate`] to be used for getting transactions + /// confirmed within a reasonable amount of time. + fn select_feerate(&self) -> FeeRate { + // TODO: This should obviously not be a const :) + FeeRate::from_sat_per_vb(5.0) + } +} + /// Defines a watchable transaction. /// /// For a transaction to be watchable, we need to know two things: Its @@ -350,7 +369,7 @@ impl Watchable for (Txid, Script) { } } -struct Client { +pub struct Client { electrum: bdk::electrum_client::Client, latest_block: BlockHeight, last_ping: Instant,