From 2d888281c1c061d029e5f1b45dc30c61d0234a02 Mon Sep 17 00:00:00 2001 From: Ayush Date: Sun, 18 Feb 2024 15:02:45 +0530 Subject: [PATCH] Add unit test for Coin Selector --- packages/wallet/src/coin-selector.ts | 41 ++++++++++--- packages/wallet/test/coinselector.spec.ts | 70 +++++++++++++++++++++++ 2 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 packages/wallet/test/coinselector.spec.ts diff --git a/packages/wallet/src/coin-selector.ts b/packages/wallet/src/coin-selector.ts index 39af16e..0e9197e 100644 --- a/packages/wallet/src/coin-selector.ts +++ b/packages/wallet/src/coin-selector.ts @@ -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; } @@ -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; } @@ -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; diff --git a/packages/wallet/test/coinselector.spec.ts b/packages/wallet/test/coinselector.spec.ts new file mode 100644 index 0000000..e74e4d1 --- /dev/null +++ b/packages/wallet/test/coinselector.spec.ts @@ -0,0 +1,70 @@ +import { Coin } from '../src/coin'; +import { CoinSelector } from '../src/coin-selector.ts'; +import { Transaction } from 'bitcoinjs-lib'; +import crypto from 'crypto'; + +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, + ], + // Fixed transaction output value that ensures the cost of change is dust. + // txOutValue = totalBalance - transactionFees - coinSelector.costOfChange + 1; + // transactionFees = (virtualSize + changeOutputSize) * feeRate, P2WPKH output is 31 bytes. + txOutValue: 46707, + expectedChange: 0, // Change will be zero when less than dust. + }, + { + testName: 'change should be considered when greater than dust', + testCoinValues: [4000], + txOutValue: 1000, // Less output so that the change is greater than dust. + expectedChange: 2185, // expectedChange = totalSelectedCoin - (txOutValue + transactionFees). + }, + ])('%s', ({ txOutValue, testCoinValues, expectedChange }) => { + const coins: Coin[] = []; + + // Create coins based on the fixed array of coin values. + testCoinValues.forEach((value: number) => { + const coin = new Coin({ value }); + coins.push(coin); + }); + + // Create a new Transaction + const transaction: Transaction = new Transaction(); + + // Set the transaction output value with a random output script. + // Random 20 bytes and `0014${randomBytes.toString()}` will be a valid P2WPKH script. + transaction.addOutput( + Buffer.from(`0014${crypto.randomBytes(20).toString('hex')}`), // Ensure 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 equals all available coins. + expect(selectedCoins.length).toBe(testCoinValues.length); + // Assert the change against the expected change condition. + expect(change).toBe(expectedChange); + }); +});