Skip to content

Commit ba89b2a

Browse files
authored
feat: add psbt_bump_fee and sign_and_finalize_psbt methods (#632)
1 parent ea5ad01 commit ba89b2a

File tree

2 files changed

+141
-0
lines changed

2 files changed

+141
-0
lines changed

crates/utils/src/bitcoin_client.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use bitcoin::{
99
};
1010
use bitcoincore_rpc::{
1111
bitcoin::{BlockHash, Transaction},
12+
json,
1213
json::{EstimateMode, TestMempoolAcceptResult},
1314
jsonrpc::{error::RpcError, Error as JsonRpcError},
1415
Auth, Client, Error as BitcoinError, RpcApi,
@@ -181,6 +182,20 @@ pub struct BumpFeeResult {
181182
pub errors: Vec<String>,
182183
}
183184

185+
#[derive(Deserialize, Clone, Debug)]
186+
#[serde(rename_all = "camelCase")]
187+
pub struct PsbtBumpFeeResult {
188+
/// The base64-encoded unsigned PSBT of the new transaction. Only returned when wallet private
189+
/// keys are disabled.
190+
pub psbt: Option<String>,
191+
/// The fee of the original transaction (before bumping), denominated in BTC.
192+
pub origfee: f64,
193+
/// The fee of the newly created bumped transaction, denominated in BTC.
194+
pub fee: f64,
195+
/// Errors encountered during processing.
196+
pub errors: Vec<String>,
197+
}
198+
184199
impl From<RpcError> for BitcoinRpcError {
185200
fn from(err: RpcError) -> Self {
186201
match num::FromPrimitive::from_i32(err.code) {
@@ -216,6 +231,8 @@ pub enum Error {
216231
InvalidRecipient,
217232
#[error("Hex decoding error: {0}")]
218233
HexDecodeError(#[from] hex::FromHexError),
234+
#[error("Finalized PSBT did not return a raw transaction hex")]
235+
MissingRawTxHex,
219236
}
220237

221238
#[derive(Clone)]
@@ -454,6 +471,54 @@ impl BitcoinClient {
454471
Err(err) => Err(err.into()), // Handle the case where the RPC call fails
455472
}
456473
}
474+
475+
pub fn psbt_bump_fee(
476+
&self,
477+
txid: &Txid,
478+
options: Option<&BumpFeeOptions>,
479+
) -> Result<PsbtBumpFeeResult, Error> {
480+
// Serialize options if provided
481+
let opts = match options {
482+
Some(options) => Some(options.to_serializable(self.rpc.version()?)),
483+
None => None,
484+
};
485+
486+
// Prepare arguments
487+
let args = vec![serde_json::to_value(txid)?, serde_json::to_value(opts)?];
488+
489+
// Call the "psbtbumpfee" RPC method
490+
let result = self.rpc.call("psbtbumpfee", &args);
491+
492+
// Handle the result
493+
match result {
494+
Ok(result_value) => {
495+
let result: Result<PsbtBumpFeeResult, _> = serde_json::from_value(result_value);
496+
match result {
497+
Ok(bump_fee_result) => Ok(bump_fee_result),
498+
Err(err) => {
499+
println!("Failed to deserialize into PsbtBumpFeeResult");
500+
Err(err.into())
501+
}
502+
}
503+
}
504+
Err(err) => Err(err.into()),
505+
}
506+
}
507+
508+
pub fn sign_and_finalize_psbt(
509+
&self,
510+
psbt: &str,
511+
sign: Option<bool>,
512+
sighash_type: Option<json::SigHashType>,
513+
bip32derivs: Option<bool>,
514+
) -> Result<Transaction, Error> {
515+
let wallet_process_psbt =
516+
self.rpc.wallet_process_psbt(psbt, sign, sighash_type, bip32derivs)?;
517+
let finalized = self.rpc.finalize_psbt(&wallet_process_psbt.psbt, Some(true))?;
518+
let tx_bytes = finalized.hex.ok_or(Error::MissingRawTxHex)?;
519+
let tx: Transaction = consensus::deserialize(&tx_bytes)?;
520+
Ok(tx)
521+
}
457522
}
458523

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

crates/utils/src/bitcoin_core.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,82 @@ mod tests {
251251
);
252252
}
253253

254+
#[tokio::test]
255+
async fn test_psbt_bump_fee() -> Result<()> {
256+
// Step 1: Create and initialize BitcoinCore instance for test
257+
let bitcoin = BitcoinCore::new().spawn();
258+
259+
// Fund Alice's wallet
260+
bitcoin.fund_wallet("Alice").expect("Should fund Alice");
261+
262+
// Fund Bob's wallet
263+
bitcoin.fund_wallet("Bob").expect("Should fund Alice");
264+
265+
// Check that Bob's balance is 5000000000 satoshis (i.e., 5 BTC)
266+
assert_eq!(
267+
bitcoin.client(Some("Bob")).unwrap().get_balance(None, None).unwrap().to_sat(),
268+
5000000000
269+
);
270+
271+
// Initialize BitcoinClient for Alice (make sure Alice's wallet is used)
272+
let bitcoin_client = BitcoinClient::from(bitcoin.client(Some("Alice"))?);
273+
274+
let to_addr = bitcoin_client.rpc.get_new_address(None, None).unwrap().assume_checked();
275+
276+
// Set the amount to send
277+
let amount = Amount::from_sat(100_000); // 0.001 BTC (adjust as necessary)
278+
279+
// Send the transaction (low fee expected)
280+
let txid = bitcoin_client
281+
.rpc
282+
.send_to_address(&to_addr, amount, None, None, None, Some(true), None, None)
283+
.unwrap();
284+
285+
// Step 3: Psbt Bump the fee for the low-fee transaction by calling bump_fee
286+
let psbt_bump_fee = bitcoin_client
287+
.psbt_bump_fee(
288+
&txid,
289+
Some(&BumpFeeOptions {
290+
conf_target: None,
291+
fee_rate: None,
292+
replaceable: Some(true), // Allow the transaction to be replaceable
293+
estimate_mode: None,
294+
}),
295+
)
296+
.unwrap();
297+
298+
// the previous tx fee should be less than the newly created tx fee
299+
assert!(psbt_bump_fee.origfee < psbt_bump_fee.fee);
300+
301+
// Sign and finalize the PSBT
302+
let tx = bitcoin_client.sign_and_finalize_psbt(
303+
&psbt_bump_fee.psbt.unwrap(),
304+
None,
305+
None,
306+
None,
307+
)?;
308+
309+
// broadcast the bumped fee transaction
310+
bitcoin_client.validate_and_send_raw_transaction(&tx).unwrap();
311+
312+
// Step 4: Generate 100 blocks to confirm the bump fee transaction
313+
bitcoin_client.rpc.generate_to_address(100, &to_addr).unwrap();
314+
315+
// Check the original transaction
316+
let tx_info = bitcoin_client.rpc.get_transaction(&txid, None).unwrap();
317+
318+
// Assert that the original transaction has negative confirmations
319+
assert!(tx_info.info.confirmations.is_negative());
320+
321+
// Get the psbt bumped fee transaction's
322+
let tx_info = bitcoin_client.rpc.get_transaction(&tx.compute_txid(), None).unwrap();
323+
324+
// Assert that the psbt bumped fee transaction has confirmations
325+
assert!(tx_info.info.confirmations.is_positive());
326+
327+
Ok(())
328+
}
329+
254330
#[tokio::test]
255331
async fn test_bump_fee() -> Result<()> {
256332
// Step 1: Create and initialize BitcoinCore instance for test

0 commit comments

Comments
 (0)