Skip to content

Commit

Permalink
refactor coinselector to work with UTXO objects
Browse files Browse the repository at this point in the history
  • Loading branch information
alejoacosta74 authored and rileystephens28 committed Sep 26, 2024
1 parent 4f90044 commit 2e8fd75
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 42 deletions.
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

0 comments on commit 2e8fd75

Please sign in to comment.