Skip to content
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

Fee bumping Package #114

Merged
merged 84 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
5c233db
feat: Initialize @caravan/feebumping package
Legend101Zz Jun 23, 2024
592a875
changes in rbf file
Legend101Zz Jun 23, 2024
4f0755c
Add comprehensive fee bumping package for Caravan
Legend101Zz Jul 19, 2024
b00eaae
Implement fee bumping types and estimation functions
Legend101Zz Jul 22, 2024
9b0c967
Add transaction analysis functionality for fee bumping
Legend101Zz Jul 22, 2024
b7b2fa5
Refactor fee bumping package for improved functionality and integration
Legend101Zz Jul 22, 2024
63c129c
Changes to the RBF functionality in the fees package and added RBF si…
Legend101Zz Jul 26, 2024
4fce4fe
Implement CPFP and RBF classes with tests
Legend101Zz Aug 1, 2024
77f0cbe
Refactored RBF and CPFP classes, introduced TransactionAnalyzer class
Legend101Zz Aug 3, 2024
77f4db4
Refactor RBF, CPFP, and TransactionAnalyzer Classes and their Tests
Legend101Zz Aug 6, 2024
4f4f97a
Enhance RBF class functionality with improved state management and fe…
Legend101Zz Aug 14, 2024
07d5a84
Added methods to handle sequence numbers within PSBTs for better RBF …
Legend101Zz Aug 14, 2024
d71a0ec
minor corrections in requiredFee method
Legend101Zz Aug 14, 2024
ee553e7
minor corrections in requiredFee name , changed to requiredAbsoluteFee
Legend101Zz Aug 14, 2024
9237196
Refactor RBF class with improved state management and error handling
Legend101Zz Aug 16, 2024
63969ab
feat: Implement TransactionAnalyzer for raw transaction analysis
Legend101Zz Aug 19, 2024
0047714
Refactor of TransactionAnalyzer classfor Enhanced Fee Calculation and…
Legend101Zz Aug 22, 2024
28a1ae2
Added Bitcoin Transaction Components and Template
Legend101Zz Aug 22, 2024
8ca1b75
feat: Implemented new CPFP/RBF functions
Legend101Zz Aug 22, 2024
09cceac
chore: changes in utils,types and constants files
Legend101Zz Aug 29, 2024
47e598f
refactor: changes in transactionAnalyzer class and it's tests
Legend101Zz Aug 29, 2024
3c9f3ff
refactor : btcTransactionComponents and btcTransactionTemplate classes
Legend101Zz Aug 29, 2024
93f5e39
refactor: Refactored and corrected RBF and CPFP functions
Legend101Zz Aug 29, 2024
b247da4
changes: add util fn to convert btcTemplate to PSBT
Legend101Zz Sep 1, 2024
6662b2d
refactor : btcComponents and btcTemplate ... toPsbt() method , remove…
Legend101Zz Sep 1, 2024
f6aeea6
refactor: transactionAnalyzer ... removed redundant methods
Legend101Zz Sep 1, 2024
345d6fb
refactor: Changes in btcTransactionInput , Add methods for intiatiati…
Legend101Zz Sep 2, 2024
ad3c6ee
refactor : utils , added more util types for PSBT based calculations
Legend101Zz Sep 2, 2024
a215dea
refactor: RBF/CPFP functions to handle new toPSBT() method of templat…
Legend101Zz Sep 2, 2024
4597dc9
tests: added new btcTransactionComponents tests and refactored other …
Legend101Zz Sep 2, 2024
f39e26d
add README
Legend101Zz Sep 2, 2024
372f919
refactor btcTransactionComponents : Changes to inline docstring comme…
Legend101Zz Sep 9, 2024
d19fd1e
minor README changes
Legend101Zz Sep 9, 2024
272d946
refactor btcTransactionComponents : added methods to validate witness…
Legend101Zz Sep 9, 2024
c75270a
corrected and added new tests to btcTransactionComponents
Legend101Zz Sep 9, 2024
09d9bc5
refactored: btcTransactionTemplate class
Legend101Zz Sep 9, 2024
8da4962
Add ScriptType : Added a new script-type extending MULTISIG_ADDRESS_T…
Legend101Zz Sep 9, 2024
0acce5f
refactot transactionAnalyzer
Legend101Zz Sep 9, 2024
65717ec
minor fix: README link
Legend101Zz Sep 11, 2024
dc02fb5
Refactor: Utils file to remove unnecessary functions, move some util …
Legend101Zz Sep 12, 2024
73dd323
refactor: Transaction Analyzer ... correct rbf-fees calculation , ret…
Legend101Zz Sep 12, 2024
db3ae16
minor-refactor: Add detailed doc strings for ABSURDLY_HIGH_ABS_FEE & …
Legend101Zz Sep 12, 2024
fbd36a1
minor btcTransactionComponents changes
Legend101Zz Sep 12, 2024
82a3c5c
refactor: btcTransactionTemplate add addInputToPsbt & addOutputToPsbt…
Legend101Zz Sep 12, 2024
f142a7f
add: isCPFPFeeSatisfied method from utils to cpfp file
Legend101Zz Sep 12, 2024
104bc75
removed reverseTxid fn from utils
Legend101Zz Sep 16, 2024
e5c98c1
correction: Corrected code to extract input txid from input hash
Legend101Zz Sep 16, 2024
74511cf
correction-tests: Transaction Analyzer
Legend101Zz Sep 16, 2024
6f35442
correction-tests: BTC Components
Legend101Zz Sep 16, 2024
dcffd5e
correction tests: BTC Template
Legend101Zz Sep 22, 2024
788ff6f
correction tests: RBF
Legend101Zz Sep 22, 2024
c3f56c4
correction tests: CPFP
Legend101Zz Sep 22, 2024
74c643b
refactor : tsconfig
Legend101Zz Sep 22, 2024
c570faf
correction : RBF function , ensure that the new bumped tx ,pays gte o…
Legend101Zz Sep 24, 2024
539ac54
RBF : remove console.log statements
Legend101Zz Sep 24, 2024
bddee6e
correction : index file , added all exports
Legend101Zz Sep 24, 2024
ad43a38
correction: RBF calculations
Legend101Zz Sep 24, 2024
e8105cb
refactor: RBF fixtures
Legend101Zz Sep 24, 2024
7150a01
minor changes : Transaction Analyzer - RBF FEE CALCULATION COMMENTS .…
Legend101Zz Sep 27, 2024
a44bedc
added comments : types file
Legend101Zz Sep 27, 2024
663df1e
RBF : Add option in RBF tx creation to prevent
Legend101Zz Sep 27, 2024
09fba1f
RBF-Fixtures : minor comment fixes
Legend101Zz Sep 27, 2024
90b3227
changes: README
Legend101Zz Oct 9, 2024
9b4a174
review-changes: btcTransaction Template
Legend101Zz Oct 16, 2024
cb249e2
review-changes: constants.ts
Legend101Zz Oct 16, 2024
ed9f6c7
review-changes: CPFP
Legend101Zz Oct 16, 2024
1109c68
refactor: CPFP
Legend101Zz Oct 16, 2024
e3fe441
minor-refactor: CPFP
Legend101Zz Oct 16, 2024
d0df3dc
correction : validateCPFPPackage
Legend101Zz Oct 17, 2024
22e5479
Fix-BtcTransactionTemplate: adjustChangeOutput, Preserve sole dust ou…
Legend101Zz Oct 17, 2024
69e44cb
Fix-BtcTransactionTemplate : valildate() function , Enhance BtcTransa…
Legend101Zz Oct 17, 2024
d96b3be
Correction-btcTemplate : validate() function
Legend101Zz Oct 17, 2024
fa292dc
Add-method: needsChange
Legend101Zz Oct 17, 2024
ee59d3f
Fix : TxAnalyzer RBF calculations
Legend101Zz Oct 17, 2024
5c80694
Review-changes: RBF
Legend101Zz Oct 17, 2024
c34de8a
Refactor-RBF: Common-functionality between acceleration and cancellat…
Legend101Zz Oct 18, 2024
787be0d
review-changes : PSBTV2
Legend101Zz Oct 18, 2024
4f6b067
Fix-Utils : estimateTransactionVsize
Legend101Zz Oct 18, 2024
945d644
FINAL NITS
Legend101Zz Oct 21, 2024
96b4e44
Merge branch 'main' into fee-bumping
bucko13 Oct 21, 2024
9676c49
refactor-RBF: remove analysis object
Legend101Zz Oct 22, 2024
4057659
review-changes: CPFP
Legend101Zz Oct 22, 2024
d46fa32
FINAL NITS:2
Legend101Zz Oct 23, 2024
8f2335b
FINAL NITS:3
Legend101Zz Oct 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/caravan-fees/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
root: true,
extends: ["@caravan/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
8 changes: 8 additions & 0 deletions packages/caravan-fees/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
transform: {
"^.+\\.js$": "babel-jest",
"^.+\\.ts$": "ts-jest",
},
};
40 changes: 40 additions & 0 deletions packages/caravan-fees/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@caravan/fees",
"version": "0.0.0",
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
"description": "Utility library for fee bumping bitcoin transactions",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"module": "./dist/index.mjs",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "npm run build -- --watch",
"test": "jest src",
"test:watch": "jest --watch src"
},
"keywords": [
"bitcoin",
"cpfp",
"rbf",
"feebumping",
"blockchain"
],
"author": "",
"license": "MIT",
"engines": {
"node": ">=20"
},
"devDependencies": {
"@caravan/eslint-config": "*",
"tsconfig": "*"
},
"dependencies": {
"@caravan/bitcoin": "*"
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
}
}
11 changes: 11 additions & 0 deletions packages/caravan-fees/src/constants.ts
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import BigNumber from "bignumber.js";
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved

