From e4a5f71cda006aae79c687b93eed2b6776cf892e Mon Sep 17 00:00:00 2001 From: Sander Bosma Date: Fri, 10 Jun 2022 13:31:19 +0200 Subject: [PATCH 1/3] feat: added bumpfee rpc --- client/src/client.rs | 13 ++++++ integration_test/src/main.rs | 22 ++++++++++ json/src/lib.rs | 78 ++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/client/src/client.rs b/client/src/client.rs index 57f70678..4129d3a9 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -260,6 +260,19 @@ pub trait RpcApi: Sized { self.call("addmultisigaddress", handle_defaults(&mut args, &[into_json("")?, null()])) } + fn bump_fee( + &self, + txid: &bitcoin::Txid, + options: Option<&json::BumpFeeOptions>, + ) -> Result { + let opts = match options { + Some(options) => Some(options.to_serializable(self.version()?)), + None => None, + }; + let mut args = [into_json(txid)?, opt_into_json(opts)?]; + self.call("bumpfee", handle_defaults(&mut args, &[null()])) + } + fn load_wallet(&self, wallet: &str) -> Result { self.call("loadwallet", &[wallet.into()]) } diff --git a/integration_test/src/main.rs b/integration_test/src/main.rs index a8b7a33e..d2e6cf9f 100644 --- a/integration_test/src/main.rs +++ b/integration_test/src/main.rs @@ -172,6 +172,7 @@ fn main() { test_key_pool_refill(&cl); test_create_raw_transaction(&cl); test_fund_raw_transaction(&cl); + test_bump_fee(&cl); test_test_mempool_accept(&cl); test_wallet_create_funded_psbt(&cl); test_wallet_process_psbt(&cl); @@ -676,6 +677,27 @@ fn test_fund_raw_transaction(cl: &Client) { let _ = funded.transaction().unwrap(); } +fn test_bump_fee(cl: &Client) { + let addr = cl.get_new_address(None, None).unwrap(); + let txid = cl.send_to_address(&addr, btc(1), None, None, None, Some(true), None, None).unwrap(); + + // bump without explicit fee rate + let bump_fee_result_1 = cl.bump_fee(&txid, None).unwrap(); + assert!(bump_fee_result_1.origfee < bump_fee_result_1.fee); + + // bump with explicit fee rate + let amount_per_vbyte = Amount::from_sat(500); + let new_fee_rate = json::FeeRate::new(amount_per_vbyte); + let options = json::BumpFeeOptions { + fee_rate: Some(new_fee_rate), + replaceable: Some(true), + ..Default::default() + }; + let bump_fee_result_2 = cl.bump_fee(&bump_fee_result_1.txid.unwrap(), Some(&options)).unwrap(); + let vbytes = cl.get_mempool_entry(&bump_fee_result_2.txid.unwrap()).unwrap().vsize; + assert_eq!(bump_fee_result_2.fee, amount_per_vbyte * vbytes); +} + fn test_test_mempool_accept(cl: &Client) { let options = json::ListUnspentQueryOptions { minimum_amount: Some(btc(2)), diff --git a/json/src/lib.rs b/json/src/lib.rs index 58e8933c..fb7be174 100644 --- a/json/src/lib.rs +++ b/json/src/lib.rs @@ -1812,6 +1812,84 @@ pub struct FundRawTransactionResult { pub change_position: i32, } +#[derive(Clone, PartialEq, Eq, Debug, Default)] +pub struct BumpFeeOptions { + pub conf_target: Option, + /// Specify a fee rate instead of relying on the built-in fee estimator. + pub fee_rate: Option, + /// Whether this transaction could be replaced due to BIP125 (replace-by-fee) + pub replaceable: Option, + /// The fee estimate mode + pub estimate_mode: Option, +} + +impl BumpFeeOptions { + pub fn to_serializable(&self, version: usize) -> SerializableBumpFeeOptions { + let fee_rate = self.fee_rate.map(|x| { + if version < 210000 { + x.btc_per_kvbyte() + } else { + x.sat_per_vbyte() + } + }); + + SerializableBumpFeeOptions { + fee_rate, + conf_target: self.conf_target, + replaceable: self.replaceable, + estimate_mode: self.estimate_mode, + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +pub struct FeeRate(Amount); + +impl FeeRate { + pub fn new(amount_per_vbyte: Amount) -> Self { + Self(amount_per_vbyte) + } + pub fn sat_per_vbyte(&self) -> f64 { + // multiply by the number of decimals to get sat + self.0.as_sat() as f64 + } + pub fn btc_per_kvbyte(&self) -> f64 { + // divide by 10^8 to get btc/vbyte, then multiply by 10^3 to get btc/kbyte + self.0.as_sat() as f64 / 100_000.0 + } +} + +#[derive(Serialize, Clone, PartialEq, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct SerializableBumpFeeOptions { + #[serde(rename = "conf_target", skip_serializing_if = "Option::is_none")] + pub conf_target: Option, + /// Specify a fee rate instead of relying on the built-in fee estimator. + #[serde(rename = "fee_rate")] + pub fee_rate: Option, + /// Whether this transaction could be replaced due to BIP125 (replace-by-fee) + #[serde(skip_serializing_if = "Option::is_none")] + pub replaceable: Option, + /// The fee estimate mode + #[serde(rename = "estimate_mode", skip_serializing_if = "Option::is_none")] + pub estimate_mode: Option, +} + +#[derive(Deserialize, Clone, PartialEq, Eq, Debug)] +#[serde(rename_all = "camelCase")] +pub struct BumpFeeResult { + /// The base64-encoded unsigned PSBT of the new transaction. Only returned when wallet private keys are disabled. + pub psbt: Option, + /// The id of the new transaction. Only returned when wallet private keys are enabled. + pub txid: Option, + #[serde(with = "bitcoin::util::amount::serde::as_btc")] + pub origfee: Amount, + #[serde(with = "bitcoin::util::amount::serde::as_btc")] + pub fee: Amount, + /// Errors encountered during processing. + pub errors: Vec, +} + #[derive(Deserialize, Clone, PartialEq, Eq, Debug)] pub struct GetBalancesResultEntry { #[serde(with = "bitcoin::util::amount::serde::as_btc")] From ad5535d391afb0ccea1b5c2f5b53aadf11bbac26 Mon Sep 17 00:00:00 2001 From: Sander Bosma Date: Fri, 19 Aug 2022 10:12:29 +0200 Subject: [PATCH 2/3] move, document and test the FeeRate struct --- integration_test/src/main.rs | 2 +- json/src/lib.rs | 67 ++++++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/integration_test/src/main.rs b/integration_test/src/main.rs index d2e6cf9f..91cafef6 100644 --- a/integration_test/src/main.rs +++ b/integration_test/src/main.rs @@ -687,7 +687,7 @@ fn test_bump_fee(cl: &Client) { // bump with explicit fee rate let amount_per_vbyte = Amount::from_sat(500); - let new_fee_rate = json::FeeRate::new(amount_per_vbyte); + let new_fee_rate = json::FeeRate::per_vbyte(amount_per_vbyte); let options = json::BumpFeeOptions { fee_rate: Some(new_fee_rate), replaceable: Some(true), diff --git a/json/src/lib.rs b/json/src/lib.rs index fb7be174..530d3884 100644 --- a/json/src/lib.rs +++ b/json/src/lib.rs @@ -35,6 +35,37 @@ use std::fmt; //TODO(stevenroose) consider using a Time type +/// A representation of a fee rate. Bitcoin Core uses different units in different +/// versions. To avoid burdening the user with using the correct unit, this struct +/// provides an umambiguous way to represent the fee rate, and the lib will perform +/// the necessary conversions. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +pub struct FeeRate(Amount); + +impl FeeRate { + /// Construct FeeRate from the amount per vbyte + pub fn per_vbyte(amount_per_vbyte: Amount) -> Self { + // internal representation is amount per vbyte + Self(amount_per_vbyte) + } + + /// Construct FeeRate from the amount per kilo-vbyte + pub fn per_kvbyte(amount_per_kvbyte: Amount) -> Self { + // internal representation is amount per vbyte, so divide by 1000 + Self::per_vbyte(amount_per_kvbyte / 1000) + } + + pub fn as_sat_per_vbyte(&self) -> f64 { + // multiply by the number of decimals to get sat + self.0.as_sat() as f64 + } + + pub fn as_btc_per_kvbyte(&self) -> f64 { + // divide by 10^8 to get btc/vbyte, then multiply by 10^3 to get btc/kbyte + self.0.as_sat() as f64 / 100_000.0 + } +} + /// A module used for serde serialization of bytes in hexadecimal format. /// /// The module is compatible with the serde attribute. @@ -1827,9 +1858,9 @@ impl BumpFeeOptions { pub fn to_serializable(&self, version: usize) -> SerializableBumpFeeOptions { let fee_rate = self.fee_rate.map(|x| { if version < 210000 { - x.btc_per_kvbyte() + x.as_btc_per_kvbyte() } else { - x.sat_per_vbyte() + x.as_sat_per_vbyte() } }); @@ -1842,23 +1873,6 @@ impl BumpFeeOptions { } } -#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] -pub struct FeeRate(Amount); - -impl FeeRate { - pub fn new(amount_per_vbyte: Amount) -> Self { - Self(amount_per_vbyte) - } - pub fn sat_per_vbyte(&self) -> f64 { - // multiply by the number of decimals to get sat - self.0.as_sat() as f64 - } - pub fn btc_per_kvbyte(&self) -> f64 { - // divide by 10^8 to get btc/vbyte, then multiply by 10^3 to get btc/kbyte - self.0.as_sat() as f64 / 100_000.0 - } -} - #[derive(Serialize, Clone, PartialEq, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct SerializableBumpFeeOptions { @@ -2072,3 +2086,18 @@ where } Ok(Some(res)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fee_rate_conversion() { + let rate_1 = FeeRate::per_kvbyte(Amount::from_sat(10_000)); + let rate_2 = FeeRate::per_vbyte(Amount::from_sat(10)); + assert_eq!(rate_1, rate_2); + + assert_eq!(rate_1.as_sat_per_vbyte(), 10.0); + assert_eq!(rate_1.as_btc_per_kvbyte(), 10.0 * 1e3 / 1e8); + } +} From aabe2fb03a3b62df6232e3578e32ec558dcb478a Mon Sep 17 00:00:00 2001 From: Sander Bosma Date: Tue, 30 Aug 2022 08:32:05 +0200 Subject: [PATCH 3/3] renaming & minor cleanup --- json/src/lib.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/json/src/lib.rs b/json/src/lib.rs index 530d3884..f27cb98d 100644 --- a/json/src/lib.rs +++ b/json/src/lib.rs @@ -55,12 +55,12 @@ impl FeeRate { Self::per_vbyte(amount_per_kvbyte / 1000) } - pub fn as_sat_per_vbyte(&self) -> f64 { + pub fn to_sat_per_vbyte(&self) -> f64 { // multiply by the number of decimals to get sat self.0.as_sat() as f64 } - pub fn as_btc_per_kvbyte(&self) -> f64 { + pub fn to_btc_per_kvbyte(&self) -> f64 { // divide by 10^8 to get btc/vbyte, then multiply by 10^3 to get btc/kbyte self.0.as_sat() as f64 / 100_000.0 } @@ -1845,6 +1845,7 @@ pub struct FundRawTransactionResult { #[derive(Clone, PartialEq, Eq, Debug, Default)] pub struct BumpFeeOptions { + /// Confirmation target in blocks. pub conf_target: Option, /// Specify a fee rate instead of relying on the built-in fee estimator. pub fee_rate: Option, @@ -1858,9 +1859,9 @@ impl BumpFeeOptions { pub fn to_serializable(&self, version: usize) -> SerializableBumpFeeOptions { let fee_rate = self.fee_rate.map(|x| { if version < 210000 { - x.as_btc_per_kvbyte() + x.to_btc_per_kvbyte() } else { - x.as_sat_per_vbyte() + x.to_sat_per_vbyte() } }); @@ -1877,6 +1878,7 @@ impl BumpFeeOptions { #[serde(rename_all = "camelCase")] pub struct SerializableBumpFeeOptions { #[serde(rename = "conf_target", skip_serializing_if = "Option::is_none")] + /// Confirmation target in blocks. pub conf_target: Option, /// Specify a fee rate instead of relying on the built-in fee estimator. #[serde(rename = "fee_rate")] @@ -2097,7 +2099,8 @@ mod tests { let rate_2 = FeeRate::per_vbyte(Amount::from_sat(10)); assert_eq!(rate_1, rate_2); - assert_eq!(rate_1.as_sat_per_vbyte(), 10.0); - assert_eq!(rate_1.as_btc_per_kvbyte(), 10.0 * 1e3 / 1e8); + assert_eq!(rate_1.to_sat_per_vbyte(), 10.0); + // multiply 10.0 by 1e3 to get sat/kvbyte, then divide by 1e8 to get btc/kvbyte + assert_eq!(rate_1.to_btc_per_kvbyte(), 0.0001); } }