diff --git a/crates/utils/src/bitcoin_client.rs b/crates/utils/src/bitcoin_client.rs index 92be62df..050e07d3 100644 --- a/crates/utils/src/bitcoin_client.rs +++ b/crates/utils/src/bitcoin_client.rs @@ -9,6 +9,7 @@ use bitcoin::{ }; use bitcoincore_rpc::{ bitcoin::{BlockHash, Transaction}, + json, json::{EstimateMode, TestMempoolAcceptResult}, jsonrpc::{error::RpcError, Error as JsonRpcError}, Auth, Client, Error as BitcoinError, RpcApi, @@ -181,6 +182,20 @@ pub struct BumpFeeResult { pub errors: Vec, } +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PsbtBumpFeeResult { + /// The base64-encoded unsigned PSBT of the new transaction. Only returned when wallet private + /// keys are disabled. + pub psbt: Option, + /// The fee of the original transaction (before bumping), denominated in BTC. + pub origfee: f64, + /// The fee of the newly created bumped transaction, denominated in BTC. + pub fee: f64, + /// Errors encountered during processing. + pub errors: Vec, +} + impl From for BitcoinRpcError { fn from(err: RpcError) -> Self { match num::FromPrimitive::from_i32(err.code) { @@ -216,6 +231,8 @@ pub enum Error { InvalidRecipient, #[error("Hex decoding error: {0}")] HexDecodeError(#[from] hex::FromHexError), + #[error("Finalized PSBT did not return a raw transaction hex")] + MissingRawTxHex, } #[derive(Clone)] @@ -454,6 +471,54 @@ impl BitcoinClient { Err(err) => Err(err.into()), // Handle the case where the RPC call fails } } + + pub fn psbt_bump_fee( + &self, + txid: &Txid, + options: Option<&BumpFeeOptions>, + ) -> Result { + // Serialize options if provided + let opts = match options { + Some(options) => Some(options.to_serializable(self.rpc.version()?)), + None => None, + }; + + // Prepare arguments + let args = vec![serde_json::to_value(txid)?, serde_json::to_value(opts)?]; + + // Call the "psbtbumpfee" RPC method + let result = self.rpc.call("psbtbumpfee", &args); + + // Handle the result + match result { + Ok(result_value) => { + let result: Result = serde_json::from_value(result_value); + match result { + Ok(bump_fee_result) => Ok(bump_fee_result), + Err(err) => { + println!("Failed to deserialize into PsbtBumpFeeResult"); + Err(err.into()) + } + } + } + Err(err) => Err(err.into()), + } + } + + pub fn sign_and_finalize_psbt( + &self, + psbt: &str, + sign: Option, + sighash_type: Option, + bip32derivs: Option, + ) -> Result { + let wallet_process_psbt = + self.rpc.wallet_process_psbt(psbt, sign, sighash_type, bip32derivs)?; + let finalized = self.rpc.finalize_psbt(&wallet_process_psbt.psbt, Some(true))?; + let tx_bytes = finalized.hex.ok_or(Error::MissingRawTxHex)?; + let tx: Transaction = consensus::deserialize(&tx_bytes)?; + Ok(tx) + } } fn merklize(left: Sha256dHash, right: Sha256dHash) -> Sha256dHash { diff --git a/crates/utils/src/bitcoin_core.rs b/crates/utils/src/bitcoin_core.rs index 42fd7f22..c834c1ea 100644 --- a/crates/utils/src/bitcoin_core.rs +++ b/crates/utils/src/bitcoin_core.rs @@ -250,6 +250,82 @@ mod tests { ); } + #[tokio::test] + async fn test_psbt_bump_fee() -> Result<()> { + // Step 1: Create and initialize BitcoinCore instance for test + let bitcoin = BitcoinCore::new().spawn(); + + // Fund Alice's wallet + bitcoin.fund_wallet("Alice").expect("Should fund Alice"); + + // Fund Bob's wallet + bitcoin.fund_wallet("Bob").expect("Should fund Alice"); + + // Check that Bob's balance is 5000000000 satoshis (i.e., 5 BTC) + assert_eq!( + bitcoin.client(Some("Bob")).unwrap().get_balance(None, None).unwrap().to_sat(), + 5000000000 + ); + + // Initialize BitcoinClient for Alice (make sure Alice's wallet is used) + let bitcoin_client = BitcoinClient::from(bitcoin.client(Some("Alice"))?); + + let to_addr = bitcoin_client.rpc.get_new_address(None, None).unwrap().assume_checked(); + + // Set the amount to send + let amount = Amount::from_sat(100_000); // 0.001 BTC (adjust as necessary) + + // Send the transaction (low fee expected) + let txid = bitcoin_client + .rpc + .send_to_address(&to_addr, amount, None, None, None, Some(true), None, None) + .unwrap(); + + // Step 3: Psbt Bump the fee for the low-fee transaction by calling bump_fee + let psbt_bump_fee = bitcoin_client + .psbt_bump_fee( + &txid, + Some(&BumpFeeOptions { + conf_target: None, + fee_rate: None, + replaceable: Some(true), // Allow the transaction to be replaceable + estimate_mode: None, + }), + ) + .unwrap(); + + // the previous tx fee should be less than the newly created tx fee + assert!(psbt_bump_fee.origfee < psbt_bump_fee.fee); + + // Sign and finalize the PSBT + let tx = bitcoin_client.sign_and_finalize_psbt( + &psbt_bump_fee.psbt.unwrap(), + None, + None, + None, + )?; + + // broadcast the bumped fee transaction + bitcoin_client.validate_and_send_raw_transaction(&tx).unwrap(); + + // Step 4: Generate 100 blocks to confirm the bump fee transaction + bitcoin_client.rpc.generate_to_address(100, &to_addr).unwrap(); + + // Check the original transaction + let tx_info = bitcoin_client.rpc.get_transaction(&txid, None).unwrap(); + + // Assert that the original transaction has negative confirmations + assert!(tx_info.info.confirmations.is_negative()); + + // Get the psbt bumped fee transaction's + let tx_info = bitcoin_client.rpc.get_transaction(&tx.compute_txid(), None).unwrap(); + + // Assert that the psbt bumped fee transaction has confirmations + assert!(tx_info.info.confirmations.is_positive()); + + Ok(()) + } + #[tokio::test] async fn test_bump_fee() -> Result<()> { // Step 1: Create and initialize BitcoinCore instance for test