Skip to content

feat: Add fee rate handling and bump fee functionality #571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 23, 2025

Conversation

nakul1010
Copy link
Contributor

@nakul1010 nakul1010 commented Apr 18, 2025

Summary by CodeRabbit

  • New Features
    • Added support for fee bumping transactions, allowing users to increase the fee of an existing Bitcoin transaction.
    • Introduced options to customize fee bumping, including confirmation target, fee rate, replaceability, and estimate mode.
    • Users can now view detailed results of fee bumping actions, including new transaction IDs, updated fees, and any errors.
    • Added an automated test to verify fee bumping functionality and transaction confirmation behavior.

@nakul1010 nakul1010 self-assigned this Apr 18, 2025
Copy link

vercel bot commented Apr 18, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
bob-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Apr 21, 2025 7:19am

Copy link
Contributor

coderabbitai bot commented Apr 18, 2025

Walkthrough

This change introduces new abstractions and methods to support fee bumping functionality in a Bitcoin Core RPC client. It adds a FeeRate struct for representing and converting fee rates, a BumpFeeOptions struct for specifying bump fee parameters, and a BumpFeeResult struct for handling responses from the Bitcoin Core bumpfee RPC call. The BitcoinClient receives a new private method to perform fee bumping, which serializes options according to the Bitcoin Core version and processes the response. Additionally, an asynchronous integration test verifies the fee bumping workflow using the BitcoinCore and BitcoinClient instances. All changes are additive and do not modify existing methods.

Changes

File(s) Change Summary
crates/utils/src/bitcoin_client.rs Added FeeRate, BumpFeeOptions, SerializableBumpFeeOptions, and BumpFeeResult structs; implemented related methods; added a private bump_fee method to BitcoinClient for handling fee bumping via RPC. Updated imports for serialization and deserialization.
crates/utils/src/bitcoin_core.rs Added new asynchronous test function test_bump_fee that exercises fee bumping by creating wallets, sending a low-fee transaction, bumping its fee, and verifying confirmations.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Test (caller)
    participant BitcoinClient
    participant BitcoinCoreRPC

    Test->>BitcoinClient: bump_fee(txid, options)
    BitcoinClient->>BitcoinClient: Serialize options (version-aware)
    BitcoinClient->>BitcoinCoreRPC: Call "bumpfee" with txid and options
    BitcoinCoreRPC-->>BitcoinClient: Return bumpfee result (JSON)
    BitcoinClient->>BitcoinClient: Deserialize result into BumpFeeResult
    BitcoinClient-->>Test: Return BumpFeeResult
Loading

Poem

A hop and a skip, a fee we must lift,
With structs and with options, we give it a shift.
From vbyte to kilobyte, conversions are neat,
The bumpfee now dances, no sign of defeat.
With rabbits and code, the journey’s begun—
More satoshis per hop, and the work is well done!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
crates/utils/src/bitcoin_client.rs (3)

89-118: Consider resolving TODO comment and clarifying the constants.

The FeeRate implementation is well-structured with clear unit conversions, but:

  1. There's an unresolved TODO comment on line 111 which should be addressed before merging: // TODO: Changed this
  2. The constant 100_000.0 in to_btc_per_kvbyte() could benefit from a named constant or clearer comments explaining the calculation (10^8 for sat-to-btc divided by 10^3 for vbyte-to-kvbyte)
-    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.to_sat() as f64 / 100_000.0
-    }
+    pub fn to_btc_per_kvbyte(&self) -> f64 {
+        // Convert from satoshis/vbyte to BTC/kvbyte:
+        // 1. satoshis → BTC: divide by 10^8 
+        // 2. vbyte → kvbyte: multiply by 10^3
+        // Combined: divide by 10^5
+        const SAT_TO_BTC_PER_KVBYTE_DIVISOR: f64 = 100_000.0; // 10^8 / 10^3
+        self.0.to_sat() as f64 / SAT_TO_BTC_PER_KVBYTE_DIVISOR
+    }