export const DEFAULT_RBF_SEQUENCE = 0xfffffffd;
export const MIN_FEE_RATE = 1;
export const MAX_FEE_RATE = 5000;
export const DEFAULT_FEE_INCREASE_THRESHOLD = 1.1;
export const MIN_DUST_AMOUNT = new BigNumber(546);
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
export const SATS_PER_BTC = new BigNumber(100000000);
export const DUST_THRESHOLD = 546; // in satoshis
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
export const MAX_STANDARD_TX_WEIGHT = 400000; // in weight units
export const MIN_RELAY_FEE_RATE = 1; // in satoshis per byte
82 changes: 82 additions & 0 deletions packages/caravan-fees/src/cpfp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { prepareCPFPTransaction } from "./cpfp";
import { Transaction } from "bitcoinjs-lib-v5";
import { Network, unsignedMultisigTransaction } from "@caravan/bitcoin";
import BigNumber from "bignumber.js";
import { CPFPOptions, UTXO } from "./types";

jest.mock("@caravan/bitcoin");
jest.mock("./utils");

describe("CPFP", () => {
let mockParentTransaction: Transaction;
let mockUTXOs: UTXO[];
let mockOptions: CPFPOptions;

beforeEach(() => {
mockParentTransaction = {
getId: jest.fn().mockReturnValue("parentTxId"),
outs: [{ value: 100000, address: "address1" }],
virtualSize: jest.fn().mockReturnValue(200),
getAddressType: jest.fn().mockReturnValue("P2SH"),
getRequiredSigners: jest.fn().mockReturnValue(2),
getTotalSigners: jest.fn().mockReturnValue(3),
} as unknown as Transaction;
mockUTXOs = [
{
txid: "parentTxId",
vout: 0,
value: new BigNumber(100000),
address: "address1",
scriptPubKey: "script1",
},
];
mockOptions = {
parentTransaction: mockParentTransaction,
newFeeRate: { satoshisPerByte: 10 },
availableUTXOs: mockUTXOs,
destinationAddress: "destinationAddress",
network: Network.TESTNET,
};

// Mock utility functions
jest.requireMock("./utils").estimateVirtualSize.mockReturnValue(200);
(unsignedMultisigTransaction as jest.Mock).mockReturnValue(
{} as Transaction
);
});

it("should prepare CPFP transaction", () => {
const result = prepareCPFPTransaction(mockOptions);
expect(result).toBeDefined();
expect(unsignedMultisigTransaction).toHaveBeenCalled();
});

it("should throw error if no suitable parent output found", () => {
mockOptions.availableUTXOs = [];
expect(() => prepareCPFPTransaction(mockOptions)).toThrow(
"No suitable output found in parent transaction for CPFP"
);
});

it("should throw error if CPFP transaction would create dust output", () => {
mockOptions.newFeeRate = { satoshisPerByte: 1000 }; // Unrealistically high fee to force dust output
expect(() => prepareCPFPTransaction(mockOptions)).toThrow(
"CPFP transaction would create a dust output"
);
});

it("should calculate correct fee for child transaction", () => {
prepareCPFPTransaction(mockOptions);
const expectedFee = new BigNumber(400).multipliedBy(10); // (200 + 200) * 10
expect(unsignedMultisigTransaction).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.arrayContaining([
expect.objectContaining({
amountSats: new BigNumber(100000).minus(expectedFee),
}),
]),
true
);
});
});
105 changes: 105 additions & 0 deletions packages/caravan-fees/src/cpfp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Transaction } from "bitcoinjs-lib-v5";
import { unsignedMultisigTransaction } from "@caravan/bitcoin";
import BigNumber from "bignumber.js";
import { CPFPOptions, UTXO } from "./types";
import { estimateVirtualSize } from "./utils";

