Skip to content

Commit 653d161

Browse files
authored
Merge pull request #571 from bob-collective/feat/bob-utils-rbf
feat: Add fee rate handling and bump fee functionality
2 parents 51a5610 + b941c3d commit 653d161

File tree

2 files changed

+200
-1
lines changed

2 files changed

+200
-1
lines changed

crates/utils/src/bitcoin_client.rs

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ use bitcoin::{
99
};
1010
use bitcoincore_rpc::{
1111
bitcoin::{BlockHash, Transaction},
12-
json::TestMempoolAcceptResult,
12+
json::{EstimateMode, TestMempoolAcceptResult},
1313
jsonrpc::{error::RpcError, Error as JsonRpcError},
1414
Auth, Client, Error as BitcoinError, RpcApi,
1515
};
1616
use num_derive::FromPrimitive;
17+
use serde::{Deserialize, Serialize};
1718
use serde_json::error::Category as SerdeJsonCategory;
1819
use std::{sync::Arc, time::Duration};
1920
use tokio::time::{error::Elapsed, sleep, timeout};
@@ -85,6 +86,101 @@ pub enum BitcoinRpcError {
8586
RpcUnknownError = 0,
8687
}
8788

89+
/// A representation of a fee rate. Bitcoin Core uses different units in different
90+
/// versions. To avoid burdening the user with using the correct unit, this struct
91+
/// provides an umambiguous way to represent the fee rate, and the lib will perform
92+
/// the necessary conversions.
93+
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
94+
pub struct FeeRate(Amount);
95+
96+
impl FeeRate {
97+
/// Construct FeeRate from the amount per vbyte
98+
pub fn per_vbyte(amount_per_vbyte: Amount) -> Self {
99+
// internal representation is amount per vbyte
100+
Self(amount_per_vbyte)
101+
}
102+
103+
/// Construct FeeRate from the amount per kilo-vbyte
104+
pub fn per_kvbyte(amount_per_kvbyte: Amount) -> Self {
105+
// internal representation is amount per vbyte, so divide by 1000
106+
Self::per_vbyte(amount_per_kvbyte / 1000)
107+
}
108+
109+
pub fn to_sat_per_vbyte(&self) -> f64 {
110+
// multiply by the number of decimals to get sat
111+
self.0.to_sat() as f64 // TODO: Changed this
112+
}
113+
114+
pub fn to_btc_per_kvbyte(&self) -> f64 {
115+
// divide by 10^8 to get btc/vbyte, then multiply by 10^3 to get btc/kbyte
116+
self.0.to_sat() as f64 / 100_000.0
117+
}
118+
}
119+
120+
#[derive(Clone, PartialEq, Eq, Debug, Default)]
121+
pub struct BumpFeeOptions {
122+
/// Confirmation target in blocks.
123+
pub conf_target: Option<u16>,
124+
/// Specify a fee rate instead of relying on the built-in fee estimator.
125+
pub fee_rate: Option<FeeRate>,
126+
/// Whether this transaction could be replaced due to BIP125 (replace-by-fee)
127+
pub replaceable: Option<bool>,
128+
/// The fee estimate mode
129+
pub estimate_mode: Option<EstimateMode>,
130+
}
131+
132+
#[derive(Serialize, Clone, PartialEq, Debug, Default)]
133+
#[serde(rename_all = "camelCase")]
134+
pub struct SerializableBumpFeeOptions {
135+
#[serde(rename = "conf_target", skip_serializing_if = "Option::is_none")]
136+
/// Confirmation target in blocks.
137+
pub conf_target: Option<u16>,
138+
/// Specify a fee rate instead of relying on the built-in fee estimator.
139+
#[serde(rename = "fee_rate")]
140+
pub fee_rate: Option<f64>,
141+
/// Whether this transaction could be replaced due to BIP125 (replace-by-fee)
142+
#[serde(skip_serializing_if = "Option::is_none")]
143+
pub replaceable: Option<bool>,
144+
/// The fee estimate mode
145+
#[serde(rename = "estimate_mode", skip_serializing_if = "Option::is_none")]
146+
pub estimate_mode: Option<EstimateMode>,
147+
}
148+
149+
impl BumpFeeOptions {
150+
pub fn to_serializable(&self, version: usize) -> SerializableBumpFeeOptions {
151+
let fee_rate = self.fee_rate.map(|x| {
152+
if version < 210000 {
153+
x.to_btc_per_kvbyte()
154+
} else {
155+
x.to_sat_per_vbyte()
156+
}
157+
});
158+
159+
SerializableBumpFeeOptions {
160+
fee_rate,
161+
conf_target: self.conf_target,
162+
replaceable: self.replaceable,
163+
estimate_mode: self.estimate_mode,
164+
}
165+
}
166+
}
167+
168+
#[derive(Deserialize, Clone, Debug)]
169+
#[serde(rename_all = "camelCase")]
170+
pub struct BumpFeeResult {
171+
/// The base64-encoded unsigned PSBT of the new transaction. Only returned when wallet private
172+
/// keys are disabled.
173+
pub psbt: Option<String>,
174+
/// The id of the new transaction. Only returned when wallet private keys are enabled.
175+
pub txid: Option<bitcoin::Txid>,
176+
/// The fee of the original transaction (before bumping), denominated in BTC.
177+
pub origfee: f64,
178+
/// The fee of the newly created bumped transaction, denominated in BTC.
179+
pub fee: f64,
180+
/// Errors encountered during processing.
181+
pub errors: Vec<String>,
182+
}
183+
88184
impl From<RpcError> for BitcoinRpcError {
89185
fn from(err: RpcError) -> Self {
90186
match num::FromPrimitive::from_i32(err.code) {
@@ -323,6 +419,41 @@ impl BitcoinClient {
323419
let transaction = signed_tx.transaction()?;
324420
Ok(transaction)
325421
}
422+
423+
pub fn bump_fee(
424+
&self,
425+
txid: &Txid,
426+
options: Option<&BumpFeeOptions>,
427+
) -> Result<BumpFeeResult, Error> {
428+
// Serialize options if provided
429+
let opts = match options {
430+
Some(options) => Some(options.to_serializable(self.rpc.version()?)),
431+
None => None,
432+
};
433+
434+
// Prepare arguments
435+
let args = vec![serde_json::to_value(txid)?, serde_json::to_value(opts)?];
436+
437+
// Call the "bumpfee" RPC method with borrowed args
438+
let result = self.rpc.call("bumpfee", &args);
439+
440+
// Handle the result
441+
match result {
442+
Ok(result_value) => {
443+
// Try to deserialize the result into the expected BumpFeeResult
444+
let result: Result<BumpFeeResult, _> = serde_json::from_value(result_value);
445+
446+
match result {
447+
Ok(bump_fee_result) => Ok(bump_fee_result),
448+
Err(err) => {
449+
println!("Failed to deserialize into BumpFeeResult");
450+
Err(err.into())
451+
}
452+
}
453+
}
454+
Err(err) => Err(err.into()), // Handle the case where the RPC call fails
455+
}
456+
}
326457
}
327458

328459
fn merklize(left: Sha256dHash, right: Sha256dHash) -> Sha256dHash {

crates/utils/src/bitcoin_core.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ impl BitcoinCore {
221221
#[cfg(test)]
222222
mod tests {
223223
use super::*;
224+
use crate::{BitcoinClient, BumpFeeOptions};
225+
use bitcoin::Amount;
224226

225227
#[test]
226228
fn can_launch_bitcoin() {
@@ -237,4 +239,70 @@ mod tests {
237239
5000000000
238240
);
239241
}
242+
243+
#[tokio::test]
244+
async fn test_bump_fee() -> Result<()> {
245+
// Step 1: Create and initialize BitcoinCore instance for test
246+
let bitcoin = BitcoinCore::new().spawn();
247+
248+
// Fund Alice's wallet
249+
bitcoin.fund_wallet("Alice").expect("Should fund Alice");
250+
251+
// Fund Bob's wallet
252+
bitcoin.fund_wallet("Bob").expect("Should fund Alice");
253+
254+
// Check that Bob's balance is 5000000000 satoshis (i.e., 5 BTC)
255+
assert_eq!(
256+
bitcoin.client(Some("Bob")).unwrap().get_balance(None, None).unwrap().to_sat(),
257+
5000000000
258+
);
259+
260+
// Initialize BitcoinClient for Alice (make sure Alice's wallet is used)
261+
let bitcoin_client = BitcoinClient::from(bitcoin.client(Some("Alice"))?);
262+
263+
// Step 2: Send to yourself with very low fee (simulate low fee transaction)
264+
let to_addr = bitcoin_client.rpc.get_new_address(None, None).unwrap().assume_checked();
265+
266+
// Set the amount to send (you can adjust the amount as needed)
267+
let amount = Amount::from_sat(100_000); // 0.001 BTC (adjust as necessary)
268+
269+
// Send the transaction to yourself (low fee expected)
270+
let txid = bitcoin_client
271+
.rpc
272+
.send_to_address(&to_addr, amount, None, None, None, Some(true), None, None)
273+
.unwrap();
274+
275+
// Step 3: Bump the fee for the low-fee transaction by calling bump_fee
276+
let bump_fee = bitcoin_client
277+
.bump_fee(
278+
&txid,
279+
Some(&BumpFeeOptions {
280+
conf_target: None,
281+
fee_rate: None,
282+
replaceable: Some(true), // Allow the transaction to be replaceable
283+
estimate_mode: None,
284+
}),
285+
)
286+
.unwrap();
287+
288+
// Assert there are no errors when bumping the fee
289+
assert!(bump_fee.errors.is_empty());
290+
291+
// Step 4: Generate 100 blocks to confirm the bump fee transaction
292+
bitcoin_client.rpc.generate_to_address(100, &to_addr).unwrap();
293+
294+
// Check the original transaction
295+
let tx_info = bitcoin_client.rpc.get_transaction(&txid, None).unwrap();
296+
297+
// Assert that the original transaction has negative confirmations
298+
assert!(tx_info.info.confirmations.is_negative());
299+
300+
// Get the bumped fee transaction's
301+
let tx_info = bitcoin_client.rpc.get_transaction(&bump_fee.txid.unwrap(), None).unwrap();
302+
303+
// Assert that the bumped fee transaction has confirmations
304+
assert!(tx_info.info.confirmations.is_positive());
305+
306+
Ok(())
307+
}
240308
}

0 commit comments

Comments
 (0)