From 08792b2fcde84b22b3673a81a075ef5fc375ae13 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Fri, 7 Aug 2020 11:23:01 +0200 Subject: [PATCH] [wallet] Add a type convert fee units, add `Wallet::estimate_fee()` --- src/blockchain/electrum.rs | 10 ++++++++ src/blockchain/esplora.rs | 35 +++++++++++++++++++++++++- src/blockchain/mod.rs | 2 ++ src/cli.rs | 4 +-- src/lib.rs | 4 ++- src/wallet/mod.rs | 11 +++++--- src/wallet/tx_builder.rs | 14 ++++------- src/wallet/utils.rs | 51 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 115 insertions(+), 16 deletions(-) diff --git a/src/blockchain/electrum.rs b/src/blockchain/electrum.rs index d0bc9710..5ac86151 100644 --- a/src/blockchain/electrum.rs +++ b/src/blockchain/electrum.rs @@ -11,6 +11,7 @@ use self::utils::{ELSGetHistoryRes, ELSListUnspentRes, ElectrumLikeSync}; use super::*; use crate::database::{BatchDatabase, DatabaseUtils}; use crate::error::Error; +use crate::FeeRate; pub struct ElectrumBlockchain(Option); @@ -77,6 +78,15 @@ impl OnlineBlockchain for ElectrumBlockchain { .block_headers_subscribe() .map(|data| data.height)?) } + + fn estimate_fee(&self, target: usize) -> Result { + Ok(FeeRate::from_btc_per_kvb( + self.0 + .as_ref() + .ok_or(Error::OfflineClient)? + .estimate_fee(target)? as f32, + )) + } } impl ElectrumLikeSync for Client { diff --git a/src/blockchain/esplora.rs b/src/blockchain/esplora.rs index f49fdcfd..53dcb5b3 100644 --- a/src/blockchain/esplora.rs +++ b/src/blockchain/esplora.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use futures::stream::{self, StreamExt, TryStreamExt}; @@ -18,6 +18,7 @@ use self::utils::{ELSGetHistoryRes, ELSListUnspentRes, ElectrumLikeSync}; use super::*; use crate::database::{BatchDatabase, DatabaseUtils}; use crate::error::Error; +use crate::FeeRate; #[derive(Debug)] pub struct UrlClient { @@ -99,6 +100,27 @@ impl OnlineBlockchain for EsploraBlockchain { .ok_or(Error::OfflineClient)? ._get_height())?) } + + fn estimate_fee(&self, target: usize) -> Result { + let estimates = await_or_block!(self + .0 + .as_ref() + .ok_or(Error::OfflineClient)? + ._get_fee_estimates())?; + + let fee_val = estimates + .into_iter() + .map(|(k, v)| Ok::<_, std::num::ParseIntError>((k.parse::()?, v))) + .collect::, _>>() + .map_err(|e| Error::Generic(e.to_string()))? + .into_iter() + .take_while(|(k, _)| k <= &target) + .map(|(_, v)| v) + .last() + .unwrap_or(1.0); + + Ok(FeeRate::from_sat_per_vb(fee_val as f32)) + } } impl UrlClient { @@ -232,6 +254,17 @@ impl UrlClient { }) .collect()) } + + async fn _get_fee_estimates(&self) -> Result, EsploraError> { + Ok(self + .client + .get(&format!("{}/api/fee-estimates", self.url,)) + .send() + .await? + .error_for_status()? + .json::>() + .await?) + } } #[maybe_async] diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index 2c7081ac..08e46694 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -5,6 +5,7 @@ use bitcoin::{Transaction, Txid}; use crate::database::{BatchDatabase, DatabaseUtils}; use crate::error::Error; +use crate::FeeRate; pub mod utils; @@ -64,6 +65,7 @@ pub trait OnlineBlockchain: Blockchain { fn broadcast(&self, tx: &Transaction) -> Result<(), Error>; fn get_height(&self) -> Result; + fn estimate_fee(&self, target: usize) -> Result; } pub type ProgressData = (f32, Option); diff --git a/src/cli.rs b/src/cli.rs index d4e45789..48f5e42b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -13,7 +13,7 @@ use bitcoin::{Address, OutPoint}; use crate::error::Error; use crate::types::ScriptType; -use crate::{TxBuilder, Wallet}; +use crate::{FeeRate, TxBuilder, Wallet}; fn parse_addressee(s: &str) -> Result<(Address, u64), String> { let parts: Vec<_> = s.split(":").collect(); @@ -331,7 +331,7 @@ where if let Some(fee_rate) = sub_matches.value_of("fee_rate") { let fee_rate = f32::from_str(fee_rate).map_err(|s| Error::Generic(s.to_string()))?; - tx_builder = tx_builder.fee_rate(fee_rate); + tx_builder = tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate)); } if let Some(utxos) = sub_matches.values_of("utxos") { let utxos = utxos diff --git a/src/lib.rs b/src/lib.rs index 0d5ce881..96dcc6bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,4 +42,6 @@ pub mod types; pub mod wallet; pub use descriptor::ExtendedDescriptor; -pub use wallet::{OfflineWallet, TxBuilder, Wallet}; +pub use wallet::tx_builder::TxBuilder; +pub use wallet::utils::FeeRate; +pub use wallet::{OfflineWallet, Wallet}; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 6dd5ad59..609f2f45 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -22,8 +22,8 @@ pub mod time; pub mod tx_builder; pub mod utils; -pub use tx_builder::TxBuilder; -use utils::IsDust; +use tx_builder::TxBuilder; +use utils::{FeeRate, IsDust}; use crate::blockchain::{noop_progress, Blockchain, OfflineBlockchain, OnlineBlockchain}; use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; @@ -142,7 +142,7 @@ where output: vec![], }; - let fee_rate = builder.fee_perkb.unwrap_or(1e3) * 100_000.0; + let fee_rate = builder.fee_rate.unwrap_or_default().as_sat_vb(); if builder.send_all && builder.addressees.len() != 1 { return Err(Error::SendAllMultipleOutputs); } @@ -759,6 +759,11 @@ where Ok(tx.txid()) } + + #[maybe_async] + pub fn estimate_fee(&self, target: usize) -> Result { + Ok(maybe_await!(self.client.estimate_fee(target))?) + } } #[cfg(test)] diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index dab488fe..b644660a 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -3,13 +3,14 @@ use std::collections::BTreeMap; use bitcoin::{Address, OutPoint, SigHashType}; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; +use super::utils::FeeRate; // TODO: add a flag to ignore change outputs (make them unspendable) #[derive(Debug, Default)] pub struct TxBuilder { pub(crate) addressees: Vec<(Address, u64)>, pub(crate) send_all: bool, - pub(crate) fee_perkb: Option, + pub(crate) fee_rate: Option, pub(crate) policy_path: Option>>, pub(crate) utxos: Option>, pub(crate) unspendable: Option>, @@ -44,13 +45,8 @@ impl TxBuilder { self } - pub fn fee_rate(mut self, satoshi_per_vbyte: f32) -> Self { - self.fee_perkb = Some(satoshi_per_vbyte * 1e3); - self - } - - pub fn fee_rate_perkb(mut self, satoshi_per_kb: f32) -> Self { - self.fee_perkb = Some(satoshi_per_kb); + pub fn fee_rate(mut self, fee_rate: FeeRate) -> Self { + self.fee_rate = Some(fee_rate); self } @@ -93,7 +89,7 @@ impl TxBuilder { TxBuilder { addressees: self.addressees, send_all: self.send_all, - fee_perkb: self.fee_perkb, + fee_rate: self.fee_rate, policy_path: self.policy_path, utxos: self.utxos, unspendable: self.unspendable, diff --git a/src/wallet/utils.rs b/src/wallet/utils.rs index 0b969b44..465036f0 100644 --- a/src/wallet/utils.rs +++ b/src/wallet/utils.rs @@ -14,6 +14,34 @@ impl IsDust for u64 { } } +#[derive(Debug, Copy, Clone)] +// Internally stored as satoshi/vbyte +pub struct FeeRate(f32); + +impl FeeRate { + pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self { + FeeRate(btc_per_kvb * 1e5) + } + + pub fn from_sat_per_vb(sat_per_vb: f32) -> Self { + FeeRate(sat_per_vb) + } + + pub fn default_min_relay_fee() -> Self { + FeeRate(1.0) + } + + pub fn as_sat_vb(&self) -> f32 { + self.0 + } +} + +impl std::default::Default for FeeRate { + fn default() -> Self { + FeeRate::default_min_relay_fee() + } +} + pub struct ChunksIterator { iter: I, size: usize, @@ -46,3 +74,26 @@ impl Iterator for ChunksIterator { Some(v) } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_fee_from_btc_per_kb() { + let fee = FeeRate::from_btc_per_kvb(1e-5); + assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001); + } + + #[test] + fn test_fee_from_sats_vbyte() { + let fee = FeeRate::from_sat_per_vb(1.0); + assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001); + } + + #[test] + fn test_fee_default_min_relay_fee() { + let fee = FeeRate::default_min_relay_fee(); + assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001); + } +}