/**
* Prepare a CPFP (Child-Pays-for-Parent) transaction.
* This function doesn't broadcast the transaction, it just prepares it.
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
* @param options CPFP options including the parent transaction and new fee rate
* @returns A new transaction ready for signing and broadcasting
*/
export function prepareCPFPTransaction(options: CPFPOptions): Transaction {
const {
parentTransaction,
newFeeRate,
availableUTXOs,
destinationAddress,
network,
multisigDetails,
} = options;
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved

// Find suitable output from parent transaction to spend
const parentOutput = findSuitableParentOutput(
parentTransaction,
availableUTXOs
);
if (!parentOutput) {
throw new Error("No suitable output found in parent transaction for CPFP");
}

const childSize = estimateChildTransactionSize({
inputCount: 1, // We're spending one output from the parent
outputCount: 2, // Destination output and potentially change
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
addressType: multisigDetails.addressType,
requiredSigners: multisigDetails.requiredSigners,
totalSigners: multisigDetails.totalSigners,
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
});
// Calculate required fee for both transactions
const combinedSize = parentTransaction.virtualSize() + childSize;
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
const requiredFee = new BigNumber(combinedSize).multipliedBy(
newFeeRate.satoshisPerByte
);

// Calculate amount to send to destination
const outputAmount = new BigNumber(parentOutput.value).minus(requiredFee);

if (outputAmount.isLessThan(546)) {
// Check if output would be dust
throw new Error("CPFP transaction would create a dust output");
}

const inputs = [{ ...parentOutput, txid: parentTransaction.getId() }];
const outputs = [{ address: destinationAddress, amountSats: outputAmount }];

// Use Caravan's unsignedMultisigTransaction function to create the new transaction
return unsignedMultisigTransaction(network, inputs, outputs, true); // true to enable RBF
}

