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")]