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-03-04 00:28:58 +00:00
|
|
|
use bdk::{FeeRate, KeychainKind};
|
2021-03-24 07:16:58 +00:00
|
|
|
use bitcoin::{Network, Script};
|
2021-03-05 06:06:17 +00:00
|
|
|
use reqwest::Url;
|
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-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-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-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-03-02 06:10:29 +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-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-03-05 05:07:13 +00:00
|
|
|
tracing::info!(%txid, "Published Bitcoin {} transaction", kind);
|
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:53:40 +00:00
|
|
|
pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result<Transaction> {
|
2021-03-11 07:16:00 +00:00
|
|
|
let (signed_psbt, finalized) = self.wallet.lock().await.sign(psbt, None)?;
|
2021-03-02 01:53:40 +00:00
|
|
|
|
2021-02-01 23:39:34 +00:00
|
|
|
if !finalized {
|
2021-03-02 01:53:40 +00:00
|
|
|
bail!("PSBT is not finalized")
|
2021-02-01 23:39:34 +00:00
|
|
|
}
|
2021-03-02 01:53:40 +00:00
|
|
|
|
2021-02-01 23:39:34 +00:00
|
|
|
let tx = signed_psbt.extract_tx();
|
2021-03-02 01:53:40 +00:00
|
|
|
|
2021-01-05 03:08:36 +00:00
|
|
|
Ok(tx)
|
|
|
|
}
|
2021-02-26 06:01: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,
|
|
|
|
Err(e) => {
|
|
|
|
tracing::warn!(%txid, "Failed to get status of script: {:#}", e);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if Some(new_status) != last_status {
|
|
|
|
tracing::debug!(%txid, "Transaction is {}", new_status);
|
|
|
|
}
|
|
|
|
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-03-23 02:49:57 +00:00
|
|
|
tracing::info!(%txid, "Waiting for {} confirmation{} of Bitcoin transaction", conf_target, if conf_target > 1 { "s" } else { "" });
|
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-03-23 02:49:57 +00:00
|
|
|
tracing::info!(%txid, "Bitcoin tx has {} out of {} confirmation{}", confirmations, conf_target, if conf_target > 1 { "s" } else { "" });
|
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)
|
|
|
|
},
|
|
|
|
_ => false
|
|
|
|
})
|
2021-03-23 02:49:57 +00:00
|
|
|
.await
|
|
|
|
}
|
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
|
|
|
|
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
|
2021-04-01 05:46:39 +00:00
|
|
|
.get_address(AddressIndex::New)
|
2021-03-24 07:16:58 +00:00
|
|
|
.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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-24 07:30:55 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
impl Wallet<(), bdk::database::MemoryDatabase, ()> {
|
|
|
|
/// Creates a new, funded wallet to be used within tests.
|
|
|
|
pub fn new_funded(amount: u64) -> Self {
|
|
|
|
use bdk::database::MemoryDatabase;
|
|
|
|
use bdk::{LocalUtxo, TransactionDetails};
|
|
|
|
use bitcoin::OutPoint;
|
|
|
|
use std::str::FromStr;
|
|
|
|
use testutils::testutils;
|
|
|
|
|
|
|
|
let descriptors = testutils!(@descriptors ("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"));
|
|
|
|
|
|
|
|
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 {
|
|
|
|
client: Arc::new(Mutex::new(())),
|
|
|
|
wallet: Arc::new(Mutex::new(wallet)),
|
|
|
|
finality_confirmations: 1,
|
|
|
|
network: Network::Regtest,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
latest_block: BlockHeight,
|
|
|
|
last_ping: Instant,
|
|
|
|
interval: Duration,
|
|
|
|
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-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,
|
|
|
|
latest_block: BlockHeight::try_from(latest_block)?,
|
|
|
|
last_ping: Instant::now(),
|
|
|
|
interval,
|
|
|
|
script_history: Default::default(),
|
2021-03-23 02:49:57 +00:00
|
|
|
subscriptions: Default::default(),
|
2021-03-11 07:16:00 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Ping the electrum server unless we already did within the set interval.
|
|
|
|
///
|
|
|
|
/// Returns a boolean indicating whether we actually pinged the server.
|
|
|
|
fn ping(&mut self) -> bool {
|
|
|
|
if self.last_ping.elapsed() <= self.interval {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
match self.electrum.ping() {
|
|
|
|
Ok(()) => {
|
|
|
|
self.last_ping = Instant::now();
|
|
|
|
|
|
|
|
true
|
|
|
|
}
|
|
|
|
Err(error) => {
|
|
|
|
tracing::debug!(?error, "Failed to ping electrum server");
|
|
|
|
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn drain_notifications(&mut self) -> Result<()> {
|
|
|
|
let pinged = self.ping();
|
|
|
|
|
|
|
|
if !pinged {
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
|
|
|
self.drain_blockheight_notifications()?;
|
|
|
|
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![]);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.drain_notifications()?;
|
|
|
|
|
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() {
|
|
|
|
tracing::warn!("Found more than a single history entry for script. This is highly unexpected and those history entries will be ignored.")
|
|
|
|
}
|
|
|
|
|
|
|
|
if last.height <= 0 {
|
|
|
|
Ok(ScriptStatus::InMempool)
|
|
|
|
} else {
|
|
|
|
Ok(ScriptStatus::Confirmed(
|
|
|
|
Confirmed::from_inclusion_and_latest_block(
|
|
|
|
u32::try_from(last.height)?,
|
|
|
|
u32::from(self.latest_block),
|
|
|
|
),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn drain_blockheight_notifications(&mut self) -> Result<()> {
|
|
|
|
let latest_block = std::iter::from_fn(|| self.electrum.block_headers_pop().transpose())
|
|
|
|
.last()
|
|
|
|
.transpose()
|
2021-03-23 03:57:27 +00:00
|
|
|
.context("Failed to pop header notification")?;
|
2021-03-11 07:16:00 +00:00
|
|
|
|
|
|
|
if let Some(new_block) = latest_block {
|
|
|
|
tracing::debug!(
|
|
|
|
"Got notification for new block at height {}",
|
|
|
|
new_block.height
|
|
|
|
);
|
|
|
|
self.latest_block = BlockHeight::try_from(new_block)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
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(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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-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
|
|
|
}
|
|
|
|
}
|