/**
* Find a suitable output from the parent transaction to use for CPFP.
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
* @param parentTransaction The parent transaction
* @param availableUTXOs Available UTXOs that can be spent
* @returns A suitable output or null if none found
*/
function findSuitableParentOutput(
parentTransaction: Transaction,
availableUTXOs: UTXO[]
): { index: number; value: number } | null {
for (let i = 0; i < parentTransaction.outs.length; i++) {
const output = parentTransaction.outs[i];
if (
availableUTXOs.some(
(utxo) => utxo.txid === parentTransaction.getId() && utxo.vout === i
)
) {
return { index: i, value: output.value };
}
}
return null;
}

function estimateChildTransactionSize(options: {
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
inputCount: number;
outputCount: number;
addressType: string;
requiredSigners: number;
totalSigners: number;
}): number {
const {
inputCount,
outputCount,
addressType,
requiredSigners,
totalSigners,
} = options;

return estimateVirtualSize(
addressType,
inputCount,
outputCount,
requiredSigners,
totalSigners
);
}
6 changes: 6 additions & 0 deletions packages/caravan-fees/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from "./types";
export * from "./utils";
export * from "./transactionAnalyzer";
export * from "./rbf";
export * from "./cpfp";
export * from "./constants";
Legend101Zz marked this conversation as resolved.
Show resolved Hide resolved
86 changes: 86 additions & 0 deletions packages/caravan-fees/src/rbf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { prepareRBFTransaction } from "./rbf";
import { Transaction } from "bitcoinjs-lib-v5";
import { Network, unsignedMultisigTransaction } from "@caravan/bitcoin";
import BigNumber from "bignumber.js";
import { RBFOptions, UTXO } from "./types";

