Browse Source

Implement custom script subscribe for btc

Janus 4 năm trước cách đây
mục cha
commit
05d036c642
3 tập tin đã thay đổi với 464 bổ sung69 xóa
  1. 88 2
      Cargo.lock
  2. 4 4
      Cargo.toml
  3. 372 63
      src/service/btc.rs

+ 88 - 2
Cargo.lock

@@ -420,6 +420,46 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 
+[[package]]
+name = "base64-compat"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "bdk"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecf7e997526ceefbab7dd99fc0da6834ed8853bd051f53523415ed1dc82b870d"
+dependencies = [
+ "async-trait",
+ "bdk-macros",
+ "bitcoin",
+ "electrum-client",
+ "js-sys",
+ "log",
+ "miniscript",
+ "rand 0.7.3",
+ "serde",
+ "serde_json",
+ "sled",
+ "tokio",
+]
+
+[[package]]
+name = "bdk-macros"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81c1980e50ae23bb6efa9283ae8679d6ea2c6fa6a99fe62533f65f4a25a1a56c"
+dependencies = [
+ "proc-macro2 1.0.30",
+ "quote 1.0.10",
+ "syn 1.0.80",
+]
+
 [[package]]
 name = "bech32"
 version = "0.8.1"
@@ -486,6 +526,7 @@ version = "0.27.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a41df6ad9642c5c15ae312dd3d074de38fd3eb7cc87ad4ce10f90292a83fe4d"
 dependencies = [
+ "base64-compat",
  "bech32",
  "bitcoin_hashes",
  "secp256k1",
@@ -1255,16 +1296,17 @@ dependencies = [
 name = "darkfi"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "async-channel",
  "async-executor",
  "async-native-tls",
  "async-std",
  "async-trait",
  "async-tungstenite",
+ "bdk",
  "bellman",
  "bimap",
  "bitcoin",
- "bitcoin_hashes",
  "bitvec 0.18.5",
  "blake2b_simd",
  "blake2s_simd",
@@ -1275,7 +1317,6 @@ dependencies = [
  "crypto_api_chachapoly",
  "dirs 4.0.0",
  "easy-parallel",
- "electrum-client",
  "ff",
  "futures 0.3.17",
  "group",
@@ -1715,6 +1756,16 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "fuchsia-cprng"
 version = "0.1.1"
@@ -1852,6 +1903,15 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.12.4"
@@ -2507,6 +2567,16 @@ version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
 
+[[package]]
+name = "miniscript"
+version = "6.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d69450033bf162edf854d4aacaff82ca5ef34fa81f6cf69e1c81a103f0834997"
+dependencies = [
+ "bitcoin",
+ "serde",
+]
+
 [[package]]
 name = "miniz_oxide"
 version = "0.4.4"
@@ -3767,6 +3837,22 @@ version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
 
+[[package]]
+name = "sled"
+version = "0.34.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
+dependencies = [
+ "crc32fast",
+ "crossbeam-epoch 0.9.5",
+ "crossbeam-utils 0.8.5",
+ "fs2",
+ "fxhash",
+ "libc",
+ "log",
+ "parking_lot 0.11.2",
+]
+
 [[package]]
 name = "smallvec"
 version = "1.7.0"

+ 4 - 4
Cargo.toml

@@ -70,16 +70,16 @@ spl-token = {version = "3.2.0", features = ["no-entrypoint"], optional = true}
 spl-associated-token-account = {version = "1.0.3", features = ["no-entrypoint"], optional = true}
 
 ## Cashier Bitcoin Dependencies
-bitcoin = {version = "0.27.0", optional = true}
-bitcoin_hashes = "0.10.0"
+anyhow = "1.0.44"
+bdk = {version = "0.12.0", optional = true}
+bitcoin = {version = "0.27.0", optional = true }
 secp256k1 = {version = "0.20.3", default-features = false, features = ["rand-std"], optional = true}
-electrum-client = {version = "0.8.0", optional = true}
 
 ## Cashier Ethereum Dependencies
 hash-db = {version = "0.15.2", optional = true}
 keccak-hasher = {version = "0.15.3", optional = true}
 
 [features]
-btc = ["bitcoin", "secp256k1", "electrum-client"]
+btc = ["bdk", "bitcoin", "secp256k1"]
 sol = ["solana-sdk", "solana-client", "spl-token", "spl-associated-token-account"]
 eth = ["keccak-hasher", "hash-db"]

+ 372 - 63
src/service/btc.rs

@@ -1,8 +1,13 @@
-use async_std::sync::Arc;
-use std::convert::From;
+use async_std::sync::{Arc, Mutex};
+use std::collections::{BTreeMap, HashMap};
+use std::convert::{From, TryFrom, TryInto};
+use std::cmp::max;
+use std::fmt;
+use std::ops::Add;
 use std::str::FromStr;
-use std::time::Duration;
+use std::time::{Duration, Instant};
 
+use anyhow::Context;
 use async_executor::Executor;
 use async_trait::async_trait;
 
@@ -11,12 +16,18 @@ use bitcoin::blockdata::{
     transaction::{OutPoint, SigHashType, Transaction, TxIn, TxOut},
 };
 use bitcoin::consensus::encode::serialize_hex;
-use bitcoin::hash_types::PubkeyHash as BtcPubKeyHash;
+use bitcoin::hash_types::{PubkeyHash as BtcPubKeyHash, Txid};
 use bitcoin::network::constants::Network;
 use bitcoin::util::address::Address;
 use bitcoin::util::ecdsa::{PrivateKey as BtcPrivKey, PublicKey as BtcPubKey};
 use bitcoin::util::psbt::serialize::Serialize;
-use electrum_client::{Client as ElectrumClient, ElectrumApi, GetBalanceRes};
+use bdk::electrum_client::{
+    Client as ElectrumClient,
+    ElectrumApi,
+    GetBalanceRes,
+    GetHistoryRes,
+    HeaderNotification
+};
 use log::*;
 use secp256k1::{
     constants::{PUBLIC_KEY_SIZE, SECRET_KEY_SIZE},
@@ -37,6 +48,40 @@ pub type PrivKey = BtcPrivKey;
 
 const KEYPAIR_LENGTH: usize = SECRET_KEY_SIZE + PUBLIC_KEY_SIZE;
 
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
+pub struct BlockHeight(u32);
+
+impl From<BlockHeight> for u32 {
+fn from(height: BlockHeight) -> Self {
+        height.0
+    }
+}
+
+impl TryFrom<HeaderNotification> for BlockHeight {
+    type Error = BtcFailed;
+    fn try_from(value: HeaderNotification) -> BtcResult<Self> {
+        Ok(Self(
+            value
+                .height
+                .try_into()
+                .context("Failed to fit usize into u32")?,
+        ))
+    }
+}
+
+impl Add<u32> for BlockHeight {
+    type Output = BlockHeight;
+    fn add(self, rhs: u32) -> Self::Output {
+        BlockHeight(self.0 + rhs)
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum ExpiredTimelocks {
+    None,
+    Cancel,
+    Punish,
+}
 #[derive(Clone, Debug, PartialEq)]
 pub struct Keypair {
     secret: SecretKey,
@@ -155,17 +200,163 @@ impl Account {
         Script::new_p2pkh(&btc_pubkey_hash)
     }
 }
+fn print_status_change(txid: Txid, old: Option<ScriptStatus>, new: ScriptStatus) -> ScriptStatus {
+    match (old, new) {
+        (None, new_status) => {
+            debug!(target: "BTC BRIDGE", "Found relevant Bitcoin transaction: {:?} {:?}", txid, new_status);
+        }
+        (Some(old_status), new_status) if old_status != new_status => {
+            debug!(target: "BTC BRIDGE", "Bitcoin transaction status changed: {:?} {} {}", txid, new_status, old_status);
+        }
+        _ => {}
+    }
+
+    new
+}
+
+fn sync_interval(avg_block_time: Duration) -> Duration {
+    max(avg_block_time / 10, Duration::from_secs(1))
+}
+pub struct Client {
+    electrum: ElectrumClient,
+    subscriptions: Arc<Mutex<Vec<Script>>>,
+    latest_block_height: BlockHeight,
+    last_sync: Instant,
+    sync_interval: Duration,
+    script_history: BTreeMap<Script, Vec<GetHistoryRes>>,
+
+}
+impl Client {
+    pub fn new(electrum_url: &str) -> BtcResult<Self> {
+
+       let config = bdk::electrum_client::ConfigBuilder::default()
+            .retry(5)
+            .build();
+        let client = ElectrumClient::from_config(electrum_url, config)?;
+
+        let electrum = ElectrumClient::new(electrum_url)
+            .map_err(|err| crate::Error::from(super::BtcFailed::from(err)))?;
+
+        let latest_block = electrum
+            .block_headers_subscribe()?;
+
+        //testnet avg block time
+        let interval = sync_interval(Duration::from_secs(300));
+
+        Ok(Self {
+            electrum: electrum,
+            subscriptions: Arc::new(Mutex::new(Vec::new())),
+            latest_block_height: BlockHeight::try_from(latest_block)
+                .map_err(|_| crate::Error::TryFromError)?,
+            last_sync: Instant::now(),
+            sync_interval: interval,
+            script_history: Default::default(),
+        })
+    }
+    fn update_state(&mut self) -> Result<()> {
+        let now = Instant::now();
+        if now < self.last_sync + self.sync_interval {
+            return Ok(());
+        }
+
+        self.last_sync = now;
+        self.update_latest_block()?;
+        self.update_script_histories()?;
+
+        Ok(())
+    }
+    fn update_latest_block(&mut self) -> BtcResult<()> {
+        let latest_block = self
+            .electrum
+            .block_headers_subscribe()?;
+        let latest_block_height = BlockHeight::try_from(latest_block)
+            .map_err(|err| crate::Error::from(super::BtcFailed::from(err)))?;
+
+        if latest_block_height > self.latest_block_height {
+            // debug!( target: "BTC BRIDGE", "{} {}"
+            //     u32::from(latest_block_height),
+            //     "Got notification for new block"
+            // );
+            self.latest_block_height = latest_block_height;
+        }
+
+        Ok(())
+    }
+
+    fn update_script_histories(&mut self) -> BtcResult<()> {
+        let histories = self
+            .electrum
+            .batch_script_get_history(self.script_history.keys())?;
+
+        if histories.len() != self.script_history.len() {
+            debug!(
+                "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(())
+    }
+
+    pub fn status_of_script<T>(&mut self, tx: &T) -> BtcResult<ScriptStatus>
+    where
+        T: Watchable,
+    {
+        let txid = tx.id();
+        let script = tx.script();
+
 
+        if !self.script_history.contains_key(&script) {
+            self.script_history.insert(script.clone(), vec![]);
+        }
+
+        self.update_state()?;
+
+        let history = self.script_history.entry(script).or_default();
+
+        let history_of_tx = history
+            .iter()
+            .filter(|entry| entry.tx_hash == txid)
+            .collect::<Vec<_>>();
+
+        match history_of_tx.as_slice() {
+            [] => Ok(ScriptStatus::Unseen),
+            [remaining @ .., last] => {
+                if !remaining.is_empty() {
+                    debug!("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)
+                                .map_err(|_| crate::Error::TryFromError)?,
+                            u32::from(self.latest_block_height),
+                        ),
+                    ))
+                }
+            }
+        }
+    }
+
+}
 pub struct BtcClient {
     main_account: Account,
+    client: Arc<Mutex<Client>>,
     notify_channel: (
         async_channel::Sender<TokenNotification>,
         async_channel::Receiver<TokenNotification>,
     ),
-    client: Arc<ElectrumClient>,
     network: Network,
 }
-
 impl BtcClient {
     pub async fn new(main_keypair: Keypair, network: &str) -> Result<Arc<Self>> {
         //TODO
@@ -181,19 +372,18 @@ impl BtcClient {
 
         let main_account = Account::new(&main_keypair, network);
 
-        let electrum_client = ElectrumClient::new(url)
-            .map_err(|err| crate::Error::from(super::BtcFailed::from(err)))?;
-
         Ok(Arc::new(Self {
             main_account,
+            client: Arc::new(Mutex::new(Client::new(url)?)),
             notify_channel,
-            client: Arc::new(electrum_client),
             network,
         }))
     }
 
+
     async fn handle_subscribe_request(
         self: Arc<Self>,
+        tx: impl Watchable + Send + 'static,
         btc_keys: Account,
         drk_pub_key: jubjub::SubgroupPoint,
     ) -> BtcResult<()> {
@@ -201,51 +391,54 @@ impl BtcClient {
             target: "BTC BRIDGE",
             "Handle subscribe request"
         );
-        let client = &self.client;
-
-        let keys_clone = btc_keys.clone();
-        // p2pkh script
-        let script = keys_clone.script_pubkey;
-
+        let client = self.client.clone();
+        let electrum = &client.lock().await.electrum;
+        let script = tx.script();
+        let txid = tx.id();
+
+        // Check if we're already subscribed
+        if client.lock().await
+            .subscriptions.lock().await.contains(&script) {
+            return Ok(());
+        }
         //Fetch any current balance
-        let prev_balance = client.script_get_balance(&script)?;
+        let prev_balance = electrum.script_get_balance(&script)?;
 
         let cur_balance: GetBalanceRes;
+        //let status = client.script_subscribe(&script)?;
+        let mut last_status = None;
 
-        let status = client.script_subscribe(&script)?;
 
-        loop {
-            let current_status = client.script_pop(&script)?;
-            debug!(target: "BTC BRIDGE", "script status: {:?}", status);
-            debug!(target: "BTC BRIDGE", "current_script status: {:?}", current_status);
-            if current_status == status {
-                async_std::task::sleep(Duration::from_secs(5)).await;
-                debug!(
-                    target: "BTC BRIDGE",
-                    "ScriptPubKey status has not changed, amtucfd: {}, amtcfd: {}",
-                    client.script_get_balance(&script)?.unconfirmed,
-                    client.script_get_balance(&script)?.confirmed
-                );
-                continue;
-            }
 
-            match current_status {
-                Some(_) => {
-                    // Script has a notification update
-                    debug!(target: "BTC BRIDGE", "ScripPubKey notify update");
-                    //TODO: unsubscribe is never successful
-                    //let _ = client.script_unsubscribe(&script)?;
-                    break;
-                }
-                None => {
-                    return Err(BtcFailed::ElectrumError(
-                        "ScriptPubKey was not found".to_string(),
-                    ));
+        loop {
+            async_std::task::sleep(Duration::from_secs(5)).await;
+            //let current_status = client.script_pop(&script)?;
+
+            let new_status = match client.lock().await.status_of_script(&tx) {
+                Ok(new_status) => new_status,
+                Err(error) => {
+                    debug!(target: "BTC BRIDGE", "Failed to get status of script: {:#}", error);
+                    return Err(BtcFailed::BtcError("Failed to get status of script".to_string()));
                 }
             };
+
+            last_status = Some(print_status_change(txid, last_status, new_status));
+
+            match new_status {
+                ScriptStatus::Unseen => continue,
+                ScriptStatus::InMempool => continue,
+                ScriptStatus::Confirmed(inner) => {
+                    let confirmations = inner.confirmations();
+                    debug!(target: "BTC BRIDGE", "Received confirmed tx: {:#}", confirmations);
+                    if confirmations > 0 {
+                        break
+                    }
+                },
+            }
+
         } // Endloop
 
-        cur_balance = client.script_get_balance(&script)?;
+        cur_balance = electrum.script_get_balance(&script)?;
 
         let send_notification = self.notify_channel.0.clone();
 
@@ -271,17 +464,18 @@ impl BtcClient {
             .map_err(Error::from)?;
 
         debug!(target: "BTC BRIDGE", "Received {} btc", ui_amnt);
-        let _ = self.send_btc_to_main_wallet(amnt as u64, btc_keys)?;
+        let _ = self.send_btc_to_main_wallet(amnt as u64, btc_keys).await;
 
         Ok(())
     }
 
-    fn send_btc_to_main_wallet(self: Arc<Self>, amount: u64, btc_keys: Account) -> BtcResult<()> {
+    async fn send_btc_to_main_wallet(self: Arc<Self>, amount: u64, btc_keys: Account) -> BtcResult<()> {
         debug!(target: "BTC BRIDGE", "Sending {} BTC to main wallet", amount);
-        let client = &self.client;
+        let client = self.client.lock().await;
+        let electrum = &client.electrum;
         let keys_clone = btc_keys.clone();
         let script = keys_clone.script_pubkey;
-        let utxo = client.script_list_unspent(&script)?;
+        let utxo = electrum.script_list_unspent(&script)?;
 
         let mut inputs = Vec::new();
         let mut amounts: u64 = 0;
@@ -311,13 +505,10 @@ impl BtcClient {
             version: 2,
         };
 
-        //TODO: Better handling of fees, don't cast to u64
         let tx_size = transaction.get_size();
-        //Estimate fee for getting in next block
 
         let fee_per_kb = client.estimate_fee(1)?;
         let _fee = tx_size as f64 * fee_per_kb * 100000_f64;
-        //let value = amounts - fee as u64;
 
         let transaction = Transaction {
             input: inputs,
@@ -346,11 +537,12 @@ impl BtcClient {
         debug!(target: "BTC BRIDGE", "Signed tx: {:?}",
                serialize_hex(&signed_tx));
 
-        let txid = client.transaction_broadcast_raw(&signed_tx.serialize().to_vec())?;
+        let txid = electrum.transaction_broadcast_raw(&signed_tx.serialize().to_vec())?;
 
         debug!(target: "BTC BRIDGE", "Sent {} satoshi to main wallet, txid: {}", amount, txid);
         Ok(())
     }
+
 }
 
 #[async_trait]
@@ -367,12 +559,22 @@ impl NetworkClient for BtcClient {
         let private_key = serialize(&keypair);
         let public_key = btc_keys.address.to_string();
 
+        let keys_clone = btc_keys.clone();
+        let script = keys_clone.script_pubkey;
+
+        let txid = self.client.lock().await.electrum
+            .script_get_history(&script).unwrap()[0].tx_hash;
+
         // start scheduler for checking balance
         debug!(target: "BRIDGE BITCOIN", "Subscribing for deposit");
 
         executor
             .spawn(async move {
-                let result = self.handle_subscribe_request(btc_keys, drk_pub_key).await;
+                let result = self.handle_subscribe_request(
+                    (txid, script),
+                    btc_keys,
+                    drk_pub_key
+                ).await;
                 if let Err(e) = result {
                     error!(target: "BTC BRIDGE SUBSCRIPTION","{}", e.to_string());
                 }
@@ -397,9 +599,20 @@ impl NetworkClient for BtcClient {
         let btc_keys = Account::new(&keypair, self.network);
         let public_key = btc_keys.address.to_string();
 
+        let keys_clone = btc_keys.clone();
+        let script = keys_clone.script_pubkey;
+
+        //Ugly
+        let txid = self.client.lock().await.electrum.
+            script_get_history(&script).unwrap()[0].tx_hash;
+
         executor
             .spawn(async move {
-                let result = self.handle_subscribe_request(btc_keys, drk_pub_key).await;
+                let result = self.handle_subscribe_request(
+                    (txid, script),
+                    btc_keys,
+                    drk_pub_key
+                ).await;
                 if let Err(e) = result {
                     error!(target: "BTC BRIDGE SUBSCRIPTION","{}", e.to_string());
                 }
@@ -418,14 +631,14 @@ impl NetworkClient for BtcClient {
         amount: u64,
     ) -> Result<()> {
         // address is not a btc address, so derive the btc address
-        let client = &self.client;
+        let electrum = &self.client.lock().await.electrum;
         let public_key = deserialize(&address)?;
         let script_pubkey = Account::derive_btc_script_pubkey(public_key, self.network);
 
         let main_script_pubkey = &self.main_account.script_pubkey;
 
-        let main_utxo = client
-            .script_list_unspent(main_script_pubkey)
+        let main_utxo = electrum
+            .script_list_unspent(&main_script_pubkey)
             .map_err(|e| Error::from(BtcFailed::from(e)))?;
 
         let transaction = Transaction {
@@ -455,7 +668,7 @@ impl NetworkClient for BtcClient {
             &self.main_account.keypair.context,
         )?;
 
-        let txid = client
+        let txid = electrum
             .transaction_broadcast_raw(&signed_tx.serialize().to_vec())
             .map_err(|e| Error::from(BtcFailed::from(e)))?;
 
@@ -502,6 +715,97 @@ pub fn sign_transaction(
         output: tx.output,
     })
 }
+#[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 {
+    depth: u32,
+}
+
+impl Confirmed {
+    pub fn new(depth: u32) -> Self {
+        Self { depth }
+    }
+    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
+    }
+
+    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())
+            }
+        }
+    }
+}
+
+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()
+    }
+}
 impl Encodable for bitcoin::Transaction {
     fn encode<S: std::io::Write>(&self, s: S) -> Result<usize> {
         let tx = self.serialize();
@@ -605,7 +909,6 @@ pub enum BtcFailed {
     KeypairError(String),
     Notification(String),
 }
-
 impl std::error::Error for BtcFailed {}
 
 impl std::fmt::Display for BtcFailed {
@@ -649,8 +952,8 @@ impl From<bitcoin::util::address::Error> for BtcFailed {
         BtcFailed::BadBtcAddress(err.to_string())
     }
 }
-impl From<electrum_client::Error> for BtcFailed {
-    fn from(err: electrum_client::Error) -> BtcFailed {
+impl From<bdk::electrum_client::Error> for BtcFailed {
+    fn from(err: bdk::electrum_client::Error) -> BtcFailed {
         BtcFailed::ElectrumError(err.to_string())
     }
 }
@@ -660,6 +963,12 @@ impl From<bitcoin::util::key::Error> for BtcFailed {
         BtcFailed::DecodeAndEncodeError(err.to_string())
     }
 }
+impl From<anyhow::Error> for BtcFailed {
+    fn from(err: anyhow::Error) -> BtcFailed {
+        BtcFailed::DecodeAndEncodeError(err.to_string())
+    }
+}
+
 
 pub type BtcResult<T> = std::result::Result<T, BtcFailed>;