Boost Transactions

Transactions can get stuck in the mempool due to low fees or a sudden surge in network fees. Stuck transactions can create a backlog of subsequent transactions from your vault account in some networks. To address this issue, Fireblocks customers can utilize the Replace By Fee (RBF) mechanism for EVM-based networks and the Child Pays For Parent (CPFP) mechanism for BTC.

The Replace By Fee (RBF) mechanism allows you to replace a stuck transaction with a new one that includes a higher fee, increasing the chances of the transaction being processed promptly. For Bitcoin transactions, the CPFP mechanism allows a child transaction to pay for the parent transaction's fee, effectively incentivizing miners to process both transactions.

By using these mechanisms, you can help ensure the smooth flow of transactions from your vault account and prevent delays caused by stuck transactions.




Replace By Fee (RBF for EVM networks)

For Ethereum blockchain transactions, when customers want to perform an RBF transaction, they should use the Create Transaction endpoint with the replaceTxByHash parameter as the hash of the stuck transaction that needs to be replaced.

The new transaction will be created with the same nonce value but with a higher fee than the stuck transaction, ensuring that the new transaction gets included in the network while the stuck transaction is dropped.


📘

Kindly note

The new transaction created will be of the TRANSFER type, even if the stuck transaction was of the CONTRACT_CALL type.


const transactionPayload = {
  assetId: "ETH",
  feeLevel: "HIGH",
  amount: "0.001",
	replaceTxByHash, // the hash of the transaction you wish to be replaced
  source: {
    type: TransferPeerPathType.VaultAccount,
    id: "0",
  },
  destination: {
    type: TransferPeerPathType.VaultAccount,
    id: "1",
  },
  note: "Your first failOnLowFee transaction",
};

const createTransaction = async (
  transactionPayload: TransactionRequest,
): Promise<CreateTransactionResponse | undefined> => {
  try {
    const transactionResponse = await fireblocks.transactions.createTransaction(
      {
        transactionRequest: transactionPayload,
      },
    );
    console.log(JSON.stringify(transactionResponse.data, null, 2));
    return transactionResponse.data;
  } catch (error) {
    console.error(error);
  }
};
createTransaction(transactionPayload);
async function createTransaction(assetId, amount, srcId, destId, replaceTxByHash){
    let payload = {
        assetId,
        amount,
        replaceTxByHash, // the hash of the transaction you wish to be replaced
        source: {
            type: PeerType.VAULT_ACCOUNT, 
         		id: String(srcId)
        },
        destination: {
            type: PeerType.VAULT_ACCOUNT, 
        		id: String(destId)
        },
        note: "Your first fee replacement transaction"
    };
    const result = await fireblocks.createTransaction(payload);
    console.log(JSON.stringify(result, null, 2));
}
createTransaction("ETH", "0.001", "0", "1", "0x5e0ce0b1242d1c85c17fc5127daa88e9eb842650e3e6a9a6de7c1bd9c3977cc2");
def create_transaction(asset_id, amount, src_id, dest_id, replace_tx_by_hash):
    tx_result = fireblocks.create_transaction(
        asset_id=asset_id,
        amount=amount,
        replace_tx_by_hash=replace_tx_by_hash, # the hash of the transaction you wish to be replaced
        source=TransferPeerPath(VAULT_ACCOUNT, src_id),
        destination=DestinationTransferPeerPath(VAULT_ACCOUNT, dest_id),
        note="Your first fee replacement transaction"
    )
    print(tx_result)

create_transaction("ETH", "0.001", "0", "1", "0x5e0ce0b1242d1c85c17fc5127daa88e9eb842650e3e6a9a6de7c1bd9c3977cc2")



Drop EVM transaction

Drop transaction uses RBF to replace the original transaction with a new transaction of 0 value, and a destination identical to the source, effectively canceling the transfer.

Use the Drop ETH (EVM) transaction by ID endpoint to drop a stuck transaction.

(async() => {

  const dropTx = await fireblocks.transactions.dropTransaction({
    dropTransactionRequest: {
      txId: "7cf964e7-0470-4b7e-a77b-5c88a963a514",
      feeLevel: "HIGH"
    }
  })

  console.log(JSON.stringify(dropTx, null, 2))

})();

