2021-03-04 00:28:58 +00:00
|
|
|
use crate::bitcoin::timelocks::BlockHeight;
|
|
|
|
use crate::bitcoin::{Address, Amount, Transaction};
|
2021-03-17 04:01:08 +00:00
|
|
|
use crate::env;
|
2021-03-04 00:28:58 +00:00
|
|
|
use ::bitcoin::util::psbt::PartiallySignedTransaction;
|
|
|
|
use ::bitcoin::Txid;
|
2021-03-23 03:57:27 +00:00
|
|
|
use anyhow::{bail, Context, Result};
|
2021-03-04 00:28:58 +00:00
|
|
|
use bdk::blockchain::{noop_progress, Blockchain, ElectrumBlockchain};
|
2021-03-24 07:16:58 +00:00
|
|
|
use bdk::database::BatchDatabase;
|
2021-03-04 00:28:58 +00:00
|
|
|
use bdk::descriptor::Segwitv0;
|
2021-03-23 03:57:27 +00:00
|
|
|
use bdk::electrum_client::{ElectrumApi, GetHistoryRes};
|
2021-03-04 00:28:58 +00:00
|
|
|
use bdk::keys::DerivableKey;
|
2021-04-01 05:46:39 +00:00
|
|
|
use bdk::wallet::AddressIndex;
|
2021-05-13 06:42:14 +00:00
|
|
|
use bdk::{FeeRate, KeychainKind, SignOptions};
|
2021-03-24 07:16:58 +00:00
|
|
|
use bitcoin::{Network, Script};
|
2021-03-05 06:06:17 +00:00
|
|
|
use reqwest::Url;
|
2021-05-07 04:44:15 +00:00
|
|
|
use rust_decimal::prelude::*;
|
|
|
|
use rust_decimal::Decimal;
|
|
|
|
use rust_decimal_macros::dec;
|
2021-05-19 06:21:22 +00:00
|
|
|
use std::cmp::Ordering;
|
2021-03-23 02:49:57 +00:00
|
|
|
use std::collections::{BTreeMap, HashMap};
|
2021-03-11 07:16:00 +00:00
|
|
|
use std::convert::TryFrom;
|
|
|
|
use std::fmt;
|
2021-03-04 00:28:58 +00:00
|
|
|
use std::path::Path;
|
|
|
|
use std::sync::Arc;
|
2021-03-11 07:16:00 +00:00
|
|
|
use std::time::{Duration, Instant};
|
2021-03-23 02:49:57 +00:00
|
|
|
use tokio::sync::{watch, Mutex};
|
2021-02-01 23:39:34 +00:00
|
|
|
|
|
|
|
const SLED_TREE_NAME: &str = "default_tree";
|
2021-05-04 01:34:00 +00:00
|
|
|
|
|
|
|
/// Assuming we add a spread of 3% we don't want to pay more than 3% of the
|
|
|
|
/// amount for tx fees.
|
2021-05-07 04:44:15 +00:00
|
|
|
const MAX_RELATIVE_TX_FEE: Decimal = dec!(0.03);
|
|
|
|
const MAX_ABSOLUTE_TX_FEE: Decimal = dec!(100_000);
|
2021-05-12 04:36:06 +00:00
|
|
|
const DUST_AMOUNT: u64 = 546;
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-03-24 07:16:58 +00:00
|
|
|
pub struct Wallet<B = ElectrumBlockchain, D = bdk::sled::Tree, C = Client> {
|
|
|
|
client: Arc<Mutex<C>>,
|
|
|
|
wallet: Arc<Mutex<bdk::Wallet<B, D>>>,
|
2021-03-17 02:36:43 +00:00
|
|
|
finality_confirmations: u32,
|
2021-03-24 07:16:58 +00:00
|
|
|
network: Network,
|
2021-05-01 00:08:54 +00:00
|
|
|
target_block: usize,
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Wallet {
|
2021-02-01 23:39:34 +00:00
|
|
|
pub async fn new(
|
|
|
|
electrum_rpc_url: Url,
|
2021-02-09 06:23:13 +00:00
|
|
|
wallet_dir: &Path,
|
2021-03-02 06:10:29 +00:00
|
|
|
key: impl DerivableKey<Segwitv0> + Clone,
|
2021-03-17 04:01:08 +00:00
|
|
|
env_config: env::Config,
|
2021-05-01 00:08:54 +00:00
|
|
|
target_block: usize,
|
2021-02-01 23:39:34 +00:00
|
|
|
) -> Result<Self> {
|
2021-03-23 03:57:27 +00:00
|
|
|
let client = bdk::electrum_client::Client::new(electrum_rpc_url.as_str())
|
|
|
|
.context("Failed to initialize Electrum RPC client")?;
|
2021-02-01 23:39:34 +00:00
|
|
|
|
2021-02-09 06:23:13 +00:00
|
|
|
let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?;
|
2021-02-01 23:39:34 +00:00
|
|
|
|
2021-03-24 07:16:58 +00:00
|
|
|
let wallet = bdk::Wallet::new(
|
2021-04-19 00:14:14 +00:00
|
|
|
bdk::template::Bip84(key.clone(), KeychainKind::External),
|
|
|
|
Some(bdk::template::Bip84(key, KeychainKind::Internal)),
|
2021-03-17 03:55:42 +00:00
|
|
|
env_config.bitcoin_network,
|
2021-02-01 23:39:34 +00:00
|
|
|
db,
|
|
|
|
ElectrumBlockchain::from(client),
|
|
|
|
)?;
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-03-23 03:57:27 +00:00
|
|
|
let electrum = bdk::electrum_client::Client::new(electrum_rpc_url.as_str())
|
|
|
|
.context("Failed to initialize Electrum RPC client")?;
|
2021-03-11 07:16:00 +00:00
|
|
|
|
2021-03-24 07:16:58 +00:00
|
|
|
let network = wallet.network();
|
|
|
|
|
2021-01-05 03:08:36 +00:00
|
|
|
Ok(Self {
|
2021-03-17 04:01:08 +00:00
|
|
|
client: Arc::new(Mutex::new(Client::new(
|
|
|
|
electrum,
|
|
|
|
env_config.bitcoin_sync_interval(),
|
|
|
|
)?)),
|
2021-03-24 07:16:58 +00:00
|
|
|
wallet: Arc::new(Mutex::new(wallet)),
|
2021-03-17 03:55:42 +00:00
|
|
|
finality_confirmations: env_config.bitcoin_finality_confirmations,
|
2021-03-24 07:16:58 +00:00
|
|
|
network,
|
2021-05-01 00:08:54 +00:00
|
|
|
target_block,
|
2021-01-05 03:08:36 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-03-02 01:29:11 +00:00
|
|
|
/// Broadcast the given transaction to the network and emit a log statement
|
|
|
|
/// if done so successfully.
|
2021-03-16 08:11:14 +00:00
|
|
|
///
|
|
|
|
/// Returns the transaction ID and a future for when the transaction meets
|
|
|
|
/// the configured finality confirmations.
|
|
|
|
pub async fn broadcast(
|
|
|
|
&self,
|
|
|
|
transaction: Transaction,
|
|
|
|
kind: &str,
|
2021-03-23 02:49:57 +00:00
|
|
|
) -> Result<(Txid, Subscription)> {
|
2021-03-02 01:22:23 +00:00
|
|
|
let txid = transaction.txid();
|
|
|
|
|
2021-03-16 08:11:14 +00:00
|
|
|
// to watch for confirmations, watching a single output is enough
|
2021-03-23 02:49:57 +00:00
|
|
|
let subscription = self
|
|
|
|
.subscribe_to((txid, transaction.output[0].script_pubkey.clone()))
|
|
|
|
.await;
|
2021-03-16 08:11:14 +00:00
|
|
|
|
2021-03-11 07:16:00 +00:00
|
|
|
self.wallet
|
2021-03-02 01:22:23 +00:00
|
|
|
.lock()
|
|
|
|
.await
|
|
|
|
.broadcast(transaction)
|
2021-03-02 01:29:11 +00:00
|
|
|
.with_context(|| {
|
2021-03-04 06:25:05 +00:00
|
|
|
format!("Failed to broadcast Bitcoin {} transaction {}", kind, txid)
|
2021-03-02 01:29:11 +00:00
|
|
|
})?;
|
|
|
|
|
2021-05-05 03:49:11 +00:00
|
|
|
tracing::info!(%txid, %kind, "Published Bitcoin transaction");
|
2021-03-02 01:22:23 +00:00
|
|
|
|
2021-03-23 02:49:57 +00:00
|
|
|
Ok((txid, subscription))
|
2021-02-26 03:31:09 +00:00
|
|
|
}
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-03-02 01:22:23 +00:00
|
|
|
pub async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
|
|
|
self.get_tx(txid)
|
|
|
|
.await?
|
2021-03-23 03:57:27 +00:00
|
|
|
.with_context(|| format!("Could not get raw tx with id: {}", txid))
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
2021-03-16 08:11:14 +00:00
|
|
|
pub async fn status_of_script<T>(&self, tx: &T) -> Result<ScriptStatus>
|
|
|
|
where
|
|
|
|
T: Watchable,
|
|
|
|
{
|
|
|
|
self.client.lock().await.status_of_script(tx)
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
2021-03-23 02:49:57 +00:00
|
|
|
pub async fn subscribe_to(&self, tx: impl Watchable + Send + 'static) -> Subscription {
|
2021-03-16 08:11:14 +00:00
|
|
|
let txid = tx.id();
|
2021-03-23 02:49:57 +00:00
|
|
|
let script = tx.script();
|
2021-03-16 08:11:14 +00:00
|
|
|
|
2021-03-23 02:49:57 +00:00
|
|
|
let sub = self
|
|
|
|
.client
|
|
|
|
.lock()
|
|
|
|
.await
|
|
|
|
.subscriptions
|
|
|
|
.entry((txid, script.clone()))
|
|
|
|
.or_insert_with(|| {
|
|
|
|
let (sender, receiver) = watch::channel(ScriptStatus::Unseen);
|
|
|
|
let client = self.client.clone();
|
|
|
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
let mut last_status = None;
|
|
|
|
|
|
|
|
loop {
|
|
|
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
|
|
|
|
|
|
|
let new_status = match client.lock().await.status_of_script(&tx) {
|
|
|
|
Ok(new_status) => new_status,
|
2021-05-05 03:49:11 +00:00
|
|
|
Err(error) => {
|
2021-05-11 01:08:33 +00:00
|
|
|
tracing::warn!(%txid, "Failed to get status of script. Error {:#}", error);
|
2021-03-23 02:49:57 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if Some(new_status) != last_status {
|
2021-05-11 06:06:44 +00:00
|
|
|
tracing::debug!(%txid, status = %new_status, "Transaction");
|
2021-03-23 02:49:57 +00:00
|
|
|
}
|
2021-05-05 03:49:11 +00:00
|
|
|
|
2021-03-23 02:49:57 +00:00
|
|
|
last_status = Some(new_status);
|
|
|
|
|
|
|
|
let all_receivers_gone = sender.send(new_status).is_err();
|
|
|
|
|
|
|
|
if all_receivers_gone {
|
|
|
|
tracing::debug!(%txid, "All receivers gone, removing subscription");
|
|
|
|
client.lock().await.subscriptions.remove(&(txid, script));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
Subscription {
|
|
|
|
receiver,
|
|
|
|
finality_confirmations: self.finality_confirmations,
|
|
|
|
txid,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.clone();
|
2021-03-05 05:49:49 +00:00
|
|
|
|
2021-03-23 02:49:57 +00:00
|
|
|
sub
|
|
|
|
}
|
|
|
|
}
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-03-23 02:49:57 +00:00
|
|
|
/// Represents a subscription to the status of a given transaction.
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct Subscription {
|
|
|
|
receiver: watch::Receiver<ScriptStatus>,
|
|
|
|
finality_confirmations: u32,
|
|
|
|
txid: Txid,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Subscription {
|
|
|
|
pub async fn wait_until_final(&self) -> Result<()> {
|
2021-03-17 02:36:43 +00:00
|
|
|
let conf_target = self.finality_confirmations;
|
2021-03-23 02:49:57 +00:00
|
|
|
let txid = self.txid;
|
2021-03-05 05:07:13 +00:00
|
|
|
|
2021-05-11 01:08:33 +00:00
|
|
|
tracing::info!(%txid, required_confirmation=%conf_target, "Waiting for Bitcoin transaction finality");
|
2021-03-05 05:07:13 +00:00
|
|
|
|
2021-03-11 07:16:00 +00:00
|
|
|
let mut seen_confirmations = 0;
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-03-23 02:49:57 +00:00
|
|
|
self.wait_until(|status| match status {
|
2021-03-11 07:16:00 +00:00
|
|
|
ScriptStatus::Confirmed(inner) => {
|
|
|
|
let confirmations = inner.confirmations();
|
|
|
|
|
|
|
|
if confirmations > seen_confirmations {
|
2021-05-05 03:49:11 +00:00
|
|
|
tracing::info!(%txid,
|
|
|
|
seen_confirmations = %confirmations,
|
|
|
|
needed_confirmations = %conf_target,
|
2021-05-11 01:08:33 +00:00
|
|
|
"Waiting for Bitcoin transaction finality");
|
2021-03-11 07:16:00 +00:00
|
|
|
seen_confirmations = confirmations;
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
2021-03-11 07:16:00 +00:00
|
|
|
|
|
|
|
inner.meets_target(conf_target)
|
2021-05-05 03:49:11 +00:00
|
|
|
}
|
|
|
|
_ => false,
|
2021-03-11 07:16:00 +00:00
|
|
|
})
|
2021-05-05 03:49:11 +00:00
|
|
|
.await
|
2021-03-23 02:49:57 +00:00
|
|
|
}
|
2021-01-05 03:08:36 +00:00
|
|
|
|
2021-03-23 02:49:57 +00:00
|
|
|
pub async fn wait_until_seen(&self) -> Result<()> {
|
|
|
|
self.wait_until(ScriptStatus::has_been_seen).await
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
2021-03-02 01:22:23 +00:00
|
|
|
|
2021-03-23 02:49:57 +00:00
|
|
|
pub async fn wait_until_confirmed_with<T>(&self, target: T) -> Result<()>
|
|
|
|
where
|
|
|
|
u32: PartialOrd<T>,
|
|
|
|
T: Copy,
|
|
|
|
{
|
|
|
|
self.wait_until(|status| status.is_confirmed_with(target))
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn wait_until(&self, mut predicate: impl FnMut(&ScriptStatus) -> bool) -> Result<()> {
|
|
|
|
let mut receiver = self.receiver.clone();
|
|
|
|
|
|
|
|
while !predicate(&receiver.borrow()) {
|
|
|
|
receiver
|
|
|
|
.changed()
|
|
|
|
.await
|
|
|
|
.context("Failed while waiting for next status update")?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
2021-03-02 01:22:23 +00:00
|
|
|
}
|
2021-01-05 03:08:36 +00:00
|
|
|
}
|
|
|
|
|
2021-03-24 07:16:58 +00:00
|
|
|
impl<B, D, C> Wallet<B, D, C>
|
|
|
|
where
|
2021-05-01 00:08:54 +00:00
|
|
|
C: EstimateFeeRate,
|
2021-03-24 07:16:58 +00:00
|
|
|
D: BatchDatabase,
|
|
|
|
{
|
2021-05-13 06:42:14 +00:00
|
|
|
pub async fn sign_and_finalize(
|
|
|
|
&self,
|
|
|
|
mut psbt: PartiallySignedTransaction,
|
|
|
|
) -> Result<Transaction> {
|
|
|
|
let finalized = self
|
|
|
|
.wallet
|
|
|
|
.lock()
|
|
|
|
.await
|
|
|
|
.sign(&mut psbt, SignOptions::default())?;
|
2021-05-19 06:21:22 +00:00
|
|
|
|
|
|
|
if !finalized {
|
|
|
|
bail!("PSBT is not finalized")
|
|
|
|
}
|
|
|
|
|
2021-05-13 06:42:14 +00:00
|
|
|
let tx = psbt.extract_tx();
|
2021-05-19 06:21:22 +00:00
|
|
|
|
|
|
|
Ok(tx)
|
|
|
|
}
|
|
|
|
|
2021-03-24 07:16:58 +00:00
|
|
|
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
|
2021-04-01 05:46:39 +00:00
|
|
|
.get_address(AddressIndex::New)
|
2021-06-17 11:06:14 +00:00
|
|
|
.context("Failed to get new Bitcoin address")?
|
|
|
|
.address;
|
2021-03-24 07:16:58 +00:00
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2021-05-19 06:21:22 +00:00
|
|
|
/// Builds a partially signed transaction
|
|
|
|
///
|
|
|
|
/// Ensures that the address script is at output index `0`
|
|
|
|
/// for the partially signed transaction.
|
2021-03-24 07:16:58 +00:00
|
|
|
pub async fn send_to_address(
|
|
|
|
&self,
|
|
|
|
address: Address,
|
|
|
|
amount: Amount,
|
|
|
|
) -> Result<PartiallySignedTransaction> {
|
2021-05-25 01:36:24 +00:00
|
|
|
if self.network != address.network {
|
|
|
|
bail!("Cannot build PSBT because network of given address is {} but wallet is on network {}", address.network, self.network);
|
|
|
|
}
|
|
|
|
|
2021-03-24 07:16:58 +00:00
|
|
|
let wallet = self.wallet.lock().await;
|
2021-05-01 00:08:54 +00:00
|
|
|
let client = self.client.lock().await;
|
|
|
|
let fee_rate = client.estimate_feerate(self.target_block)?;
|
2021-05-19 06:21:22 +00:00
|
|
|
let script = address.script_pubkey();
|
2021-03-24 07:16:58 +00:00
|
|
|
|
|
|
|
let mut tx_builder = wallet.build_tx();
|
2021-05-19 06:21:22 +00:00
|
|
|
tx_builder.add_recipient(script.clone(), amount.as_sat());
|
2021-05-01 00:08:54 +00:00
|
|
|
tx_builder.fee_rate(fee_rate);
|
2021-03-24 07:16:58 +00:00
|
|
|
let (psbt, _details) = tx_builder.finish()?;
|
2021-05-19 06:21:22 +00:00
|
|
|
let mut psbt: PartiallySignedTransaction = psbt;
|
|
|
|
|
|
|
|
// When subscribing to transactions we depend on the relevant script being at
|
|
|
|
// output index 0, thus we ensure the relevant output to be at index `0`.
|
|
|
|
psbt.outputs.sort_by(|a, _| {
|
|
|
|
if a.witness_script.as_ref() == Some(&script) {
|
|
|
|
Ordering::Less
|
|
|
|
} else {
|
|
|
|
Ordering::Greater
|
|
|
|
}
|
|
|
|
});
|
|
|
|
psbt.global.unsigned_tx.output.sort_by(|a, _| {
|
|
|
|
if a.script_pubkey == script {
|
|
|
|
Ordering::Less
|
|
|
|
} else {
|
|
|
|
Ordering::Greater
|
|
|
|
}
|
|
|
|
});
|
2021-03-24 07:16:58 +00:00
|
|
|
|
|
|
|
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;
|
2021-05-12 04:36:06 +00:00
|
|
|
let balance = wallet.get_balance()?;
|
|
|
|
if balance < DUST_AMOUNT {
|
|
|
|
return Ok(Amount::ZERO);
|
|
|
|
}
|
2021-05-01 00:08:54 +00:00
|
|
|
let client = self.client.lock().await;
|
2021-05-12 04:36:06 +00:00
|
|
|
let min_relay_fee = client.min_relay_fee()?.as_sat();
|
|
|
|
|
|
|
|
if balance < min_relay_fee {
|
|
|
|
return Ok(Amount::ZERO);
|
|
|
|
}
|
|
|
|
|
2021-05-01 00:08:54 +00:00
|
|
|
let fee_rate = client.estimate_feerate(self.target_block)?;
|
2021-03-24 07:16:58 +00:00
|
|
|
|
|
|
|
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();
|
2021-05-01 00:08:54 +00:00
|
|
|
tx_builder.fee_rate(fee_rate);
|
2021-03-24 07:16:58 +00:00
|
|
|
|
2021-05-12 04:36:06 +00:00
|
|
|
let response = tx_builder.finish();
|
|
|
|
match response {
|
|
|
|
Ok((_, details)) => {
|
|
|
|
let max_giveable = details.sent - details.fees;
|
|
|
|
Ok(Amount::from_sat(max_giveable))
|
|
|
|
}
|
|
|
|
Err(bdk::Error::InsufficientFunds { .. }) => Ok(Amount::ZERO),
|
|
|
|
Err(e) => bail!("Failed to build transaction. {:#}", e),
|
|
|
|
}
|
2021-03-24 07:16:58 +00:00
|
|
|
}
|
2021-05-01 00:08:54 +00:00
|
|
|
|
2021-05-06 22:30:15 +00:00
|
|
|
/// Estimate total tx fee for a pre-defined target block based on the
|
2021-05-04 01:34:00 +00:00
|
|
|
/// transaction weight. The max fee cannot be more than MAX_PERCENTAGE_FEE
|
|
|
|
/// of amount
|
|
|
|
pub async fn estimate_fee(
|
|
|
|
&self,
|
|
|
|
weight: usize,
|
|
|
|
transfer_amount: bitcoin::Amount,
|
|
|
|
) -> Result<bitcoin::Amount> {
|
2021-05-01 00:08:54 +00:00
|
|
|
let client = self.client.lock().await;
|
|
|
|
let fee_rate = client.estimate_feerate(self.target_block)?;
|
|
|
|
|
|
|
|
let min_relay_fee = client.min_relay_fee()?;
|
|
|
|
tracing::debug!("Min relay fee: {}", min_relay_fee);
|
2021-05-04 01:34:00 +00:00
|
|
|
|
2021-05-07 04:44:15 +00:00
|
|
|
estimate_fee(weight, transfer_amount, fee_rate, min_relay_fee)
|
2021-05-04 01:34:00 +00:00
|
|
|
}
|
|
|
|
}
|
2021-05-03 01:48:21 +00:00
|
|
|
|
2021-05-04 01:34:00 +00:00
|
|
|
fn estimate_fee(
|
|
|
|
weight: usize,
|
|
|
|
transfer_amount: Amount,
|
|
|
|
fee_rate: FeeRate,
|
|
|
|
min_relay_fee: Amount,
|
2021-05-07 04:44:15 +00:00
|
|
|
) -> Result<Amount> {
|
|
|
|
if transfer_amount.as_sat() <= 546 {
|
|
|
|
bail!("Amounts needs to be greater than Bitcoin dust amount.")
|
|
|
|
}
|
|
|
|
let fee_rate_svb = fee_rate.as_sat_vb();
|
|
|
|
if fee_rate_svb <= 0.0 {
|
|
|
|
bail!("Fee rate needs to be > 0")
|
|
|
|
}
|
|
|
|
if fee_rate_svb > 100_000_000.0 || min_relay_fee.as_sat() > 100_000_000 {
|
|
|
|
bail!("A fee_rate or min_relay_fee of > 1BTC does not make sense")
|
|
|
|
}
|
|
|
|
|
|
|
|
let min_relay_fee = if min_relay_fee.as_sat() == 0 {
|
|
|
|
// if min_relay_fee is 0 we don't fail, we just set it to 1 satoshi;
|
|
|
|
Amount::ONE_SAT
|
|
|
|
} else {
|
|
|
|
min_relay_fee
|
|
|
|
};
|
|
|
|
|
|
|
|
let weight = Decimal::from(weight);
|
|
|
|
let weight_factor = dec!(4.0);
|
|
|
|
let fee_rate = Decimal::from_f32(fee_rate_svb).context("Could not parse fee_rate.")?;
|
|
|
|
|
|
|
|
let sats_per_vbyte = weight / weight_factor * fee_rate;
|
2021-05-04 01:34:00 +00:00
|
|
|
tracing::debug!(
|
|
|
|
"Estimated fee for weight: {} for fee_rate: {:?} is in total: {}",
|
|
|
|
weight,
|
|
|
|
fee_rate,
|
|
|
|
sats_per_vbyte
|
|
|
|
);
|
|
|
|
|
2021-05-07 04:44:15 +00:00
|
|
|
let transfer_amount = Decimal::from(transfer_amount.as_sat());
|
|
|
|
let max_allowed_fee = transfer_amount * MAX_RELATIVE_TX_FEE;
|
2021-05-04 01:34:00 +00:00
|
|
|
|
2021-05-07 04:44:15 +00:00
|
|
|
let min_relay_fee = Decimal::from(min_relay_fee.as_sat());
|
|
|
|
let recommended_fee = if sats_per_vbyte < min_relay_fee {
|
2021-05-04 01:34:00 +00:00
|
|
|
tracing::warn!(
|
|
|
|
"Estimated fee of {} is smaller than the min relay fee, defaulting to min relay fee {}",
|
|
|
|
sats_per_vbyte,
|
2021-05-07 04:44:15 +00:00
|
|
|
min_relay_fee
|
2021-05-04 01:34:00 +00:00
|
|
|
);
|
2021-05-07 04:44:15 +00:00
|
|
|
min_relay_fee.to_u64()
|
2021-05-04 01:34:00 +00:00
|
|
|
} else if sats_per_vbyte > max_allowed_fee && sats_per_vbyte > MAX_ABSOLUTE_TX_FEE {
|
|
|
|
tracing::warn!(
|
|
|
|
"Hard bound of transaction fees reached. Falling back to: {} sats",
|
|
|
|
MAX_ABSOLUTE_TX_FEE
|
|
|
|
);
|
2021-05-07 04:44:15 +00:00
|
|
|
MAX_ABSOLUTE_TX_FEE.to_u64()
|
2021-05-04 01:34:00 +00:00
|
|
|
} else if sats_per_vbyte > max_allowed_fee {
|
|
|
|
tracing::warn!(
|
|
|
|
"Relative bound of transaction fees reached. Falling back to: {} sats",
|
|
|
|
max_allowed_fee
|
|
|
|
);
|
2021-05-07 04:44:15 +00:00
|
|
|
max_allowed_fee.to_u64()
|
2021-05-04 01:34:00 +00:00
|
|
|
} else {
|
2021-05-07 04:44:15 +00:00
|
|
|
sats_per_vbyte.to_u64()
|
|
|
|
};
|
|
|
|
let amount = recommended_fee
|
|
|
|
.map(bitcoin::Amount::from_sat)
|
|
|
|
.context("Could not estimate tranasction fee.")?;
|
|
|
|
Ok(amount)
|
2021-03-24 07:16:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2021-05-01 00:08:54 +00:00
|
|
|
}
|
2021-03-24 07:16:58 +00:00
|
|
|
|
2021-05-01 00:08:54 +00:00
|
|
|
pub trait EstimateFeeRate {
|
|
|
|
fn estimate_feerate(&self, target_block: usize) -> Result<FeeRate>;
|
|
|
|
fn min_relay_fee(&self) -> Result<bitcoin::Amount>;
|
2021-03-24 07:16:58 +00:00
|
|
|
}
|
|
|
|
|
2021-03-24 07:30:55 +00:00
|
|
|
#[cfg(test)]
|
2021-05-20 00:08:18 +00:00
|
|
|
pub struct StaticFeeRate {
|
|
|
|
fee_rate: FeeRate,
|
|
|
|
min_relay_fee: bitcoin::Amount,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
impl EstimateFeeRate for StaticFeeRate {
|
|
|
|
fn estimate_feerate(&self, _target_block: usize) -> Result<FeeRate> {
|
|
|
|
Ok(self.fee_rate)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn min_relay_fee(&self) -> Result<bitcoin::Amount> {
|
|
|
|
Ok(self.min_relay_fee)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
impl Wallet<(), bdk::database::MemoryDatabase, StaticFeeRate> {
|
|
|
|
/// Creates a new, funded wallet with sane default fees.
|
|
|
|
///
|
|
|
|
/// Unless you are testing things related to fees, this is likely what you
|
|
|
|
/// want.
|
|
|
|
pub fn new_funded_default_fees(amount: u64) -> Self {
|
|
|
|
Self::new_funded(amount, 1.0, 1000)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Creates a new, funded wallet that doesn't pay any fees.
|
|
|
|
///
|
|
|
|
/// This will create invalid transactions but can be useful if you want full
|
|
|
|
/// control over the output amounts.
|
|
|
|
pub fn new_funded_zero_fees(amount: u64) -> Self {
|
|
|
|
Self::new_funded(amount, 0.0, 0)
|
|
|
|
}
|
|
|
|
|
2021-03-24 07:30:55 +00:00
|
|
|
/// Creates a new, funded wallet to be used within tests.
|
2021-05-20 00:08:18 +00:00
|
|
|
pub fn new_funded(amount: u64, sats_per_vb: f32, min_relay_fee_sats: u64) -> Self {
|
2021-03-24 07:30:55 +00:00
|
|
|
use bdk::database::MemoryDatabase;
|
|
|
|
use bdk::{LocalUtxo, TransactionDetails};
|
|
|
|
use bitcoin::OutPoint;
|
|
|
|
use testutils::testutils;
|
|
|
|
|
2021-05-19 06:21:22 +00:00
|
|
|
let descriptors = testutils!(@descriptors ("wpkh(tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m/*)"));
|
2021-03-24 07:30:55 +00:00
|
|
|
|
|
|
|
let mut database = MemoryDatabase::new();
|
|
|
|
bdk::populate_test_db!(
|
|
|
|
&mut database,
|
|
|
|
testutils! {
|
|
|
|
@tx ( (@external descriptors, 0) => amount ) (@confirmations 1)
|
|
|
|
},
|
|
|
|
Some(100)
|
|
|
|
);
|
|
|
|
|
|
|
|
let wallet =
|
|
|
|
bdk::Wallet::new_offline(&descriptors.0, None, Network::Regtest, database).unwrap();
|
|
|
|
|
|
|
|
Self {
|
2021-05-20 00:08:18 +00:00
|
|
|
client: Arc::new(Mutex::new(StaticFeeRate {
|
|
|
|
fee_rate: FeeRate::from_sat_per_vb(sats_per_vb),
|
|
|
|
min_relay_fee: bitcoin::Amount::from_sat(min_relay_fee_sats),
|
|
|
|
})),
|
2021-03-24 07:30:55 +00:00
|
|
|
wallet: Arc::new(Mutex::new(wallet)),
|
|
|
|
finality_confirmations: 1,
|
|
|
|
network: Network::Regtest,
|
2021-05-01 00:08:54 +00:00
|
|
|
target_block: 1,
|
2021-03-24 07:30:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-16 08:11:14 +00:00
|
|
|
/// Defines a watchable transaction.
|
|
|
|
///
|
|
|
|
/// For a transaction to be watchable, we need to know two things: Its
|
|
|
|
/// transaction ID and the specific output script that is going to change.
|
|
|
|
/// A transaction can obviously have multiple outputs but our protocol purposes,
|
|
|
|
/// we are usually interested in a specific one.
|
|
|
|
pub trait Watchable {
|
|
|
|
fn id(&self) -> Txid;
|
|
|
|
fn script(&self) -> Script;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Watchable for (Txid, Script) {
|
|
|
|
fn id(&self) -> Txid {
|
|
|
|
self.0
|
|
|
|
}
|
|
|
|
|
|
|
|
fn script(&self) -> Script {
|
|
|
|
self.1.clone()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-24 07:16:58 +00:00
|
|
|
pub struct Client {
|
2021-03-11 07:16:00 +00:00
|
|
|
electrum: bdk::electrum_client::Client,
|
2021-05-17 08:40:10 +00:00
|
|
|
latest_block_height: BlockHeight,
|
2021-05-17 09:10:11 +00:00
|
|
|
last_sync: Instant,
|
|
|
|
sync_interval: Duration,
|
2021-03-11 07:16:00 +00:00
|
|
|
script_history: BTreeMap<Script, Vec<GetHistoryRes>>,
|
2021-03-23 02:49:57 +00:00
|
|
|
subscriptions: HashMap<(Txid, Script), Subscription>,
|
2021-03-11 07:16:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Client {
|
|
|
|
fn new(electrum: bdk::electrum_client::Client, interval: Duration) -> Result<Self> {
|
2021-05-17 08:40:10 +00:00
|
|
|
// Initially fetch the latest block for storing the height.
|
|
|
|
// We do not act on this subscription after this call.
|
2021-03-23 03:57:27 +00:00
|
|
|
let latest_block = electrum
|
|
|
|
.block_headers_subscribe()
|
|
|
|
.context("Failed to subscribe to header notifications")?;
|
2021-03-11 07:16:00 +00:00
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
electrum,
|
2021-05-17 08:40:10 +00:00
|
|
|
latest_block_height: BlockHeight::try_from(latest_block)?,
|
2021-05-17 09:10:11 +00:00
|
|
|
last_sync: Instant::now(),
|
|
|
|
sync_interval: interval,
|
2021-03-11 07:16:00 +00:00
|
|
|
script_history: Default::default(),
|
2021-03-23 02:49:57 +00:00
|
|
|
subscriptions: Default::default(),
|
2021-03-11 07:16:00 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-05-17 08:40:10 +00:00
|
|
|
fn update_state(&mut self) -> Result<()> {
|
2021-05-17 09:10:11 +00:00
|
|
|
let now = Instant::now();
|
|
|
|
if now < self.last_sync + self.sync_interval {
|
2021-03-11 07:16:00 +00:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2021-05-17 09:10:11 +00:00
|
|
|
self.last_sync = now;
|
2021-05-17 08:40:10 +00:00
|
|
|
self.update_latest_block()?;
|
2021-03-11 07:16:00 +00:00
|
|
|
self.update_script_histories()?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-03-16 08:11:14 +00:00
|
|
|
fn status_of_script<T>(&mut self, tx: &T) -> Result<ScriptStatus>
|
|
|
|
where
|
|
|
|
T: Watchable,
|
|
|
|
{
|
|
|
|
let txid = tx.id();
|
|
|
|
let script = tx.script();
|
|
|
|
|
|
|
|
if !self.script_history.contains_key(&script) {
|
2021-03-11 07:16:00 +00:00
|
|
|
self.script_history.insert(script.clone(), vec![]);
|
|
|
|
}
|
|
|
|
|
2021-05-17 08:40:10 +00:00
|
|
|
self.update_state()?;
|
2021-03-11 07:16:00 +00:00
|
|
|
|
2021-03-16 08:11:14 +00:00
|
|
|
let history = self.script_history.entry(script).or_default();
|
2021-03-05 05:45:50 +00:00
|
|
|
|
2021-03-11 07:16:00 +00:00
|
|
|
let history_of_tx = history
|
|
|
|
.iter()
|
2021-03-16 08:11:14 +00:00
|
|
|
.filter(|entry| entry.tx_hash == txid)
|
2021-03-11 07:16:00 +00:00
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
match history_of_tx.as_slice() {
|
|
|
|
[] => Ok(ScriptStatus::Unseen),
|
|
|
|
[remaining @ .., last] => {
|
|
|
|
if !remaining.is_empty() {
|
2021-05-11 01:08:33 +00:00
|
|
|
tracing::warn!("Found more than a single history entry for script. This is highly unexpected and those history entries will be ignored")
|
2021-03-11 07:16:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if last.height <= 0 {
|
|
|
|
Ok(ScriptStatus::InMempool)
|
|
|
|
} else {
|
|
|
|
Ok(ScriptStatus::Confirmed(
|
|
|
|
Confirmed::from_inclusion_and_latest_block(
|
|
|
|
u32::try_from(last.height)?,
|
2021-05-17 08:40:10 +00:00
|
|
|
u32::from(self.latest_block_height),
|
2021-03-11 07:16:00 +00:00
|
|
|
),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-17 08:40:10 +00:00
|
|
|
fn update_latest_block(&mut self) -> Result<()> {
|
|
|
|
// Fetch the latest block for storing the height.
|
|
|
|
// We do not act on this subscription after this call, as we cannot rely on
|
|
|
|
// subscription push notifications because eventually the Electrum server will
|
|
|
|
// close the connection and subscriptions are not automatically renewed
|
|
|
|
// upon renewing the connection.
|
|
|
|
let latest_block = self
|
|
|
|
.electrum
|
|
|
|
.block_headers_subscribe()
|
|
|
|
.context("Failed to subscribe to header notifications")?;
|
|
|
|
let latest_block_height = BlockHeight::try_from(latest_block)?;
|
2021-03-11 07:16:00 +00:00
|
|
|
|
2021-05-17 08:40:10 +00:00
|
|
|
if latest_block_height > self.latest_block_height {
|
2021-03-11 07:16:00 +00:00
|
|
|
tracing::debug!(
|
2021-05-17 08:40:10 +00:00
|
|
|
block_height = u32::from(latest_block_height),
|
2021-05-11 01:08:33 +00:00
|
|
|
"Got notification for new block"
|
2021-03-11 07:16:00 +00:00
|
|
|
);
|
2021-05-17 08:40:10 +00:00
|
|
|
self.latest_block_height = latest_block_height;
|
2021-03-11 07:16:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn update_script_histories(&mut self) -> Result<()> {
|
|
|
|
let histories = self
|
|
|
|
.electrum
|
|
|
|
.batch_script_get_history(self.script_history.keys())
|
2021-03-23 03:57:27 +00:00
|
|
|
.context("Failed to get script histories")?;
|
2021-03-11 07:16:00 +00:00
|
|
|
|
|
|
|
if histories.len() != self.script_history.len() {
|
|
|
|
bail!(
|
|
|
|
"Expected {} history entries, received {}",
|
|
|
|
self.script_history.len(),
|
|
|
|
histories.len()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
let scripts = self.script_history.keys().cloned();
|
|
|
|
let histories = histories.into_iter();
|
|
|
|
|
|
|
|
self.script_history = scripts.zip(histories).collect::<BTreeMap<_, _>>();
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-01 00:08:54 +00:00
|
|
|
impl EstimateFeeRate for Client {
|
|
|
|
fn estimate_feerate(&self, target_block: usize) -> Result<FeeRate> {
|
|
|
|
// https://github.com/romanz/electrs/blob/f9cf5386d1b5de6769ee271df5eef324aa9491bc/src/rpc.rs#L213
|
|
|
|
// Returned estimated fees are per BTC/kb.
|
|
|
|
let fee_per_byte = self.electrum.estimate_fee(target_block)?;
|
|
|
|
// we do not expect fees being that high.
|
|
|
|
#[allow(clippy::cast_possible_truncation)]
|
|
|
|
Ok(FeeRate::from_btc_per_kvb(fee_per_byte as f32))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn min_relay_fee(&self) -> Result<bitcoin::Amount> {
|
|
|
|
// https://github.com/romanz/electrs/blob/f9cf5386d1b5de6769ee271df5eef324aa9491bc/src/rpc.rs#L219
|
|
|
|
// Returned fee is in BTC/kb
|
|
|
|
let relay_fee = bitcoin::Amount::from_btc(self.electrum.relay_fee()?)?;
|
|
|
|
Ok(relay_fee)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-11 07:16:00 +00:00
|
|
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
|
|
pub enum ScriptStatus {
|
|
|
|
Unseen,
|
|
|
|
InMempool,
|
|
|
|
Confirmed(Confirmed),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ScriptStatus {
|
|
|
|
pub fn from_confirmations(confirmations: u32) -> Self {
|
|
|
|
match confirmations {
|
|
|
|
0 => Self::InMempool,
|
|
|
|
confirmations => Self::Confirmed(Confirmed::new(confirmations - 1)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
|
|
pub struct Confirmed {
|
|
|
|
/// The depth of this transaction within the blockchain.
|
|
|
|
///
|
|
|
|
/// Will be zero if the transaction is included in the latest block.
|
|
|
|
depth: u32,
|
2021-02-16 00:48:46 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 07:16:00 +00:00
|
|
|
impl Confirmed {
|
|
|
|
pub fn new(depth: u32) -> Self {
|
|
|
|
Self { depth }
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Compute the depth of a transaction based on its inclusion height and the
|
|
|
|
/// latest known block.
|
|
|
|
///
|
|
|
|
/// Our information about the latest block might be outdated. To avoid an
|
|
|
|
/// overflow, we make sure the depth is 0 in case the inclusion height
|
|
|
|
/// exceeds our latest known block,
|
|
|
|
pub fn from_inclusion_and_latest_block(inclusion_height: u32, latest_block: u32) -> Self {
|
|
|
|
let depth = latest_block.saturating_sub(inclusion_height);
|
|
|
|
|
|
|
|
Self { depth }
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn confirmations(&self) -> u32 {
|
|
|
|
self.depth + 1
|
|
|
|
}
|
2021-03-05 05:45:50 +00:00
|
|
|
|
2021-03-11 07:16:00 +00:00
|
|
|
pub fn meets_target<T>(&self, target: T) -> bool
|
|
|
|
where
|
|
|
|
u32: PartialOrd<T>,
|
|
|
|
{
|
|
|
|
self.confirmations() >= target
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ScriptStatus {
|
|
|
|
/// Check if the script has any confirmations.
|
|
|
|
pub fn is_confirmed(&self) -> bool {
|
|
|
|
matches!(self, ScriptStatus::Confirmed(_))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check if the script has met the given confirmation target.
|
|
|
|
pub fn is_confirmed_with<T>(&self, target: T) -> bool
|
|
|
|
where
|
|
|
|
u32: PartialOrd<T>,
|
|
|
|
{
|
|
|
|
match self {
|
|
|
|
ScriptStatus::Confirmed(inner) => inner.meets_target(target),
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn has_been_seen(&self) -> bool {
|
|
|
|
matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for ScriptStatus {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
match self {
|
|
|
|
ScriptStatus::Unseen => write!(f, "unseen"),
|
|
|
|
ScriptStatus::InMempool => write!(f, "in mempool"),
|
|
|
|
ScriptStatus::Confirmed(inner) => {
|
|
|
|
write!(f, "confirmed with {} blocks", inner.confirmations())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-02-16 00:48:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2021-03-05 05:45:50 +00:00
|
|
|
use super::*;
|
2021-05-19 06:21:22 +00:00
|
|
|
use crate::bitcoin::{PublicKey, TxLock};
|
2021-05-06 23:37:10 +00:00
|
|
|
use proptest::prelude::*;
|
2021-02-16 00:48:46 +00:00
|
|
|
|
|
|
|
#[test]
|
2021-03-11 07:16:00 +00:00
|
|
|
fn given_depth_0_should_meet_confirmation_target_one() {
|
|
|
|
let script = ScriptStatus::Confirmed(Confirmed { depth: 0 });
|
|
|
|
|
|
|
|
let confirmed = script.is_confirmed_with(1);
|
|
|
|
|
|
|
|
assert!(confirmed)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn given_confirmations_1_should_meet_confirmation_target_one() {
|
|
|
|
let script = ScriptStatus::from_confirmations(1);
|
2021-03-05 05:45:50 +00:00
|
|
|
|
2021-03-11 07:16:00 +00:00
|
|
|
let confirmed = script.is_confirmed_with(1);
|
2021-03-05 05:45:50 +00:00
|
|
|
|
2021-03-11 07:16:00 +00:00
|
|
|
assert!(confirmed)
|
2021-02-16 00:48:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2021-03-11 07:16:00 +00:00
|
|
|
fn given_inclusion_after_lastest_known_block_at_least_depth_0() {
|
|
|
|
let included_in = 10;
|
|
|
|
let latest_block = 9;
|
2021-03-05 05:45:50 +00:00
|
|
|
|
2021-03-11 07:16:00 +00:00
|
|
|
let confirmed = Confirmed::from_inclusion_and_latest_block(included_in, latest_block);
|
2021-03-05 05:45:50 +00:00
|
|
|
|
2021-03-11 07:16:00 +00:00
|
|
|
assert_eq!(confirmed.depth, 0)
|
2021-02-16 00:48:46 +00:00
|
|
|
}
|
2021-05-04 01:34:00 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn given_one_BTC_and_100k_sats_per_vb_fees_should_not_hit_max() {
|
|
|
|
// 400 weight = 100 vbyte
|
|
|
|
let weight = 400;
|
|
|
|
let amount = bitcoin::Amount::from_sat(100_000_000);
|
|
|
|
|
|
|
|
let sat_per_vb = 100.0;
|
|
|
|
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
|
|
|
|
|
|
|
|
let relay_fee = bitcoin::Amount::ONE_SAT;
|
2021-05-07 04:44:15 +00:00
|
|
|
let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee).unwrap();
|
2021-05-04 01:34:00 +00:00
|
|
|
|
|
|
|
// weight / 4.0 * sat_per_vb
|
|
|
|
let should_fee = bitcoin::Amount::from_sat(10_000);
|
|
|
|
assert_eq!(is_fee, should_fee);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn given_1BTC_and_1_sat_per_vb_fees_and_100ksat_min_relay_fee_should_hit_min() {
|
|
|
|
// 400 weight = 100 vbyte
|
|
|
|
let weight = 400;
|
|
|
|
let amount = bitcoin::Amount::from_sat(100_000_000);
|
|
|
|
|
|
|
|
let sat_per_vb = 1.0;
|
|
|
|
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
|
|
|
|
|
|
|
|
let relay_fee = bitcoin::Amount::from_sat(100_000);
|
2021-05-07 04:44:15 +00:00
|
|
|
let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee).unwrap();
|
2021-05-04 01:34:00 +00:00
|
|
|
|
|
|
|
// weight / 4.0 * sat_per_vb would be smaller than relay fee hence we take min
|
|
|
|
// relay fee
|
|
|
|
let should_fee = bitcoin::Amount::from_sat(100_000);
|
|
|
|
assert_eq!(is_fee, should_fee);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn given_1mio_sat_and_1k_sats_per_vb_fees_should_hit_relative_max() {
|
|
|
|
// 400 weight = 100 vbyte
|
|
|
|
let weight = 400;
|
|
|
|
let amount = bitcoin::Amount::from_sat(1_000_000);
|
|
|
|
|
|
|
|
let sat_per_vb = 1_000.0;
|
|
|
|
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
|
|
|
|
|
|
|
|
let relay_fee = bitcoin::Amount::ONE_SAT;
|
2021-05-07 04:44:15 +00:00
|
|
|
let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee).unwrap();
|
2021-05-04 01:34:00 +00:00
|
|
|
|
2021-05-06 23:37:10 +00:00
|
|
|
// weight / 4.0 * sat_per_vb would be greater than 3% hence we take max
|
2021-05-04 01:34:00 +00:00
|
|
|
// relative fee.
|
|
|
|
let should_fee = bitcoin::Amount::from_sat(30_000);
|
|
|
|
assert_eq!(is_fee, should_fee);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn given_1BTC_and_4mio_sats_per_vb_fees_should_hit_total_max() {
|
|
|
|
// even if we send 1BTC we don't want to pay 0.3BTC in fees. This would be
|
|
|
|
// $1,650 at the moment.
|
|
|
|
let weight = 400;
|
|
|
|
let amount = bitcoin::Amount::from_sat(100_000_000);
|
|
|
|
|
|
|
|
let sat_per_vb = 4_000_000.0;
|
|
|
|
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
|
|
|
|
|
|
|
|
let relay_fee = bitcoin::Amount::ONE_SAT;
|
2021-05-07 04:44:15 +00:00
|
|
|
let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee).unwrap();
|
2021-05-04 01:34:00 +00:00
|
|
|
|
2021-05-06 23:37:10 +00:00
|
|
|
// weight / 4.0 * sat_per_vb would be greater than 3% hence we take total
|
2021-05-04 01:34:00 +00:00
|
|
|
// max allowed fee.
|
2021-05-07 04:44:15 +00:00
|
|
|
assert_eq!(is_fee.as_sat(), MAX_ABSOLUTE_TX_FEE.to_u64().unwrap());
|
2021-05-04 01:34:00 +00:00
|
|
|
}
|
2021-05-06 23:37:10 +00:00
|
|
|
|
|
|
|
proptest! {
|
|
|
|
#[test]
|
2021-05-07 04:44:15 +00:00
|
|
|
fn given_randon_amount_random_fee_and_random_relay_rate_but_fix_weight_does_not_error(
|
|
|
|
amount in 547u64..,
|
|
|
|
sat_per_vb in 1.0f32..100_000_000.0f32,
|
|
|
|
relay_fee in 0u64..100_000_000u64
|
2021-05-06 23:37:10 +00:00
|
|
|
) {
|
|
|
|
let weight = 400;
|
|
|
|
let amount = bitcoin::Amount::from_sat(amount);
|
|
|
|
|
|
|
|
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
|
|
|
|
|
|
|
|
let relay_fee = bitcoin::Amount::from_sat(relay_fee);
|
2021-05-07 04:44:15 +00:00
|
|
|
let _is_fee = estimate_fee(weight, amount, fee_rate, relay_fee).unwrap();
|
2021-05-06 23:37:10 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
proptest! {
|
|
|
|
#[test]
|
|
|
|
fn given_amount_in_range_fix_fee_fix_relay_rate_fix_weight_fee_always_smaller_max(
|
2021-05-07 04:44:15 +00:00
|
|
|
amount in 1u64..100_000_000,
|
2021-05-06 23:37:10 +00:00
|
|
|
) {
|
|
|
|
let weight = 400;
|
|
|
|
let amount = bitcoin::Amount::from_sat(amount);
|
|
|
|
|
|
|
|
let sat_per_vb = 100.0;
|
|
|
|
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
|
|
|
|
|
|
|
|
let relay_fee = bitcoin::Amount::ONE_SAT;
|
2021-05-07 04:44:15 +00:00
|
|
|
let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee).unwrap();
|
2021-05-06 23:37:10 +00:00
|
|
|
|
|
|
|
// weight / 4 * 1_000 is always lower than MAX_ABSOLUTE_TX_FEE
|
2021-05-07 04:44:15 +00:00
|
|
|
assert!(is_fee.as_sat() < MAX_ABSOLUTE_TX_FEE.to_u64().unwrap());
|
2021-05-06 23:37:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
proptest! {
|
|
|
|
#[test]
|
|
|
|
fn given_amount_high_fix_fee_fix_relay_rate_fix_weight_fee_always_max(
|
|
|
|
amount in 100_000_000u64..,
|
|
|
|
) {
|
|
|
|
let weight = 400;
|
|
|
|
let amount = bitcoin::Amount::from_sat(amount);
|
|
|
|
|
|
|
|
let sat_per_vb = 1_000.0;
|
|
|
|
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
|
|
|
|
|
|
|
|
let relay_fee = bitcoin::Amount::ONE_SAT;
|
2021-05-07 04:44:15 +00:00
|
|
|
let is_fee = estimate_fee(weight, amount, fee_rate, relay_fee).unwrap();
|
2021-05-06 23:37:10 +00:00
|
|
|
|
|
|
|
// weight / 4 * 1_000 is always higher than MAX_ABSOLUTE_TX_FEE
|
2021-05-07 04:44:15 +00:00
|
|
|
assert!(is_fee.as_sat() >= MAX_ABSOLUTE_TX_FEE.to_u64().unwrap());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
proptest! {
|
|
|
|
#[test]
|
|
|
|
fn given_fee_above_max_should_always_errors(
|
|
|
|
sat_per_vb in 100_000_000.0f32..,
|
|
|
|
) {
|
|
|
|
let weight = 400;
|
|
|
|
let amount = bitcoin::Amount::from_sat(547u64);
|
|
|
|
|
|
|
|
let fee_rate = FeeRate::from_sat_per_vb(sat_per_vb);
|
|
|
|
|
|
|
|
let relay_fee = bitcoin::Amount::from_sat(1);
|
|
|
|
assert!(estimate_fee(weight, amount, fee_rate, relay_fee).is_err());
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
proptest! {
|
|
|
|
#[test]
|
|
|
|
fn given_relay_fee_above_max_should_always_errors(
|
|
|
|
relay_fee in 100_000_000u64..
|
|
|
|
) {
|
|
|
|
let weight = 400;
|
|
|
|
let amount = bitcoin::Amount::from_sat(547u64);
|
|
|
|
|
|
|
|
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
|
|
|
|
|
|
|
let relay_fee = bitcoin::Amount::from_sat(relay_fee);
|
|
|
|
assert!(estimate_fee(weight, amount, fee_rate, relay_fee).is_err());
|
2021-05-06 23:37:10 +00:00
|
|
|
}
|
|
|
|
}
|
2021-05-12 04:36:06 +00:00
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn given_no_balance_returns_amount_0() {
|
2021-05-20 00:08:18 +00:00
|
|
|
let wallet = Wallet::new_funded(0, 1.0, 1);
|
2021-05-12 04:36:06 +00:00
|
|
|
let amount = wallet.max_giveable(TxLock::script_size()).await.unwrap();
|
|
|
|
|
|
|
|
assert_eq!(amount, Amount::ZERO);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn given_balance_below_min_relay_fee_returns_amount_0() {
|
2021-05-20 00:08:18 +00:00
|
|
|
let wallet = Wallet::new_funded(1000, 1.0, 1001);
|
2021-05-12 04:36:06 +00:00
|
|
|
let amount = wallet.max_giveable(TxLock::script_size()).await.unwrap();
|
|
|
|
|
|
|
|
assert_eq!(amount, Amount::ZERO);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn given_balance_above_relay_fee_returns_amount_greater_0() {
|
2021-05-20 00:08:18 +00:00
|
|
|
let wallet = Wallet::new_funded_default_fees(10_000);
|
2021-05-12 04:36:06 +00:00
|
|
|
let amount = wallet.max_giveable(TxLock::script_size()).await.unwrap();
|
|
|
|
|
|
|
|
assert!(amount.as_sat() > 0);
|
|
|
|
}
|
2021-05-19 06:21:22 +00:00
|
|
|
|
|
|
|
/// This test ensures that the relevant script output of the transaction
|
|
|
|
/// created out of the PSBT is at index 0. This is important because
|
|
|
|
/// subscriptions to the transaction are on index `0` when broadcasting the
|
|
|
|
/// transaction.
|
|
|
|
#[tokio::test]
|
|
|
|
async fn given_amounts_with_change_outputs_when_signing_tx_then_output_index_0_is_ensured_for_script(
|
|
|
|
) {
|
|
|
|
// This value is somewhat arbitrary but the indexation problem usually occurred
|
|
|
|
// on the first or second value (i.e. 547, 548) We keep the test
|
|
|
|
// iterations relatively low because these tests are expensive.
|
|
|
|
let above_dust = 547;
|
|
|
|
let balance = 2000;
|
|
|
|
|
2021-05-20 00:08:18 +00:00
|
|
|
// We don't care about fees in this test, thus use a zero fee rate
|
|
|
|
let wallet = Wallet::new_funded_zero_fees(balance);
|
2021-05-19 06:21:22 +00:00
|
|
|
|
|
|
|
// sorting is only relevant for amounts that have a change output
|
|
|
|
// if the change output is below dust it will be dropped by the BDK
|
|
|
|
for amount in above_dust..(balance - (above_dust - 1)) {
|
|
|
|
let (A, B) = (PublicKey::random(), PublicKey::random());
|
|
|
|
let txlock = TxLock::new(&wallet, bitcoin::Amount::from_sat(amount), A, B)
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
let txlock_output = txlock.script_pubkey();
|
|
|
|
|
|
|
|
let tx = wallet.sign_and_finalize(txlock.into()).await.unwrap();
|
|
|
|
let tx_output = tx.output[0].script_pubkey.clone();
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
tx_output, txlock_output,
|
|
|
|
"Output {:?} index mismatch for amount {} and balance {}",
|
|
|
|
tx.output, amount, balance
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2021-02-16 00:48:46 +00:00
|
|
|
}
|