149-166: Extract version constant for better code readability.

The version check uses a magic number 210000 which makes the code less maintainable. Consider extracting this to a named constant with a comment explaining that it refers to Bitcoin Core version 0.21.0.

+// Version at which Bitcoin Core switched from BTC/kvB to sat/vB fee rate units
+const BITCOIN_CORE_SAT_VB_VERSION: usize = 210000; // Version 0.21.0

 impl BumpFeeOptions {
     pub fn to_serializable(&self, version: usize) -> SerializableBumpFeeOptions {
         let fee_rate = self.fee_rate.map(|x| {
-            if version < 210000 {
+            if version < BITCOIN_CORE_SAT_VB_VERSION {
                 x.to_btc_per_kvbyte()
             } else {
                 x.to_sat_per_vbyte()
             }
         });

132-147: Consider adding validation for overlapping options.

The SerializableBumpFeeOptions struct looks good, but you might want to add validation to ensure that users don't provide both fee_rate and conf_target simultaneously, as they could conflict.

Consider adding a validation method:

impl SerializableBumpFeeOptions {
    pub fn validate(&self) -> Result<(), &'static str> {
        if self.fee_rate.is_some() && self.conf_target.is_some() {
            return Err("Cannot specify both fee_rate and conf_target");
        }
        Ok(())
    }
}

Then call this in the to_serializable method before returning.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1c8bf9 and 668f637.

📒 Files selected for processing (1)
  • crates/utils/src/bitcoin_client.rs (3 hunks)
🔇 Additional comments (2)
crates/utils/src/bitcoin_client.rs (2)

168-180: LGTM: Well-structured response type.

The BumpFeeResult struct is well-designed with proper field documentation and appropriate serialization attributes.


17-18: LGTM: Appropriate imports added.

The additions of serde imports and the json macro are appropriate for the new functionality.

@nakul1010 nakul1010 changed the title (WIP) feat: Add fee rate handling and bump fee functionality feat: Add fee rate handling and bump fee functionality Apr 21, 2025
@nakul1010 nakul1010 marked this pull request as ready for review April 21, 2025 12:16
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
crates/utils/src/bitcoin_core.rs (1)

285-299: Generating 100 blocks is unnecessarily slow – a single block is sufficient

For the assertions you make, one block (or even zero) is enough to:

  1. Evict the original tx from the mempool (confirmations == -1),
  2. Confirm the replacement tx (confirmations == 1).

Mining 100 blocks adds ±2 s to the test on a fast machine and much more on slower CI runners.

- bitcoin_client.rpc.generate_to_address(100, &to_addr).unwrap();
+ bitcoin_client.rpc.generate_to_address(1, &to_addr).unwrap();

This speeds up the suite and makes intermittent time‑outs far less likely.

crates/utils/src/bitcoin_client.rs (1)

423-456: Error handling leaks via println!, bypassing tracing and breaks library ergonomics

Inside a library call we should never print to stdout:

println!("Failed to deserialize into BumpFeeResult");

Replace with the structured logging already in use elsewhere:

- println!("Failed to deserialize into BumpFeeResult");
+ error!("Failed to deserialize `bumpfee` result");

or propagate a descriptive error:

Err(Error::SerdeError(err))

This keeps consumer logs clean and respects the caller’s chosen logging
configuration.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 668f637 and b941c3d.

📒 Files selected for processing (2)
  • crates/utils/src/bitcoin_client.rs (3 hunks)
  • crates/utils/src/bitcoin_core.rs (2 hunks)
🔇 Additional comments (1)
crates/utils/src/bitcoin_core.rs (1)

291-294: is_negative()/is_positive() is fragile – assert the exact expected value

Depending on mempool eviction policy and timing there are cases where the
original transaction simply disappears and the RPC returns an error rather
than a negative confirmation count. Prefer explicit pattern matching so the
test fails with a clear message instead of panicking inside the RPC layer.

- assert!(tx_info.info.confirmations.is_negative());
+ assert_eq!(tx_info.info.confirmations, -1, "original tx should be replaced");

Same comment applies to the is_positive() assertion below.

Comment on lines +237 to +241
#[tokio::test]
async fn test_bump_fee() -> Result<()> {
// Step 1: Create and initialize BitcoinCore instance for test
let bitcoin = BitcoinCore::new().spawn();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Blocking synchronous RPC calls inside #[tokio::test] can stall the async runtime

bitcoin_client.rpc is a blocking client. Calling it directly from an async test runs the blocking code on a core Tokio worker thread, potentially starving the scheduler (especially when more async tests are run concurrently).

let to_addr = bitcoin_client.rpc.get_new_address(/* … */).unwrap();

Consider off‑loading these calls:

- let to_addr = bitcoin_client.rpc.get_new_address(None, None).unwrap().assume_checked();
+ let to_addr = tokio::task::spawn_blocking(move || {
+     bitcoin_client
+         .rpc
+         .get_new_address(None, None)
+         .map(|a| a.assume_checked())
+ })
+ .await
+ .expect("task panicked")?;

(or wrap the client in tokio::task::block_in_place).
Doing so keeps the async test cooperative and avoids hidden time‑outs on CI.

Comment on lines +89 to +118
/// 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 to_sat_per_vbyte(&self) -> f64 {
// multiply by the number of decimals to get sat
self.0.to_sat() as f64 // TODO: Changed this
}

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.to_sat() as f64 / 100_000.0
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

to_sat_per_vbyte / to_btc_per_kvbyte: misleading comments and loss of precision

  1. The comment “multiply by the number of decimals to get sat” no longer
    matches the implementation – nothing is multiplied.
  2. Returning f64 while the input is discrete satoshis discards information
    about sub‑sat/vB values before they reach the JSON layer. Bitcoin Core
    accepts sub‑sat values (e.g. 0.75) in bumpfee options.
-    pub fn to_sat_per_vbyte(&self) -> f64 {
-        // multiply by the number of decimals to get sat
-        self.0.to_sat() as f64 // TODO: Changed this
+    pub fn to_sat_per_vbyte(&self) -> f64 {
+        self.0.to_sat_f64()
     }

You can add

trait ToSatF64 {
    fn to_sat_f64(self) -> f64;
}
impl ToSatF64 for Amount {
    fn to_sat_f64(self) -> f64 {
        self.to_sat() as f64 / 1.0 // placeholder – implement once upstream exposes sub‑sat support
    }
}

to preserve eventual sub‑sat support while keeping the public API stable.

Comment on lines +149 to +166
impl BumpFeeOptions {
pub fn to_serializable(&self, version: usize) -> SerializableBumpFeeOptions {
let fee_rate = self.fee_rate.map(|x| {
if version < 210000 {
x.to_btc_per_kvbyte()
} else {
x.to_sat_per_vbyte()
}
});

SerializableBumpFeeOptions {
fee_rate,
conf_target: self.conf_target,
replaceable: self.replaceable,
estimate_mode: self.estimate_mode,
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid sending null when no options are provided

bumpfee treats the absence of the second parameter differently from a
null value (the latter triggers a type error in ≥ 25.0). Assemble the
parameter list lazily:

- let args = vec![serde_json::to_value(txid)?, serde_json::to_value(opts)?];
+ let mut args = vec![serde_json::to_value(txid)?];
+ if let Some(o) = opts {
+     args.push(serde_json::to_value(o)?);
+ }

This mirrors the CLI behaviour (bitcoin-cli bumpfee <txid> [options]) and
keeps the code forward‑compatible.

Committable suggestion skipped: line range outside the PR's diff.

@gregdhill
Copy link
Contributor

Taken from rust-bitcoin/rust-bitcoincore-rpc#226

@gregdhill gregdhill merged commit 653d161 into master Apr 23, 2025
7 checks passed
@gregdhill gregdhill deleted the feat/bob-utils-rbf branch April 23, 2025 09:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants