diff --git a/Cargo.toml b/Cargo.toml index 84171ab58..448cdee0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ rand = "^0.7" # Optional dependencies sled = { version = "0.34", optional = true } -electrum-client = { version = "0.10", optional = true } +electrum-client = { git = "https://github.com/tnull/rust-electrum-client", branch = "TheCharlatan-verboseTransactionGet-rebased", optional = true} rusqlite = { version = "0.27.0", optional = true } ahash = { version = "0.7.6", optional = true } reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] } diff --git a/src/blockchain/any.rs b/src/blockchain/any.rs index 5ef1a3385..0a1c11422 100644 --- a/src/blockchain/any.rs +++ b/src/blockchain/any.rs @@ -120,6 +120,13 @@ impl GetTx for AnyBlockchain { } } +#[maybe_async] +impl GetTxStatus for AnyBlockchain { + fn get_tx_status(&self, txid: &Txid) -> Result, Error> { + maybe_await!(impl_inner_method!(self, get_tx_status, txid)) + } +} + #[maybe_async] impl GetBlockHash for AnyBlockchain { fn get_block_hash(&self, height: u64) -> Result { diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index faf7ea756..cf5650387 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -98,6 +98,42 @@ impl GetTx for ElectrumBlockchain { } } +impl GetTxStatus for ElectrumBlockchain { + fn get_tx_status(&self, txid: &Txid) -> Result, Error> { + let tx_verbose_res = self.client.transaction_get_verbose(txid)?; + if let Some(confirmations) = tx_verbose_res.confirmations { + if confirmations > 0 { + // TODO: this is unfortunately race-y since a new block might have come in between the + // first and second request. + let cur_height = self + .client + .block_headers_subscribe() + .map(|data| data.height as u32)?; + + // Calculate the confirmation block height. If a transaction has one confirmation, it was confirmed in + // the current tip. + let block_height = cur_height + 1 - confirmations; + + return Ok(Some(TxStatus { + confirmed: true, + block_height: Some(block_height), + block_hash: tx_verbose_res.blockhash, + block_time: tx_verbose_res.blocktime.map(|t| t as u64), + })); + } else { + return Ok(Some(TxStatus { + confirmed: false, + block_height: None, + block_hash: None, + block_time: None, + })); + } + } + + Ok(None) + } +} + impl GetBlockHash for ElectrumBlockchain { fn get_block_hash(&self, height: u64) -> Result { let block_header = self.client.block_header(height as usize)?; diff --git a/src/blockchain/esplora/api.rs b/src/blockchain/esplora/api.rs index d548b5be8..3dc485eaa 100644 --- a/src/blockchain/esplora/api.rs +++ b/src/blockchain/esplora/api.rs @@ -1,8 +1,10 @@ //! structs from the esplora API //! //! see: +use crate::blockchain::TxStatus; use crate::BlockTime; -use bitcoin::{OutPoint, Script, Transaction, TxIn, TxOut, Txid, Witness}; +use bitcoin::{BlockHash, OutPoint, Script, Transaction, TxIn, TxOut, Txid, Witness}; +use std::convert::From; #[derive(serde::Deserialize, Clone, Debug)] pub struct PrevOut { @@ -29,13 +31,6 @@ pub struct Vout { pub scriptpubkey: Script, } -#[derive(serde::Deserialize, Clone, Debug)] -pub struct TxStatus { - pub confirmed: bool, - pub block_height: Option, - pub block_time: Option, -} - #[derive(serde::Deserialize, Clone, Debug)] pub struct Tx { pub txid: Txid, @@ -84,6 +79,7 @@ impl Tx { confirmed: true, block_height: Some(height), block_time: Some(timestamp), + .. } => Some(BlockTime { timestamp, height }), _ => None, } diff --git a/src/blockchain/esplora/reqwest.rs b/src/blockchain/esplora/reqwest.rs index 302e811fd..3ba9a2b83 100644 --- a/src/blockchain/esplora/reqwest.rs +++ b/src/blockchain/esplora/reqwest.rs @@ -117,6 +117,13 @@ impl GetTx for EsploraBlockchain { } } +#[maybe_async] +impl GetTxStatus for EsploraBlockchain { + fn get_tx_status(&self, txid: &Txid) -> Result { + Ok(await_or_block!(self.url_client._get_tx_status(txid))?) + } +} + #[maybe_async] impl GetBlockHash for EsploraBlockchain { fn get_block_hash(&self, height: u64) -> Result { @@ -232,6 +239,20 @@ impl UrlClient { Ok(Some(deserialize(&resp.error_for_status()?.bytes().await?)?)) } + async fn _get_tx_status(&self, txid: &Txid) -> Result, EsploraError> { + let resp = self + .client + .get(&format!("{}/tx/{}/status", self.url, txid)) + .send() + .await?; + + if let StatusCode::NOT_FOUND = resp.status() { + return Ok(None); + } + + Ok(Some(resp.error_for_status()?.json().await?)) + } + async fn _get_tx_no_opt(&self, txid: &Txid) -> Result { match self._get_tx(txid).await { Ok(Some(tx)) => Ok(tx), diff --git a/src/blockchain/esplora/ureq.rs b/src/blockchain/esplora/ureq.rs index 9899b9046..8f7718ec4 100644 --- a/src/blockchain/esplora/ureq.rs +++ b/src/blockchain/esplora/ureq.rs @@ -26,7 +26,7 @@ use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::hashes::{sha256, Hash}; use bitcoin::{BlockHeader, Script, Transaction, Txid}; -use super::api::Tx; +use super::api::{Tx, TxStatus}; use crate::blockchain::esplora::EsploraError; use crate::blockchain::*; use crate::database::BatchDatabase; @@ -112,6 +112,12 @@ impl GetTx for EsploraBlockchain { } } +impl GetTxStatus for EsploraBlockchain { + fn get_tx_status(&self, txid: &Txid) -> Result, Error> { + Ok(self.url_client._get_tx_status(txid)?) + } +} + impl GetBlockHash for EsploraBlockchain { fn get_block_hash(&self, height: u64) -> Result { let block_header = self.url_client._get_header(height as u32)?; @@ -235,6 +241,24 @@ impl UrlClient { } } + fn _get_tx_status(&self, txid: &Txid) -> Result, EsploraError> { + let resp = self + .agent + .get(&format!("{}/tx/{}/status", self.url, txid)) + .call(); + + match resp { + Ok(resp) => Ok(Some(resp.into_json()?)), + Err(ureq::Error::Status(code, _)) => { + if is_status_not_found(code) { + return Ok(None); + } + Err(EsploraError::HttpResponse(code)) + } + Err(e) => Err(EsploraError::Ureq(e)), + } + } + fn _get_tx_no_opt(&self, txid: &Txid) -> Result { match self._get_tx(txid) { Ok(Some(tx)) => Ok(tx), diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index 1dc5c95a1..f3df582a1 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -110,6 +110,23 @@ pub trait GetTx { fn get_tx(&self, txid: &Txid) -> Result, Error>; } +#[maybe_async] +/// Trait for getting the status of a transaction by txid +pub trait GetTxStatus { + /// Fetch the status of a transaction given its txid + fn get_tx_status(&self, txid: &Txid) -> Result, Error>; +} + +/// The confirmation status of a transaction +#[derive(serde::Deserialize, Clone, Debug, PartialEq)] +pub struct TxStatus { + confirmed: bool, + block_height: Option, + block_hash: Option, + pub block_time: Option, +} +} + #[maybe_async] /// Trait for getting block hash by block height pub trait GetBlockHash { @@ -359,6 +376,13 @@ impl GetTx for Arc { } } +#[maybe_async] +impl GetTxStatus for Arc { + fn get_tx_status(&self, txid: &Txid) -> Result, Error> { + maybe_await!(self.deref().get_tx_status(txid)) + } +} + #[maybe_async] impl GetHeight for Arc { fn get_height(&self) -> Result { diff --git a/src/blockchain/rpc.rs b/src/blockchain/rpc.rs index 1d0d884c0..b2e72896d 100644 --- a/src/blockchain/rpc.rs +++ b/src/blockchain/rpc.rs @@ -163,6 +163,36 @@ impl GetTx for RpcBlockchain { } } +impl GetTxStatus for RpcBlockchain { + fn get_tx_status(&self, txid: &Txid) -> Result, Error> { + let tx_info = self.client.get_raw_transaction_info(txid, None)?; + if let Some(confirmations) = tx_info.confirmations { + if confirmations > 0 { + // TODO: this is unfortunately race-y since a new block might have come in between the + // first and second request. + let cur_height = self.client.get_blockchain_info().map(|i| i.blocks as u32)?; + + // Calculate the confirmation block height. If a transaction has one confirmation, it was confirmed in + // the current tip. + let block_height = cur_height + 1 - confirmations; + + return Ok(Some(TxStatus { + confirmed: true, + block_hash: tx_info.blockhash, + block_height: Some(block_height), + block_time: tx_info.blocktime.map(|t| t as u64), + })); + } + } + return Ok(Some(TxStatus { + confirmed: false, + block_height: None, + block_hash: None, + block_time: None, + })); + } +} + impl GetHeight for RpcBlockchain { fn get_height(&self) -> Result { Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)