diff --git a/src/api.rs b/src/api.rs index e7522de..e1f63fa 100644 --- a/src/api.rs +++ b/src/api.rs @@ -4,7 +4,7 @@ use std::borrow::Borrow; use std::convert::TryInto; use bitcoin::consensus::encode::{deserialize, serialize}; -use bitcoin::{block, Script, Transaction, Txid}; +use bitcoin::{block, FeeRate, Script, Transaction, Txid}; use crate::batch::Batch; use crate::types::*; @@ -94,11 +94,11 @@ pub trait ElectrumApi { /// Tries to fetch `count` block headers starting from `start_height`. fn block_headers(&self, start_height: usize, count: usize) -> Result; - /// Estimates the fee required in **Bitcoin per kilobyte** to confirm a transaction in `number` blocks. - fn estimate_fee(&self, number: usize) -> Result; + /// Estimates the fee required in [`FeeRate`] to confirm a transaction in `number` blocks. + fn estimate_fee(&self, number: usize) -> Result; /// Returns the minimum accepted fee by the server's node in **Bitcoin, not Satoshi**. - fn relay_fee(&self) -> Result; + fn relay_fee(&self) -> Result; /// Subscribes to notifications for activity on a specific *scriptPubKey*. /// @@ -189,7 +189,7 @@ pub trait ElectrumApi { /// /// Takes a list of `numbers` of blocks and returns a list of fee required in /// **Satoshis per kilobyte** to confirm a transaction in the given number of blocks. - fn batch_estimate_fee(&self, numbers: I) -> Result, Error> + fn batch_estimate_fee(&self, numbers: I) -> Result, Error> where I: IntoIterator + Clone, I::Item: Borrow; diff --git a/src/client.rs b/src/client.rs index 3511724..a819edf 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,7 +4,7 @@ use std::{borrow::Borrow, sync::RwLock}; use log::{info, warn}; -use bitcoin::{Script, Txid}; +use bitcoin::{FeeRate, Script, Txid}; use crate::api::ElectrumApi; use crate::batch::Batch; @@ -207,12 +207,12 @@ impl ElectrumApi for Client { } #[inline] - fn estimate_fee(&self, number: usize) -> Result { + fn estimate_fee(&self, number: usize) -> Result { impl_inner_call!(self, estimate_fee, number) } #[inline] - fn relay_fee(&self) -> Result { + fn relay_fee(&self) -> Result { impl_inner_call!(self, relay_fee) } @@ -309,7 +309,7 @@ impl ElectrumApi for Client { } #[inline] - fn batch_estimate_fee<'s, I>(&self, numbers: I) -> Result, Error> + fn batch_estimate_fee<'s, I>(&self, numbers: I) -> Result, Error> where I: IntoIterator + Clone, I::Item: Borrow, diff --git a/src/raw_client.rs b/src/raw_client.rs index e278c84..df6db5a 100644 --- a/src/raw_client.rs +++ b/src/raw_client.rs @@ -17,7 +17,7 @@ use log::{debug, error, info, trace, warn}; use bitcoin::consensus::encode::deserialize; use bitcoin::hex::{DisplayHex, FromHex}; -use bitcoin::{Script, Txid}; +use bitcoin::{FeeRate, Script, Txid}; #[cfg(feature = "use-openssl")] use openssl::ssl::{SslConnector, SslMethod, SslStream, SslVerifyMode}; @@ -35,9 +35,11 @@ use rustls::{ pki_types::{Der, TrustAnchor}, ClientConfig, ClientConnection, RootCertStore, StreamOwned, }; +use serde_json::Value; #[cfg(any(feature = "default", feature = "proxy"))] use crate::socks::{Socks5Stream, TargetAddr, ToTargetAddr}; +use crate::utils::convert_fee_rate; use crate::stream::ClonableStream; @@ -69,6 +71,23 @@ macro_rules! impl_batch_call { Ok(answer) }}; + + ( $self:expr, $data:expr, $aux_call:expr, $call:ident, $($apply_deref:tt)? ) => {{ + let mut batch = Batch::default(); + for i in $data { + batch.$call($($apply_deref)* i.borrow()); + } + + let resp = $self.batch_call(&batch)?; + let mut answer = Vec::new(); + + for x in resp { + $aux_call(x); + answer.push(serde_json::from_value(x)?); + } + + Ok(answer) + }}; } /// A trait for [`ToSocketAddrs`](https://doc.rust-lang.org/std/net/trait.ToSocketAddrs.html) that @@ -857,7 +876,7 @@ impl ElectrumApi for RawClient { Ok(deserialized) } - fn estimate_fee(&self, number: usize) -> Result { + fn estimate_fee(&self, number: usize) -> Result { let req = Request::new_id( self.last_id.fetch_add(1, Ordering::SeqCst), "blockchain.estimatefee", @@ -867,10 +886,11 @@ impl ElectrumApi for RawClient { result .as_f64() - .ok_or_else(|| Error::InvalidResponse(result.clone())) + .map(|val| convert_fee_rate(Value::from(val))) + .expect("Invalid response") } - fn relay_fee(&self) -> Result { + fn relay_fee(&self) -> Result { let req = Request::new_id( self.last_id.fetch_add(1, Ordering::SeqCst), "blockchain.relayfee", @@ -880,7 +900,8 @@ impl ElectrumApi for RawClient { result .as_f64() - .ok_or_else(|| Error::InvalidResponse(result.clone())) + .map(|val| convert_fee_rate(Value::from(val))) + .expect("Invalid response") } fn script_subscribe(&self, script: &Script) -> Result, Error> { @@ -1061,12 +1082,12 @@ impl ElectrumApi for RawClient { .collect() } - fn batch_estimate_fee<'s, I>(&self, numbers: I) -> Result, Error> + fn batch_estimate_fee<'s, I>(&self, numbers: I) -> Result, Error> where I: IntoIterator + Clone, I::Item: Borrow, { - impl_batch_call!(self, numbers, estimate_fee, apply_deref) + impl_batch_call!(self, numbers, convert_fee_rate, estimate_fee, apply_deref) } fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result { @@ -1149,20 +1170,21 @@ mod test { assert_eq!(resp.hash_function, Some("sha256".into())); assert_eq!(resp.pruning, None); } + #[test] fn test_relay_fee() { let client = RawClient::new(get_test_server(), None).unwrap(); - let resp = client.relay_fee().unwrap(); - assert_eq!(resp, 0.00001); + let resp = client.relay_fee().unwrap().to_sat_per_vb_ceil(); + assert!(resp > 0); } #[test] fn test_estimate_fee() { let client = RawClient::new(get_test_server(), None).unwrap(); - let resp = client.estimate_fee(10).unwrap(); - assert!(resp > 0.0); + let resp = client.estimate_fee(10).unwrap().to_sat_per_vb_ceil(); + assert!(resp > 0); } #[test] @@ -1288,8 +1310,8 @@ mod test { let resp = client.batch_estimate_fee(vec![10, 20]).unwrap(); assert_eq!(resp.len(), 2); - assert!(resp[0] > 0.0); - assert!(resp[1] > 0.0); + assert!(resp[0].to_sat_per_vb_ceil() > 0); + assert!(resp[1].to_sat_per_vb_ceil() > 0); } #[test] diff --git a/src/types.rs b/src/types.rs index 84f9c94..ecb8ab0 100644 --- a/src/types.rs +++ b/src/types.rs @@ -309,7 +309,8 @@ pub enum Error { AllAttemptsErrored(Vec), /// There was an io error reading the socket, to be shared between threads SharedIOError(Arc), - + /// There was an error parsing a fee rate + FeeRate(String), /// Couldn't take a lock on the reader mutex. This means that there's already another reader /// thread running CouldntLockReader, @@ -365,6 +366,7 @@ impl Display for Error { Error::MissingDomain => f.write_str("Missing domain while it was explicitly asked to validate it"), Error::CouldntLockReader => f.write_str("Couldn't take a lock on the reader mutex. This means that there's already another reader thread is running"), Error::Mpsc => f.write_str("Broken IPC communication channel: the other thread probably has exited"), + Error::FeeRate(e) => f.write_str(e), } } } diff --git a/src/utils.rs b/src/utils.rs index fc02dfb..7ad466c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,12 @@ //! Utilities helping to handle Electrum-related data. use crate::types::GetMerkleRes; +use crate::Error; use bitcoin::hash_types::TxMerkleNode; use bitcoin::hashes::sha256d::Hash as Sha256d; use bitcoin::hashes::{Hash, HashEngine}; -use bitcoin::Txid; +use bitcoin::{Amount, FeeRate, Txid}; +use serde_json::Value; /// Verifies a Merkle inclusion proof as retrieved via [`transaction_get_merkle`] for a transaction with the /// given `txid` and `merkle_root` as included in the [`BlockHeader`]. @@ -41,3 +43,19 @@ pub fn validate_merkle_proof( cur == merkle_root.to_raw_hash() } + +/// Converts a fee rate in BTC/kB to sats/vbyte. +pub(crate) fn convert_fee_rate(fee_rate_kvb: Value) -> Result { + let fee_rate_kvb = match fee_rate_kvb.as_f64() { + Some(fee_rate_kvb) => fee_rate_kvb, + None => { + return Err(Error::FeeRate("Fee rate conversion failed".to_string())); + } + }; + let fee_rate_sat_vb = (Amount::ONE_BTC.to_sat() as f64) * fee_rate_kvb; + let fee_rate = FeeRate::from_sat_per_vb(fee_rate_sat_vb as u64); + match fee_rate { + Some(fee_rate) => Ok(fee_rate), + None => Err(Error::FeeRate("Fee rate conversion failed".to_string())), + } +}