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

wallet: drop change to fee if amount is less than dust #32

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 39 additions & 2 deletions packages/wallet/src/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 All @@ -36,20 +37,56 @@ export class CoinSelector {

const selected = this.selectCoins(pointers, target);

const change =
let change =
selected.reduce(
(acc, index) => acc + pointers[index].effectiveValue,
0,
) - target;

// TODO: check if change is lower than dust limit, if so add it to fee
// Calculate the cost of change
const costOfChange = this.costOfChange;

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

return {
coins: selected.map((index) => pointers[index].coin),
change,
};
}

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
70 changes: 70 additions & 0 deletions packages/wallet/test/coinselector.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading