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

refactor coinselector to work with UTXO objects #305

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
16 changes: 10 additions & 6 deletions src/_tests/unit/coinselection.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down
38 changes: 27 additions & 11 deletions src/transaction/abstract-coinselector.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
Expand All @@ -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;
Expand All @@ -62,6 +66,7 @@ export abstract class AbstractCoinSelector {

/**
* Gets the spend outputs.
*
* @returns {UTXO[]} The spend outputs.
*/
get spendOutputs(): UTXO[] {
Expand All @@ -70,6 +75,7 @@ export abstract class AbstractCoinSelector {

/**
* Sets the spend outputs.
*
* @param {UTXOLike[]} value - The spend outputs to set.
*/
set spendOutputs(value: UTXOLike[]) {
Expand All @@ -78,6 +84,7 @@ export abstract class AbstractCoinSelector {

/**
* Gets the change outputs.
*
* @returns {UTXO[]} The change outputs.
*/
get changeOutputs(): UTXO[] {
Expand All @@ -86,6 +93,7 @@ export abstract class AbstractCoinSelector {

/**
* Sets the change outputs.
*
* @param {UTXOLike[]} value - The change outputs to set.
*/
set changeOutputs(value: UTXOLike[]) {
Expand All @@ -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;
});
Expand All @@ -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;

Expand All @@ -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');
}
}
}
53 changes: 28 additions & 25 deletions src/transaction/coinselector-fewest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -134,7 +137,7 @@ export class FewestCoinSelector extends AbstractCoinSelector {
}

return {
inputs: selectedUTXOs,
inputs: completeSelectedUTXOs,
spendOutputs: this.spendOutputs,
changeOutputs: this.changeOutputs,
};
Expand Down Expand Up @@ -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');
}
}
Expand Down
Loading