While the txId value is the Fireblocks Transaction ID of the stuck transaction.




CPFP (Child Pays For Parent for BTC)

  1. Call the Get UTXO unspent inputs information endpoint to retrieve the original transaction's unspent outputs. Look for transactions with the Pending status in the transaction array. After you find your transaction, copy the txHash string. This is required for the CPFP transaction.
  2. Create the appropriate type of transaction. In the function, you will enter the assetId, amount, source.type, and source.id from the destination vault account in your workspace.
  3. Use the fee parameter to specify an amount that you estimate is high enough to cover the CPFP transaction fee as well as the fees of the original transactions it is designed to release. When an estimate is necessary, call the Estimate transaction fee endpoint to estimate the CPFP transaction fee.
  4. Use the extraParameters parameter to include the InputsSelection object of the original transaction. This indicates the unconfirmed input change created by the original transaction. Make sure it follows the specified structure because the inputsToSpend parameter populates the CPFP transaction with the original transaction's hash.
  5. Submit the transaction for approval and signing.

Important notes:

  1. The transaction you are trying to use CPFP with cannot be older than 10 days, otherwise it will no longer be present in the mempool. For older transactions which you want released, contact our Customer Support for assistance.
  2. The transaction must be marked as completed (confirmed) in our system, unless you are using the change UTXO.
  3. If the stuck transaction is older than 10 days and/or is not marked as completed, please contact Fireblocks Support.

How to switch the transaction to “Completed” status

You can switch the transaction to the Completed status via the Fireblocks Console or by using our set confirmation threshold API endpoint to set the threshold to zero. If it is not switched, the transaction will no longer be present in our mempool, and the CPFP transaction will fail.

Constructing the CPFP transaction

After verifying the transaction is not outdated and is in Completed status, you must use this endpoint to proceed with the CPFP operation. You must create a transaction payload with the following parameters:

  • Source: the source of the child transaction is the destination of the stuck transaction.
  • Destination: the destination of the child transaction can be the same as the source vault or any other destination you prefer.
  • Amount: must be the sum or less than the sum of the parent’s outputs that you want to use (you do not have to use all parent outputs; you can use only one output as an input for the child transaction).
  • FeeLevel: do not use FeeLevel, but instead see CustomFee below.
  • CustomFee: must be a high value, so the miner finds it attractive. You can use a CPFP calculator to get the desired fee (CPFP calculator example: Child Pays For Parent calculator).
  • ExtraParams: you must specify the inputs you want to use. These inputs serve as the outputs of the stuck parent transaction (you must use at least one). The selected inputs must be specified in the inputsToSend array inside an inputsSelection parameter, as is shown in the following example:

Example

let txPayload = {
  assetId: "BTC_TEST",
  amount: "0.001",
  fee: "1000",
  source: {
    type: TransferPeerPathType.VaultAccount,
    id: "0",
  },
  destination: {
    type: TransferPeerPathType.VaultAccount,
    id: "1",
  },
  extraParameters: {
    inputsSelection: {
      //grab input hash from UTXO endpoint
      inputsToSpend: [
        {
          txHash:
            "24f769af0c2b67965ae4b95583c049e0a8ba08f1c142676c9fc9fcaac5ad12a3",
          index: 0,
        },
      ],
    },
  },
  note: "Your CPFP transaction!",
};

const createTransaction = async (
  payload: TransactionRequest,
): Promise<CreateTransactionResponse | undefined> => {
  try {
    const result = await fireblocks.transactions.createTransaction({
      transactionRequest: payload,
    });
    console.log(JSON.stringify(result.data, null, 2));
    return result.data;
  } catch (error) {
    console.error(error);
  }
};
createTransaction(txPayload);

//prerequisites: API key from fireblocks, set up in console

const fs = require('fs');
const path = require('path');
const { FireblocksSDK } = require('fireblocks-sdk');
const { exit } = require('process');
const { inspect } = require('util');

