diff --git a/src/_tests/unit/coinselection.unit.test.ts b/src/_tests/unit/coinselection.unit.test.ts index 55015278..f961056c 100644 --- a/src/_tests/unit/coinselection.unit.test.ts +++ b/src/_tests/unit/coinselection.unit.test.ts @@ -1,16 +1,20 @@ import assert from 'assert'; import { FewestCoinSelector } from '../../transaction/coinselector-fewest.js'; -import { UTXOLike, denominations } from '../../transaction/utxo.js'; +import { UTXO, denominations } from '../../transaction/utxo.js'; const TEST_SPEND_ADDRESS = '0x00539bc2CE3eD0FD039c582CB700EF5398bB0491'; const TEST_RECEIVE_ADDRESS = '0x02b9B1D30B6cCdc7d908B82739ce891463c3FA19'; // Utility function to create UTXOs (adjust as necessary) -function createUTXOs(denominationIndices: number[]): UTXOLike[] { - return denominationIndices.map((index) => ({ - denomination: index, - address: TEST_SPEND_ADDRESS, - })); +function createUTXOs(denominationIndices: number[]): UTXO[] { + return denominationIndices.map((index) => + UTXO.from({ + txhash: '0x0000000000000000000000000000000000000000000000000000000000000000', + index: 0, + address: TEST_SPEND_ADDRESS, + denomination: index, + }), + ); } describe('FewestCoinSelector', function () { diff --git a/src/transaction/abstract-coinselector.ts b/src/transaction/abstract-coinselector.ts index bae8434d..19fa1360 100644 --- a/src/transaction/abstract-coinselector.ts +++ b/src/transaction/abstract-coinselector.ts @@ -1,7 +1,8 @@ -import { UTXO, UTXOEntry, UTXOLike } from './utxo.js'; +import { UTXO, UTXOLike } from './utxo.js'; /** * Represents a target for spending. + * * @typedef {Object} SpendTarget * @property {string} address - The address to send to. * @property {bigint} value - The amount to send. @@ -13,6 +14,7 @@ export type SpendTarget = { /** * Represents the result of selected coins. + * * @typedef {Object} SelectedCoinsResult * @property {UTXO[]} inputs - The selected UTXOs. * @property {UTXO[]} spendOutputs - The outputs for spending. @@ -36,24 +38,26 @@ export type SelectedCoinsResult = { * @abstract */ export abstract class AbstractCoinSelector { - #availableUXTOs: UTXO[]; + #availableUTXOs: UTXO[]; #spendOutputs: UTXO[]; #changeOutputs: UTXO[]; /** * Gets the available UTXOs. + * * @returns {UTXO[]} The available UTXOs. */ - get availableUXTOs(): UTXO[] { - return this.#availableUXTOs; + get availableUTXOs(): UTXO[] { + return this.#availableUTXOs; } /** * Sets the available UTXOs. + * * @param {UTXOLike[]} value - The UTXOs to set. */ - set availableUXTOs(value: UTXOLike[]) { - this.#availableUXTOs = value.map((val) => { + set availableUTXOs(value: UTXOLike[]) { + this.#availableUTXOs = value.map((val) => { const utxo = UTXO.from(val); this._validateUTXO(utxo); return utxo; @@ -62,6 +66,7 @@ export abstract class AbstractCoinSelector { /** * Gets the spend outputs. + * * @returns {UTXO[]} The spend outputs. */ get spendOutputs(): UTXO[] { @@ -70,6 +75,7 @@ export abstract class AbstractCoinSelector { /** * Sets the spend outputs. + * * @param {UTXOLike[]} value - The spend outputs to set. */ set spendOutputs(value: UTXOLike[]) { @@ -78,6 +84,7 @@ export abstract class AbstractCoinSelector { /** * Gets the change outputs. + * * @returns {UTXO[]} The change outputs. */ get changeOutputs(): UTXO[] { @@ -86,6 +93,7 @@ export abstract class AbstractCoinSelector { /** * Sets the change outputs. + * * @param {UTXOLike[]} value - The change outputs to set. */ set changeOutputs(value: UTXOLike[]) { @@ -94,11 +102,11 @@ export abstract class AbstractCoinSelector { /** * Constructs a new AbstractCoinSelector instance with an empty UTXO array. - * @param {UTXOEntry[]} [availableUXTOs=[]] - The initial available UTXOs. + * + * @param {UTXOEntry[]} [availableUXTOs=[]] - The initial available UTXOs. Default is `[]` */ - constructor(availableUXTOs: UTXOEntry[] = []) { - this.#availableUXTOs = availableUXTOs.map((val: UTXOLike) => { - const utxo = UTXO.from(val); + constructor(availableUTXOs: UTXO[] = []) { + this.#availableUTXOs = availableUTXOs.map((utxo: UTXO) => { this._validateUTXO(utxo); return utxo; }); @@ -111,9 +119,9 @@ export abstract class AbstractCoinSelector { * UTXOs from the available UTXOs that sum to the target amount and return the selected UTXOs as well as the spend * and change outputs. * + * @abstract * @param {SpendTarget} target - The target address and value to spend. * @returns {SelectedCoinsResult} The selected UTXOs and outputs. - * @abstract */ abstract performSelection(target: SpendTarget): SelectedCoinsResult; @@ -133,5 +141,13 @@ export abstract class AbstractCoinSelector { if (utxo.denomination == null) { throw new Error('UTXO denomination is required'); } + + if (utxo.txhash == null) { + throw new Error('UTXO txhash is required'); + } + + if (utxo.index == null) { + throw new Error('UTXO index is required'); + } } } diff --git a/src/transaction/coinselector-fewest.ts b/src/transaction/coinselector-fewest.ts index ba0ad1c6..19f3201d 100644 --- a/src/transaction/coinselector-fewest.ts +++ b/src/transaction/coinselector-fewest.ts @@ -27,7 +27,7 @@ export class FewestCoinSelector extends AbstractCoinSelector { this.validateTarget(target); this.validateUTXOs(); - const sortedUTXOs = this.sortUTXOsByDenomination(this.availableUXTOs, 'desc'); + const sortedUTXOs = this.sortUTXOsByDenomination(this.availableUTXOs, 'desc'); let totalValue = BigInt(0); let selectedUTXOs: UTXO[] = []; @@ -78,34 +78,37 @@ export class FewestCoinSelector extends AbstractCoinSelector { } } - // Check if the selected UTXOs meet or exceed the target amount - if (totalValue < target.value) { - throw new Error('Insufficient funds'); - } - - // Check if any denominations can be removed from the input set and it still remain valid - selectedUTXOs = this.sortUTXOsByDenomination(selectedUTXOs, 'asc'); - + // Replace the existing optimization code with this new implementation + selectedUTXOs = this.sortUTXOsByDenomination(selectedUTXOs, 'desc'); let runningTotal = totalValue; - let lastRemovableIndex = -1; // Index of the last UTXO that can be removed - // Iterate through selectedUTXOs to find the last removable UTXO - for (let i = 0; i < selectedUTXOs.length; i++) { + for (let i = selectedUTXOs.length - 1; i >= 0; i--) { const utxo = selectedUTXOs[i]; - if (utxo.denomination !== null) { - if (runningTotal - denominations[utxo.denomination] >= target.value) { - runningTotal -= denominations[utxo.denomination]; - lastRemovableIndex = i; - } else { - // Once a UTXO makes the total less than target.value, stop the loop - break; - } + if (utxo.denomination !== null && runningTotal - denominations[utxo.denomination] >= target.value) { + runningTotal -= denominations[utxo.denomination]; + selectedUTXOs.splice(i, 1); + } else { + break; } } - if (lastRemovableIndex >= 0) { - totalValue -= denominations[selectedUTXOs[lastRemovableIndex].denomination!]; - selectedUTXOs.splice(lastRemovableIndex, 1); + totalValue = runningTotal; + + // Ensure that selectedUTXOs contain all required properties + const completeSelectedUTXOs = selectedUTXOs.map((utxo) => { + const originalUTXO = this.availableUTXOs.find( + (availableUTXO) => + availableUTXO.denomination === utxo.denomination && availableUTXO.address === utxo.address, + ); + if (!originalUTXO) { + throw new Error('Selected UTXO not found in available UTXOs'); + } + return originalUTXO; + }); + + // Check if the selected UTXOs meet or exceed the target amount + if (totalValue < target.value) { + throw new Error('Insufficient funds'); } // Break down the total spend into properly denominatated UTXOs @@ -134,7 +137,7 @@ export class FewestCoinSelector extends AbstractCoinSelector { } return { - inputs: selectedUTXOs, + inputs: completeSelectedUTXOs, spendOutputs: this.spendOutputs, changeOutputs: this.changeOutputs, }; @@ -182,7 +185,7 @@ export class FewestCoinSelector extends AbstractCoinSelector { * @throws Will throw an error if there are no available UTXOs. */ private validateUTXOs() { - if (this.availableUXTOs.length === 0) { + if (this.availableUTXOs.length === 0) { throw new Error('No UTXOs available'); } }