Skip to content

Commit

Permalink
Add unit test for Coin Selector
Browse files Browse the repository at this point in the history
  • Loading branch information
Ayush170-Future committed Feb 25, 2024
1 parent 6438c55 commit ed44b85
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 8 deletions.
41 changes: 33 additions & 8 deletions src/wallet/coin-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class CoinPointer {

export class CoinSelector {
private readonly feeRate: number = 1;
public static readonly LONG_TERM_FEERATE: number = 5; // in sats/vB
constructor(feeRate?: number) {
this.feeRate = feeRate;
}
Expand Down Expand Up @@ -43,16 +44,10 @@ export class CoinSelector {
) - target;

// Calculate the cost of change
const LONG_TERM_FEERATE = 5 // in sats/vB
const outputSize = 31; // P2WPKH output is 31 B
const inputSizeOfChangeUTXO = 68.0; // P2WPKH input is 68.0 vbytes

const costOfChangeOutput = outputSize * this.feeRate;
const costOfSpendingChange = inputSizeOfChangeUTXO * LONG_TERM_FEERATE;
const costOfChange = costOfChangeOutput + costOfSpendingChange;
const costOfChange = this.costOfChange;

// Check if change is less than the cost of change
if(change <= costOfChange) {
if (change <= costOfChange) {
change = 0;
}

Expand All @@ -62,6 +57,36 @@ export class CoinSelector {
};
}

get costOfChange() {
// P2WPKH output size in bytes:
// Pay-to-Witness-Public-Key-Hash (P2WPKH) outputs have a fixed size of 31 bytes:
// - 8 bytes to encode the value
// - 1 byte variable-length integer encoding the locking script’s size
// - 22 byte locking script
const outputSize = 31;

// P2WPKH input size estimation:
// - Composition:
// - PREVOUT: hash (32 bytes), index (4 bytes)
// - SCRIPTSIG: length (1 byte), scriptsig for P2WPKH input is empty
// - sequence (4 bytes)
// - WITNESS STACK:
// - item count (1 byte)
// - signature length (1 byte)
// - signature (71 or 72 bytes)
// - pubkey length (1 byte)
// - pubkey (33 bytes)
// - Total:
// 32 + 4 + 1 + 4 + (1 + 1 + 72 + 1 + 33) / 4 = 68 vbytes
const inputSizeOfChangeUTXO = 68;

const costOfChangeOutput = outputSize * this.feeRate;
const costOfSpendingChange =
inputSizeOfChangeUTXO * CoinSelector.LONG_TERM_FEERATE;

return costOfChangeOutput + costOfSpendingChange;
}

private selectCoins(pointers: CoinPointer[], target: number) {
const selected = this.selectLowestLarger(pointers, target);
if (selected.length > 0) return selected;
Expand Down
92 changes: 92 additions & 0 deletions test/coinselector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Coin } from '../src/wallet/coin';
import { CoinSelector } from '../src/wallet/coin-selector.ts';
import { Transaction } from 'bitcoinjs-lib';

describe('CoinSelector', () => {
let coinSelector: CoinSelector;
const feeRate: number = 5;

beforeAll(() => {
coinSelector = new CoinSelector(feeRate);
});

it('should generate the correct cost of change', () => {
// Fixed value for the cost of change based on the current feeRate.
const correctCostOfChange = 495;

// Assert the correctness of the CoinSelector result.
expect(correctCostOfChange).toBe(coinSelector.costOfChange);
});

it.each([
{
testName: 'should add change to the fee if it is dust',
testCoinValues: [
1005, 9040, 6440, 2340, 7540, 3920, 5705, 9030, 1092, 5009,
],
expectedChange: 0, // Change will be zero when less than dust.
expectedCoinCount: 10, // All the coins are going to be selected.
},
{
testName: 'change should be considered when greater than dust',
testCoinValues: [5000, 4000],
txOutValue: 1000, // Less output so that the change is greater than dust.
expectedChange: 2185, // expectedChange = totalSelectedCoin - (txOutValue + transactionFees).
expectedCoinCount: 1, // One Coin will suffice for a 1000 sats output for this case.
},
])(
'%s',
({ txOutValue, testCoinValues, expectedChange, expectedCoinCount }) => {
const coins: Coin[] = [];
let totalBalance: number = 0;

// Calculate the total balance based on the fixed array of coin values.
for (const value of testCoinValues) {
const coin = new Coin({ value });
coins.push(coin);
// Calculate the total balance by subtracting the spending fee.
totalBalance += coin.value - coin.estimateSpendingFee(feeRate);
}

// Create a new Transaction
const transaction: Transaction = new Transaction();

// Fixed fees for the transaction calculated based on the total size of the transaction and the change output size.
// transactionFees = (virtualSize + changeOutputSize) * feeRate, P2WPKH output is 31 bytes.
const transactionFees = 250;

// txOutValue is not defined for the first test in order to demonstrate its calculation.
if (!txOutValue) {
// Fixed transaction output value that ensures the cost of change is dust.
txOutValue =
totalBalance -
transactionFees -
coinSelector.costOfChange +
1;
}

// Set the transaction output value with a random output script.
// Random hex string of 20 bytes and `0014${genRanHex}` will be a valid P2WPKH script.
const genRanHex = (size: number) =>
[...Array(size)]
.map(() => Math.floor(Math.random() * 16).toString(16))
.join('');
transaction.addOutput(
Buffer.from(`0014${genRanHex(40)}`), // Ensure that the first parameter passed to addOutput is a Buffer.
txOutValue,
);

// Select coins and change using the CoinSelector.
const { coins: selectedCoins, change } = coinSelector.select(
coins,
transaction,
);

// Assert that the selected coins' size meets the expected count.
expect(selectedCoins.length).toBe(expectedCoinCount);

// Assert the change against the expected change condition.
expect(change).toBe(expectedChange);
},
);
});

0 comments on commit ed44b85

Please sign in to comment.