Split bitcoin::Wallet functions into various impl blocks

This allows us to construct instances of bitcoin::Wallet for test
purposes that use a different blockchain and database implementation.

We also parameterize the electrum-client to make it possible to
construct a bitcoin::Wallet for tests that doesn't have one. This
is necessary because the client validates the connection as it is
constructed and we don't want to provide an Electrum backend for
unit tests.
This commit is contained in:
Thomas Eizinger 2021-03-24 18:16:58 +11:00
parent b9d8cbeaa2
commit 8576894c10
No known key found for this signature in database
GPG Key ID: 651AC83A6C6C8B96
2 changed files with 126 additions and 107 deletions

View File

@ -21,7 +21,7 @@ impl TxLock {
pub async fn new(wallet: &Wallet, amount: Amount, A: PublicKey, B: PublicKey) -> Result<Self> {
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?;

View File

@ -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<Mutex<Client>>,
wallet: Arc<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::sled::Tree>>>,
pub struct Wallet<B = ElectrumBlockchain, D = bdk::sled::Tree, C = Client> {
client: Arc<Mutex<C>>,
wallet: Arc<Mutex<bdk::Wallet<B, D>>>,
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<Amount> {
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<Address> {
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<Option<Transaction>> {
let tx = self.wallet.lock().await.client().get_tx(&txid)?;
Ok(tx)
}
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
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<PartiallySignedTransaction> {
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<Amount> {
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<B, D, C> Wallet<B, D, C>
where
D: BatchDatabase,
{
pub async fn balance(&self) -> Result<Amount> {
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<Address> {
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<Amount> {
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<PartiallySignedTransaction> {
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<Amount> {
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<B, D, C> Wallet<B, D, C>
where
B: Blockchain,
D: BatchDatabase,
{
pub async fn get_tx(&self, txid: Txid) -> Result<Option<Transaction>> {
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<B, D, C> Wallet<B, D, C> {
// 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,