diff --git a/src/wallet/coin-selector.ts b/src/wallet/coin-selector.ts index 39af16e..e4b5633 100644 --- a/src/wallet/coin-selector.ts +++ b/src/wallet/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,37 @@ 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.0; + + const costOfChangeOutput = outputSize * this.feeRate; + const costOfSpendingChange = + inputSizeOfChangeUTXO * CoinSelector.LONG_TERM_FEERATE; + const costOfChange = costOfChangeOutput + costOfSpendingChange; + + return costOfChange; + } + private selectCoins(pointers: CoinPointer[], target: number) { const selected = this.selectLowestLarger(pointers, target); if (selected.length > 0) return selected; diff --git a/test/coinselector.spec.ts b/test/coinselector.spec.ts new file mode 100644 index 0000000..057ecd1 --- /dev/null +++ b/test/coinselector.spec.ts @@ -0,0 +1,59 @@ +import { Coin } from '../src/wallet/coin'; +import { CoinSelector } from '../src/wallet/coin-selector.ts'; +import { Transaction } from 'bitcoinjs-lib'; +import { validTransactions } from './fixtures/transaction'; + +describe('CoinSelector', () => { + let coins: Coin[]; + let transaction: Transaction; + let coinSelector: CoinSelector; + const feeRate: number = 5; + let totalBalance: number = 0; + + beforeAll(() => { + coinSelector = new CoinSelector(feeRate); + // Add Balance + coins = []; + for (let i = 0; i < 10; i++) { + const value = Math.random() * 1000; + const coin = new Coin({ value }); + coins.push(coin); + totalBalance += coin.value - coin.estimateSpendingFee(feeRate); + } + }); + + it('should generate cost of change', () => { + const costOfChange: number = coinSelector.costOfChange; + expect(typeof costOfChange).toBe('number'); + + // Calculate transaction output value + const txOutValue: number = Math.floor(totalBalance - costOfChange); + + // Create a new transaction + transaction = new Transaction(); + // Add an output with random scriptPubKey: We are only concered about its value + transaction.addOutput( + Buffer.from(validTransactions[0].raw.outs[0].script, 'hex'), + 1, + ); + + // Calculate fees + let fees = transaction.virtualSize() * feeRate; + fees += 31 * feeRate; // ChangeOutputFee: P2WPKH is 31 B + const finalTxOutValue = txOutValue - fees; + + // Set the transaction output value + transaction.outs[0].value = finalTxOutValue; + }); + + it('should add change to fee if it is dust', () => { + const { coins: selectCoins, change } = coinSelector.select( + coins, + transaction, + ); + expect(selectCoins.length).toBeGreaterThan(0); + + // The above calculation makes sure the Change is always less than costOfChange: Dust + expect(change).toBe(0); + }); +});