//api secret key file created
const apiSecret = fs.readFileSync(path.resolve("../fireblocks_secret.key"), "utf8");
const apiKey = "<API_KEY HERE>"
// Choose the right api url for your workspace type 
const baseUrl = "https://api.fireblocks.io";
const fireblocks = new FireblocksSDK(apiSecret, apiKey, baseUrl);

(async () => {

    let newTx = await fireblocks.createTransaction({
        assetId: "BTC_TEST",
        amount: "0.001",
        fee: "1000",
        source: {
            type: "VAULT_ACCOUNT", 
            id: "2"
        },
        destination: { 
            type: "VAULT_ACCOUNT",
            id: "3"
        },
        extraParameters: {
            inputsSelection: {
                //grab input hash from UTXO endpoint
                inputsToSpend: [
                    {
                        "txHash": "24f769af0c2b67965ae4b95583c049e0a8ba08f1c142676c9fc9fcaac5ad12a3",
                        "index": 0
                    }, 
                ]
            }
        }
    });
    
    console.log(JSON.stringify(newTx));

    // let amountToConsolidate = await fireblocks.getMaxSpendableAmount("1", "BTC_TEST").maxSpendableAmount;
    // console.log(amountToConsolidate);

})().catch((e)=>{
    console.error(`Failed: ${e}`);
    exit(-1);
})
import json
from pathlib import Path

from fireblocks_sdk import FireblocksSDK
from fireblocks_sdk.api_types import *


def initialize_fireblocks(api_secret, api_key, base_url):
    try:
        return FireblocksSDK(private_key=api_secret, api_key=api_key, api_base_url=base_url)
    except Exception as e:
        raise ConnectionError(f"Error initializing Fireblocks SDK: {e}")


def create_transaction(fireblocks_instance):
    try:
        transaction_result = fireblocks_instance.create_transaction(
                asset_id="BTC_TEST",
                source=TransferPeerPath(peer_type=VAULT_ACCOUNT, peer_id="31"),
                destination=DestinationTransferPeerPath(peer_type=VAULT_ACCOUNT, peer_id="0"),
                amount="0.00009", fee="1000",
                extra_parameters={
                    "inputsSelection": {
                        "inputsToSpend": [
                            {"txHash": "0076f06b4f38dc2e14bc9abdc8e7caff6607f6f381e14e7db153ddd6267aabcd", "index": 1},
                            {"txHash": "b7496655bb71d04ee4a8dd9b13a5e78f420bc7deceddd6ae09afc14484f6abcd", "index": 1},
                            {"txHash": "2e3761fee5765d8c49bf13cf7bc4b32bed6804999bafe01c5605d64a897eabcd", "index": 0},
                            {"txHash": "436993c1a14e0ba824c7733399c8943fc8de0fdad1996a5dec53064045c4abcd", "index": 1},
                            {"txHash": "9f9af5bc9e5d57c9f8d0223bf2e1eb4c2ea45e95f2c1a0d0f02caaaa9956abcd", "index": 1},
                            ]
                        }
                    }
                )
        return transaction_result
    except Exception as e:
        raise RuntimeError(f"Error creating transaction: {e}")


def main():
    api_secret_path = ".../fireblocks_secret.key"
    api_key = "<API_KEY HERE>"
    base_url = "https://api.fireblocks.io"
    
    try:
        api_secret_file = Path(api_secret_path)
        api_secret = api_secret_file.read_text()
        fireblocks = initialize_fireblocks(api_secret, api_key, base_url)
        transaction_result = create_transaction(fireblocks)
        print(json.dumps(transaction_result, indent=2))
    except Exception as e:
        print(f"An error occurred: {e}")


if __name__ == "__main__":
    main()

Summary of the CPFP process

In order to carry out a CPFP transaction, the following requirements must be satisfied:

  1. The stuck transaction must not be older than 10 days.
  2. The transaction must be marked as Completed in our system.

Notes:

  1. If the parent transaction has a change output then it can still be used by the CPFP transaction to boost the original, even if the destination is not under the sender’s custody.
  2. If you use the change output you do not have to switch the stuck transaction to the Completed status.
  3. If the parent transaction is no longer in the Fireblocks mempool and the source of the transaction is within your workspace, you can perform a Transaction Replacement procedure instead.