From 13a971469ab1800f49b15078531867c7a3a0bcfe Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 2 Aug 2022 17:52:58 +0200 Subject: [PATCH 1/5] Add and implement `GetTxStatus` trait --- src/blockchain/esplora/ureq.rs | 24 ++++++++++++++++++++++++ src/blockchain/mod.rs | 22 ++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/blockchain/esplora/ureq.rs b/src/blockchain/esplora/ureq.rs index 9899b9046..2a7e75f4a 100644 --- a/src/blockchain/esplora/ureq.rs +++ b/src/blockchain/esplora/ureq.rs @@ -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(deserialize(&into_bytes(resp)?)?)), + 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..9bdff9329 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -110,6 +110,21 @@ 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(Debug, Clone, PartialEq)] +pub struct TransactionStatus { + confirmed: bool, + block_height: Option, + block_hash: Option, +} + #[maybe_async] /// Trait for getting block hash by block height pub trait GetBlockHash { @@ -359,6 +374,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 { From fbe50f2503cc2c67291c0895a239a62177cc02dc Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 4 Aug 2022 15:10:58 +0200 Subject: [PATCH 2/5] Implement `GetTxStatus` for `ElectrumBlockchain` --- Cargo.toml | 2 +- src/blockchain/any.rs | 7 +++++++ src/blockchain/electrum.rs | 32 ++++++++++++++++++++++++++++++++ src/blockchain/mod.rs | 6 +++--- 4 files changed, 43 insertions(+), 4 deletions(-) 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..0c7d5c552 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 { + 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..a9fbf4135 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -98,6 +98,38 @@ impl GetTx for ElectrumBlockchain { } } +impl GetTxStatus for ElectrumBlockchain { + fn get_tx_status(&self, txid: &Txid) -> Result { + 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(TransactionStatus { + confirmed: true, + block_height: Some(block_height), + block_hash: tx_verbose_res.blockhash, + }); + } + } + + Ok(TransactionStatus { + confirmed: false, + block_height: None, + block_hash: 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/mod.rs b/src/blockchain/mod.rs index 9bdff9329..0eb901f9b 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -87,7 +87,7 @@ pub enum Capability { /// Trait that defines the actions that must be supported by a blockchain backend #[maybe_async] -pub trait Blockchain: WalletSync + GetHeight + GetTx + GetBlockHash { +pub trait Blockchain: WalletSync + GetHeight + GetTx + GetTxStatus + GetBlockHash { /// Return the set of [`Capability`] supported by this backend fn get_capabilities(&self) -> HashSet; /// Broadcast a transaction @@ -114,7 +114,7 @@ pub trait GetTx { /// 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>; + fn get_tx_status(&self, txid: &Txid) -> Result; } /// The confirmation status of a transaction @@ -376,7 +376,7 @@ impl GetTx for Arc { #[maybe_async] impl GetTxStatus for Arc { - fn get_tx_status(&self, txid: &Txid) -> Result, Error> { + fn get_tx_status(&self, txid: &Txid) -> Result { maybe_await!(self.deref().get_tx_status(txid)) } } From 512996e2ce00f4986a23037a98ba6249f36e89fc Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 4 Aug 2022 15:11:21 +0200 Subject: [PATCH 3/5] Implement `GetTxStatus` for `EsploraBlockchain` --- src/blockchain/esplora/api.rs | 16 +++++++++++++++- src/blockchain/esplora/reqwest.rs | 21 ++++++++++++++++++++- src/blockchain/esplora/ureq.rs | 16 +++++++--------- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/blockchain/esplora/api.rs b/src/blockchain/esplora/api.rs index d548b5be8..4f9f34d92 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::TransactionStatus; 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 { @@ -33,9 +35,20 @@ pub struct Vout { pub struct TxStatus { pub confirmed: bool, pub block_height: Option, + pub block_hash: Option, pub block_time: Option, } +impl From for TransactionStatus { + fn from(esplora_tx_status: TxStatus) -> Self { + TransactionStatus { + confirmed: esplora_tx_status.confirmed, + block_height: esplora_tx_status.block_height, + block_hash: esplora_tx_status.block_hash, + } + } +} + #[derive(serde::Deserialize, Clone, Debug)] pub struct Tx { pub txid: Txid, @@ -84,6 +97,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..0ea753838 100644 --- a/src/blockchain/esplora/reqwest.rs +++ b/src/blockchain/esplora/reqwest.rs @@ -24,7 +24,7 @@ use log::{debug, error, info, trace}; use ::reqwest::{Client, StatusCode}; use futures::stream::{FuturesOrdered, TryStreamExt}; -use super::api::Tx; +use super::api::{Tx, TxStatus}; use crate::blockchain::esplora::EsploraError; use crate::blockchain::*; use crate::database::BatchDatabase; @@ -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,18 @@ impl UrlClient { Ok(Some(deserialize(&resp.error_for_status()?.bytes().await?)?)) } + async fn _get_tx_status(&self, txid: &Txid) -> Result { + let resp = self + .client + .get(&format!("{}/tx/{}/status", self.url, txid)) + .send() + .await?; + + let tx_status: TxStatus = resp.error_for_status()?.json::().await?; + + Ok(tx_status.into()) + } + 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 2a7e75f4a..697d740ad 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; @@ -113,7 +113,7 @@ impl GetTx for EsploraBlockchain { } impl GetTxStatus for EsploraBlockchain { - fn get_tx_status(&self, txid: &Txid) -> Result, Error> { + fn get_tx_status(&self, txid: &Txid) -> Result { Ok(self.url_client._get_tx_status(txid)?) } } @@ -241,20 +241,18 @@ impl UrlClient { } } - fn _get_tx_status(&self, txid: &Txid) -> Result, EsploraError> { + fn _get_tx_status(&self, txid: &Txid) -> Result { let resp = self .agent .get(&format!("{}/tx/{}/status", self.url, txid)) .call(); match resp { - Ok(resp) => Ok(Some(deserialize(&into_bytes(resp)?)?)), - Err(ureq::Error::Status(code, _)) => { - if is_status_not_found(code) { - return Ok(None); - } - Err(EsploraError::HttpResponse(code)) + Ok(resp) => { + let tx_status: TxStatus = resp.into_json()?; + Ok(tx_status.into()) } + Err(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)), Err(e) => Err(EsploraError::Ureq(e)), } } From 3dddbfa53b4fa2cb087ffa7bc88ec803c3555a19 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 4 Aug 2022 15:11:31 +0200 Subject: [PATCH 4/5] Implement `GetTxStatus` for `RpcBlockchain` --- src/blockchain/rpc.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/blockchain/rpc.rs b/src/blockchain/rpc.rs index 1d0d884c0..5ab1f3df5 100644 --- a/src/blockchain/rpc.rs +++ b/src/blockchain/rpc.rs @@ -163,6 +163,35 @@ impl GetTx for RpcBlockchain { } } +impl GetTxStatus for RpcBlockchain { + fn get_tx_status(&self, txid: &Txid) -> Result { + 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(TransactionStatus { + confirmed: true, + block_hash: tx_info.blockhash, + block_height: Some(block_height), + }); + } + } + + Ok(TransactionStatus { + confirmed: false, + block_height: None, + block_hash: None, + }) + } +} + impl GetHeight for RpcBlockchain { fn get_height(&self) -> Result { Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?) From 734a7486eba4442f90b8acdd77605fd80286bc46 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 5 Aug 2022 14:34:10 +0200 Subject: [PATCH 5/5] Clean up interface, re-use `TxStatus` struct --- src/blockchain/any.rs | 2 +- src/blockchain/electrum.rs | 20 ++++++++++++-------- src/blockchain/esplora/api.rs | 20 +------------------- src/blockchain/esplora/reqwest.rs | 12 +++++++----- src/blockchain/esplora/ureq.rs | 14 ++++++++------ src/blockchain/mod.rs | 12 +++++++----- src/blockchain/rpc.rs | 13 +++++++------ 7 files changed, 43 insertions(+), 50 deletions(-) diff --git a/src/blockchain/any.rs b/src/blockchain/any.rs index 0c7d5c552..0a1c11422 100644 --- a/src/blockchain/any.rs +++ b/src/blockchain/any.rs @@ -122,7 +122,7 @@ impl GetTx for AnyBlockchain { #[maybe_async] impl GetTxStatus for AnyBlockchain { - fn get_tx_status(&self, txid: &Txid) -> Result { + fn get_tx_status(&self, txid: &Txid) -> Result, Error> { maybe_await!(impl_inner_method!(self, get_tx_status, txid)) } } diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index a9fbf4135..cf5650387 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -99,7 +99,7 @@ impl GetTx for ElectrumBlockchain { } impl GetTxStatus for ElectrumBlockchain { - fn get_tx_status(&self, txid: &Txid) -> Result { + 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 { @@ -114,19 +114,23 @@ impl GetTxStatus for ElectrumBlockchain { // the current tip. let block_height = cur_height + 1 - confirmations; - return Ok(TransactionStatus { + 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(TransactionStatus { - confirmed: false, - block_height: None, - block_hash: None, - }) + Ok(None) } } diff --git a/src/blockchain/esplora/api.rs b/src/blockchain/esplora/api.rs index 4f9f34d92..3dc485eaa 100644 --- a/src/blockchain/esplora/api.rs +++ b/src/blockchain/esplora/api.rs @@ -1,7 +1,7 @@ //! structs from the esplora API //! //! see: -use crate::blockchain::TransactionStatus; +use crate::blockchain::TxStatus; use crate::BlockTime; use bitcoin::{BlockHash, OutPoint, Script, Transaction, TxIn, TxOut, Txid, Witness}; use std::convert::From; @@ -31,24 +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_hash: Option, - pub block_time: Option, -} - -impl From for TransactionStatus { - fn from(esplora_tx_status: TxStatus) -> Self { - TransactionStatus { - confirmed: esplora_tx_status.confirmed, - block_height: esplora_tx_status.block_height, - block_hash: esplora_tx_status.block_hash, - } - } -} - #[derive(serde::Deserialize, Clone, Debug)] pub struct Tx { pub txid: Txid, diff --git a/src/blockchain/esplora/reqwest.rs b/src/blockchain/esplora/reqwest.rs index 0ea753838..3ba9a2b83 100644 --- a/src/blockchain/esplora/reqwest.rs +++ b/src/blockchain/esplora/reqwest.rs @@ -24,7 +24,7 @@ use log::{debug, error, info, trace}; use ::reqwest::{Client, StatusCode}; use futures::stream::{FuturesOrdered, TryStreamExt}; -use super::api::{Tx, TxStatus}; +use super::api::Tx; use crate::blockchain::esplora::EsploraError; use crate::blockchain::*; use crate::database::BatchDatabase; @@ -119,7 +119,7 @@ impl GetTx for EsploraBlockchain { #[maybe_async] impl GetTxStatus for EsploraBlockchain { - fn get_tx_status(&self, txid: &Txid) -> Result { + fn get_tx_status(&self, txid: &Txid) -> Result { Ok(await_or_block!(self.url_client._get_tx_status(txid))?) } } @@ -239,16 +239,18 @@ impl UrlClient { Ok(Some(deserialize(&resp.error_for_status()?.bytes().await?)?)) } - async fn _get_tx_status(&self, txid: &Txid) -> Result { + async fn _get_tx_status(&self, txid: &Txid) -> Result, EsploraError> { let resp = self .client .get(&format!("{}/tx/{}/status", self.url, txid)) .send() .await?; - let tx_status: TxStatus = resp.error_for_status()?.json::().await?; + if let StatusCode::NOT_FOUND = resp.status() { + return Ok(None); + } - Ok(tx_status.into()) + Ok(Some(resp.error_for_status()?.json().await?)) } async fn _get_tx_no_opt(&self, txid: &Txid) -> Result { diff --git a/src/blockchain/esplora/ureq.rs b/src/blockchain/esplora/ureq.rs index 697d740ad..8f7718ec4 100644 --- a/src/blockchain/esplora/ureq.rs +++ b/src/blockchain/esplora/ureq.rs @@ -113,7 +113,7 @@ impl GetTx for EsploraBlockchain { } impl GetTxStatus for EsploraBlockchain { - fn get_tx_status(&self, txid: &Txid) -> Result { + fn get_tx_status(&self, txid: &Txid) -> Result, Error> { Ok(self.url_client._get_tx_status(txid)?) } } @@ -241,18 +241,20 @@ impl UrlClient { } } - fn _get_tx_status(&self, txid: &Txid) -> Result { + 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) => { - let tx_status: TxStatus = resp.into_json()?; - Ok(tx_status.into()) + 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(ureq::Error::Status(code, _)) => Err(EsploraError::HttpResponse(code)), Err(e) => Err(EsploraError::Ureq(e)), } } diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index 0eb901f9b..f3df582a1 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -87,7 +87,7 @@ pub enum Capability { /// Trait that defines the actions that must be supported by a blockchain backend #[maybe_async] -pub trait Blockchain: WalletSync + GetHeight + GetTx + GetTxStatus + GetBlockHash { +pub trait Blockchain: WalletSync + GetHeight + GetTx + GetBlockHash { /// Return the set of [`Capability`] supported by this backend fn get_capabilities(&self) -> HashSet; /// Broadcast a transaction @@ -114,15 +114,17 @@ pub trait GetTx { /// 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; + fn get_tx_status(&self, txid: &Txid) -> Result, Error>; } /// The confirmation status of a transaction -#[derive(Debug, Clone, PartialEq)] -pub struct TransactionStatus { +#[derive(serde::Deserialize, Clone, Debug, PartialEq)] +pub struct TxStatus { confirmed: bool, block_height: Option, block_hash: Option, + pub block_time: Option, +} } #[maybe_async] @@ -376,7 +378,7 @@ impl GetTx for Arc { #[maybe_async] impl GetTxStatus for Arc { - fn get_tx_status(&self, txid: &Txid) -> Result { + fn get_tx_status(&self, txid: &Txid) -> Result, Error> { maybe_await!(self.deref().get_tx_status(txid)) } } diff --git a/src/blockchain/rpc.rs b/src/blockchain/rpc.rs index 5ab1f3df5..b2e72896d 100644 --- a/src/blockchain/rpc.rs +++ b/src/blockchain/rpc.rs @@ -164,7 +164,7 @@ impl GetTx for RpcBlockchain { } impl GetTxStatus for RpcBlockchain { - fn get_tx_status(&self, txid: &Txid) -> Result { + 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 { @@ -176,19 +176,20 @@ impl GetTxStatus for RpcBlockchain { // the current tip. let block_height = cur_height + 1 - confirmations; - return Ok(TransactionStatus { + 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), + })); } } - - Ok(TransactionStatus { + return Ok(Some(TxStatus { confirmed: false, block_height: None, block_hash: None, - }) + block_time: None, + })); } }