jest.mock("@caravan/bitcoin");
jest.mock("./utils");

describe("RBF", () => {
let mockTransaction: Transaction;
let mockUTXOs: UTXO[];
let mockOptions: RBFOptions;

beforeEach(() => {
mockTransaction = {
ins: [{ sequence: 0xfffffffd }],
outs: [{ value: 90000, address: "address1" }],
getAddressType: jest.fn().mockReturnValue("P2SH"),
getRequiredSigners: jest.fn().mockReturnValue(2),
getTotalSigners: jest.fn().mockReturnValue(3),
} as unknown as Transaction;
mockUTXOs = [
{
txid: "txid1",
vout: 0,
value: new BigNumber(100000),
address: "address1",
scriptPubKey: "script1",
},
];
mockOptions = {
transaction: mockTransaction,
newFeeRate: { satoshisPerByte: 10 },
utxos: mockUTXOs,
network: Network.TESTNET,
};

// Mock utility functions
jest.requireMock("./utils").isRBFSignaled.mockReturnValue(true);
jest.requireMock("./utils").estimateVirtualSize.mockReturnValue(200);
(unsignedMultisigTransaction as jest.Mock).mockReturnValue(
{} as Transaction
);
});

it("should prepare RBF transaction", () => {
const result = prepareRBFTransaction(mockOptions);
expect(result).toBeDefined();
expect(unsignedMultisigTransaction).toHaveBeenCalled();
});

it("should throw error if transaction is not RBF enabled", () => {
jest.requireMock("./utils").isRBFSignaled.mockReturnValue(false);
expect(() => prepareRBFTransaction(mockOptions)).toThrow(
"Original transaction is not signaling RBF"
);
});

it("should handle cancelTransaction option", () => {
mockOptions.cancelTransaction = true;
mockOptions.destinationAddress = "cancelAddress";
prepareRBFTransaction(mockOptions);
expect(unsignedMultisigTransaction).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.arrayContaining([
expect.objectContaining({ address: "cancelAddress" }),
]),
true
);
});

it("should handle subtractFromOutput option", () => {
mockOptions.subtractFromOutput = true;
prepareRBFTransaction(mockOptions);
expect(unsignedMultisigTransaction).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.arrayContaining([
expect.objectContaining({ amountSats: expect.any(BigNumber) }),
]),
true
);
});
});
Loading