From 235d5d90331341b59670b4b1abfb175656fd5f69 Mon Sep 17 00:00:00 2001 From: rileystephens28 Date: Thu, 7 Mar 2024 17:46:02 -0600 Subject: [PATCH 1/6] Initial version of largest first coin selection implementation --- src.ts/transaction/utxo.ts | 319 +++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 src.ts/transaction/utxo.ts diff --git a/src.ts/transaction/utxo.ts b/src.ts/transaction/utxo.ts new file mode 100644 index 00000000..5ecc3364 --- /dev/null +++ b/src.ts/transaction/utxo.ts @@ -0,0 +1,319 @@ +import { getAddress } from "../address/index.js"; +import { getBigInt } from "../utils/index.js"; +import type { BigNumberish } from "../utils/index.js"; + +type OutPoint = { + txhash: string; + index: number; +}; + +type UTXOTransactionInput = { + previousOutPoint: OutPoint; + pubKey: Uint8Array; +}; + +export interface UTXOEntry { + denomination: null | bigint; + address: null | string; +}; + +export type UTXOTransactionOutput = UTXOEntry; + +export type UTXOTransaction = { + chainId: bigint; + inputs: UTXOTransactionInput[]; + outputs: UTXOTransactionOutput[]; + signature?: Uint8Array; +}; + +export interface UTXOLike extends UTXOEntry { + txhash?: null | string; + index?: null | number; +} + +export type SpendTarget = { + address: string; + value: bigint; +}; + +export const denominations: bigint[] = [ + BigInt(1), // 0.001 Qi + BigInt(5), // 0.005 Qi + BigInt(10), // 0.01 Qi + BigInt(50), // 0.05 Qi + BigInt(100), // 0.1 Qi + BigInt(250), // 0.25 Qi + BigInt(500), // 0.5 Qi + BigInt(1000), // 1 Qi + BigInt(5000), // 5 Qi + BigInt(10000), // 10 Qi + BigInt(20000), // 20 Qi + BigInt(50000), // 50 Qi + BigInt(100000), // 100 Qi + BigInt(1000000), // 1000 Qi + BigInt(10000000), // 10000 Qi + BigInt(100000000), // 100000 Qi + BigInt(1000000000), // 1000000 Qi +]; + +/** + * Checks if the provided denomination is valid. + * @param denomination The denomination to check. + * @returns True if the denomination is valid, false otherwise. + */ +function isValidDenomination(denomination: bigint): boolean { + return denominations.includes(denomination); +} + +/** + * Handles conversion of string to bigint, specifically for transaction parameters. + * @param value The string value to convert. + * @param param The parameter name for error context. + * @returns The bigint representation of the input string. + */ +function handleBigInt(value: string, param: string): bigint { + if (value === "0x") { return BigInt(0); } + return getBigInt(value, param); +} + +export class UTXO implements UTXOLike { + #txhash: null | string; + #index: null | number; + #address: null | string; + #denomination: null | bigint; + + get txhash(): null | string { return this.#txhash; } + set txhash(value: null | string) { + this.#txhash = value; + } + + get index(): null | number { return this.#index; } + set index(value: null | number) { + this.#index = value; + } + + get address(): null | string { return this.#address; } + set address(value: null | string) { + this.#address = (value == null) ? null : getAddress(value); + } + + get denomination(): null | bigint { return this.#denomination; } + set denomination(value: null | BigNumberish) { + if (value == null) { + this.#denomination = null; + return; + } + + const denominationBigInt = handleBigInt(value.toString(), "denomination"); + if (!isValidDenomination(denominationBigInt)) { + throw new Error("Invalid denomination value"); + } + + this.#denomination = denominationBigInt; + } + + /** + * Constructs a new UTXO instance with null properties. + */ + constructor() { + this.#txhash = null; + this.#index = null; + this.#address = null; + this.#denomination = null; + } + + /** + * Converts the UTXO instance to a JSON object. + * @returns A JSON representation of the UTXO instance. + */ + toJSON(): any { + return { + txhash: this.txhash, + index: this.index, + address: this.address, + denomination: this.denomination ? this.denomination.toString() : null, + }; + } + + /** + * Creates a UTXO instance from a UTXOLike object. + * @param utxo The UTXOLike object to create the UTXO instance from. + * @returns A new UTXO instance. + */ + static from(utxo: UTXOLike): UTXO { + if (utxo === null) { return new UTXO(); } + + const result = utxo instanceof UTXO ? utxo : new UTXO(); + if (utxo.txhash != null) { result.txhash = utxo.txhash; } + if (utxo.index != null) { result.index = utxo.index; } + if (utxo.address != null) { result.address = utxo.address; } + if (utxo.denomination != null) { result.denomination = utxo.denomination; } + + return result; + } +} + +type SelectedCoinsResult = { + inputs: UTXO[]; + spendOutputs: UTXO[]; + changeOutputs: UTXO[]; +}; + +/** + * The CoinSelector class is used to select available UTXOs for + * spending and to properly handle spend and change outputs. + */ +export class CoinSelector { + #availableUXTOs: UTXO[]; + #spendOutputs: UTXO[]; + #changeOutputs: UTXO[]; + + get availableUXTOs(): UTXO[] { return this.#availableUXTOs; } + set availableUXTOs(value: UTXOLike[]) { + this.#availableUXTOs = value.map((val: UTXOLike) => { + const utxo = UTXO.from(val); + this.#validateUTXO(utxo); + return utxo; + }); + } + + get spendOutputs(): UTXO[] { return this.#spendOutputs; } + set spendOutputs(value: UTXOLike[]) { + this.#spendOutputs = value.map((utxo: UTXOLike) => UTXO.from(utxo)); + } + + get changeOutputs(): UTXO[] { return this.#changeOutputs; } + set changeOutputs(value: UTXOLike[]) { + this.#changeOutputs = value.map((utxo: UTXOLike) => UTXO.from(utxo)); + } + + /** + * Constructs a new CoinSelector instance with an empty UTXO array. + */ + constructor() { + this.#availableUXTOs = []; + this.#spendOutputs = []; + this.#changeOutputs = []; + } + + + /** + * The largest first coin selection algorithm. + * + * This algorithm selects the largest UTXOs first, and continues to select UTXOs until the + * target amount is reached. If the total value of the selected UTXOs is greater than the + * target amount, the remaining value is returned as a change output. + * @param target The target amount to select UTXOs for. + */ + largestFirst(target: SpendTarget): SelectedCoinsResult { + if (target.value <= BigInt(0)) { + throw new Error("Target amount must be greater than 0"); + } + + if (this.availableUXTOs.length === 0) { + throw new Error("No UTXOs available"); + } + + // Sort UTXOs in descending order based on their denomination + const sortedUTXOs = this.availableUXTOs.sort((a, b) => { + const diff = (b.denomination ?? BigInt(0)) - (a.denomination ?? BigInt(0)); + return diff > 0 ? 1 : diff < 0 ? -1 : 0; + }); + + let totalValue = BigInt(0); + const selectedUTXOs: UTXO[] = []; + + // Select UTXOs until the target amount is reached or exceeded + for (const utxo of sortedUTXOs) { + if (utxo.denomination == null) continue; // Skip UTXOs without a denomination + if (totalValue >= target.value) break; // Stop if we've reached or exceeded the target + + selectedUTXOs.push(utxo); + totalValue += utxo.denomination; + } + + // 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 + const spendDenominations = this.#denominate(target.value); + this.spendOutputs = spendDenominations.map(denomination => { + const utxo = new UTXO(); + utxo.denomination = denomination; + utxo.address = target.address; + return utxo; + }); + + // Calculate change if the total value exceeds the target + const change = totalValue - target.value; + + // If there's change, break it down into properly denominatated UTXOs + if (change > BigInt(0)) { + const changeDenominations = this.#denominate(change); + this.changeOutputs = changeDenominations.map(denomination => { + const utxo = new UTXO(); + utxo.denomination = denomination; + // We do not have access to change addresses here so leave it null + return utxo; + }); + } else { + this.changeOutputs = []; + } + + return { + inputs: selectedUTXOs, + spendOutputs: this.spendOutputs, + changeOutputs: this.changeOutputs, + }; + } + + /** + * Validates the provided UTXO instance. In order to be valid for coin + * selection, the UTXO must have a valid address and denomination. + * @param utxo The UTXO instance to validate. + * @throws An error if the UTXO instance is invalid. + */ + #validateUTXO(utxo: UTXO): void { + if (utxo.address == null) { + throw new Error("UTXO address is required"); + } + + if (utxo.denomination == null) { + throw new Error("UTXO denomination is required"); + } + } + + /** + * Given a value, returns an array of supported denominations that sum to the value. + * @param value The value to denominate. + * @returns Array of supported denominations that sum to the value. + */ + #denominate(value: bigint): bigint[] { + if (value <= BigInt(0)) { + throw new Error("Value must be greater than 0"); + } + + const result: bigint[] = []; + let remainingValue = value; + + // Iterate through denominations in descending order + for (let i = denominations.length - 1; i >= 0; i--) { + const denomination = denominations[i]; + + // Add the denomination to the result array as many times as possible + while (remainingValue >= denomination) { + result.push(denomination); + remainingValue -= denomination; + } + } + + if (remainingValue > 0) { + throw new Error("Unable to match the value with available denominations"); + } + + return result; + } + +} From eb565404b4f9d6d7d3cb3da38933b132b9b9dac4 Mon Sep 17 00:00:00 2001 From: rileystephens28 Date: Fri, 8 Mar 2024 10:07:57 -0600 Subject: [PATCH 2/6] Optimize UXTO selection --- src.ts/transaction/utxo.ts | 45 ++++++++++++++++++++++++++++++++------ src.ts/utils/maths.ts | 31 ++++++++++++++++++-------- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src.ts/transaction/utxo.ts b/src.ts/transaction/utxo.ts index 5ecc3364..563460e4 100644 --- a/src.ts/transaction/utxo.ts +++ b/src.ts/transaction/utxo.ts @@ -1,6 +1,7 @@ import { getAddress } from "../address/index.js"; import { getBigInt } from "../utils/index.js"; import type { BigNumberish } from "../utils/index.js"; +import { bigIntAbs } from "../utils/maths.js"; type OutPoint = { txhash: string; @@ -205,7 +206,7 @@ export class CoinSelector { * target amount, the remaining value is returned as a change output. * @param target The target amount to select UTXOs for. */ - largestFirst(target: SpendTarget): SelectedCoinsResult { + performSelection(target: SpendTarget): SelectedCoinsResult { if (target.value <= BigInt(0)) { throw new Error("Target amount must be greater than 0"); } @@ -223,13 +224,43 @@ export class CoinSelector { let totalValue = BigInt(0); const selectedUTXOs: UTXO[] = []; - // Select UTXOs until the target amount is reached or exceeded - for (const utxo of sortedUTXOs) { - if (utxo.denomination == null) continue; // Skip UTXOs without a denomination - if (totalValue >= target.value) break; // Stop if we've reached or exceeded the target + // Get UTXOs that meets or exceeds the target value + const UTXOsEqualOrGreaterThanTarget = sortedUTXOs.filter(utxo => utxo.denomination && utxo.denomination >= target.value); - selectedUTXOs.push(utxo); - totalValue += utxo.denomination; + if (UTXOsEqualOrGreaterThanTarget.length > 0) { + // Find the smallest UTXO that meets or exceeds the target value + const optimalUTXO = UTXOsEqualOrGreaterThanTarget.reduce((minDenominationUTXO, currentUTXO) => { + if (!currentUTXO.denomination) return minDenominationUTXO; + return currentUTXO.denomination < minDenominationUTXO.denomination! ? currentUTXO : minDenominationUTXO; + }, UTXOsEqualOrGreaterThanTarget[0]); // Initialize with the first UTXO in the list + + selectedUTXOs.push(optimalUTXO); + totalValue += optimalUTXO.denomination!; + } else { + // If no single UTXO meets or exceeds the target, aggregate smaller denominations + // until the target is met/exceeded or there are no more UTXOs to aggregate + while (sortedUTXOs.length > 0 && totalValue < target.value) { + const nextOptimalUTXO = sortedUTXOs.reduce((closest, utxo) => { + if (!utxo.denomination) return closest; + + // Prioritize UTXOs that bring totalValue closer to target.value + const absThisDiff = bigIntAbs(target.value - (totalValue + utxo.denomination)); + const currentClosestDiff = closest && closest.denomination + ? bigIntAbs(target.value - (totalValue + closest.denomination)) + : BigInt(Infinity); + + return absThisDiff < currentClosestDiff ? utxo : closest; + + }, sortedUTXOs[0]); + + // Add the selected UTXO to the selection and update totalValue + selectedUTXOs.push(nextOptimalUTXO); + totalValue += nextOptimalUTXO.denomination!; + + // Remove the selected UTXO from the list of available UTXOs + const index = sortedUTXOs.findIndex(utxo => utxo.denomination === nextOptimalUTXO.denomination && utxo.address === nextOptimalUTXO.address); + sortedUTXOs.splice(index, 1); + } } // Check if the selected UTXOs meet or exceed the target amount diff --git a/src.ts/utils/maths.ts b/src.ts/utils/maths.ts index 0200c27a..6a8bd278 100644 --- a/src.ts/utils/maths.ts +++ b/src.ts/utils/maths.ts @@ -93,7 +93,7 @@ export function mask(_value: BigNumberish, _bits: Numeric): bigint { * a BigInt, then an ArgumentError will be thrown for %%name%%. */ export function getBigInt(value: BigNumberish, name?: string): bigint { - switch (typeof(value)) { + switch (typeof (value)) { case "bigint": return value; case "number": assertArgument(Number.isInteger(value), "underflow", name || "value", value); @@ -106,13 +106,26 @@ export function getBigInt(value: BigNumberish, name?: string): bigint { return -BigInt(value.substring(1)); } return BigInt(value); - } catch(e: any) { - assertArgument(false, `invalid BigNumberish string: ${ e.message }`, name || "value", value); + } catch (e: any) { + assertArgument(false, `invalid BigNumberish string: ${e.message}`, name || "value", value); } } assertArgument(false, "invalid BigNumberish value", name || "value", value); } +/** + * Returns absolute value of bigint %%value%%. + */ +export function bigIntAbs(value: BigNumberish): bigint { + value = getBigInt(value); + + // if value is negative (including -0), return -value, else return value + if (value === -BN_0 || value < BN_0) { + return -value; + } + return value; +} + /** * Returns %%value%% as a bigint, validating it is valid as a bigint * value and that it is positive. @@ -149,7 +162,7 @@ export function toBigInt(value: BigNumberish | Uint8Array): bigint { * a //number//, then an ArgumentError will be thrown for %%name%%. */ export function getNumber(value: BigNumberish, name?: string): number { - switch (typeof(value)) { + switch (typeof (value)) { case "bigint": assertArgument(value >= -maxValue && value <= maxValue, "overflow", name || "value", value); return Number(value); @@ -161,8 +174,8 @@ export function getNumber(value: BigNumberish, name?: string): number { try { if (value === "") { throw new Error("empty string"); } return getNumber(BigInt(value), name); - } catch(e: any) { - assertArgument(false, `invalid numeric string: ${ e.message }`, name || "value", value); + } catch (e: any) { + assertArgument(false, `invalid numeric string: ${e.message}`, name || "value", value); } } assertArgument(false, "invalid numeric value", name || "value", value); @@ -191,7 +204,7 @@ export function toBeHex(_value: BigNumberish, _width?: Numeric): string { if (result.length % 2) { result = "0" + result; } } else { const width = getNumber(_width, "width"); - assert(width * 2 >= result.length, `value exceeds width (${ width } bytes)`, "NUMERIC_FAULT", { + assert(width * 2 >= result.length, `value exceeds width (${width} bytes)`, "NUMERIC_FAULT", { operation: "toBeHex", fault: "overflow", value: _value @@ -211,7 +224,7 @@ export function toBeHex(_value: BigNumberish, _width?: Numeric): string { export function toBeArray(_value: BigNumberish): Uint8Array { const value = getUint(_value, "value"); - if (value === BN_0) { return new Uint8Array([ ]); } + if (value === BN_0) { return new Uint8Array([]); } let hex = value.toString(16); if (hex.length % 2) { hex = "0" + hex; } @@ -233,7 +246,7 @@ export function toBeArray(_value: BigNumberish): Uint8Array { * numeric values. */ export function toQuantity(value: BytesLike | BigNumberish): string { - let result = hexlify(isBytesLike(value) ? value: toBeArray(value)).substring(2); + let result = hexlify(isBytesLike(value) ? value : toBeArray(value)).substring(2); while (result.startsWith("0")) { result = result.substring(1); } if (result === "") { result = "0"; } return "0x" + result; From cca365e2440f43ee2bdb900da01b138ac0a5d0c2 Mon Sep 17 00:00:00 2001 From: rileystephens28 Date: Fri, 8 Mar 2024 10:34:44 -0600 Subject: [PATCH 3/6] Add AbstractCoinSelector base class for coin selection logic and create FewestCoinSelector subclass --- src.ts/transaction/abstract-coinselector.ts | 89 ++++++++ src.ts/transaction/coinselector-fewest.ts | 118 ++++++++++ src.ts/transaction/utxo.ts | 236 +++----------------- 3 files changed, 240 insertions(+), 203 deletions(-) create mode 100644 src.ts/transaction/abstract-coinselector.ts create mode 100644 src.ts/transaction/coinselector-fewest.ts diff --git a/src.ts/transaction/abstract-coinselector.ts b/src.ts/transaction/abstract-coinselector.ts new file mode 100644 index 00000000..26c3ff4c --- /dev/null +++ b/src.ts/transaction/abstract-coinselector.ts @@ -0,0 +1,89 @@ +import { getAddress } from "../address/index.js"; +import { getBigInt } from "../utils/index.js"; +import type { BigNumberish } from "../utils/index.js"; +import { bigIntAbs } from "../utils/maths.js"; +import { UTXO, UTXOLike, denominations } from "./utxo.js"; + +export type SpendTarget = { + address: string; + value: bigint; +}; + +export type SelectedCoinsResult = { + inputs: UTXO[]; + spendOutputs: UTXO[]; + changeOutputs: UTXO[]; +}; + +/** + * An **AbstractCoinSelector** provides a base class for other sub-classes to + * implement the functionality for selecting UTXOs for a spend and to properly + * handle spend and change outputs. + * + * This class is abstract and should not be used directly. Sub-classes should + * implement the [[performSelection]] method to provide the actual coin + * selection logic. + * + * @abstract + */ +export abstract class AbstractCoinSelector { + #availableUXTOs: UTXO[]; + #spendOutputs: UTXO[]; + #changeOutputs: UTXO[]; + + get availableUXTOs(): UTXO[] { return this.#availableUXTOs; } + set availableUXTOs(value: UTXOLike[]) { + this.#availableUXTOs = value.map((val: UTXOLike) => { + const utxo = UTXO.from(val); + this._validateUTXO(utxo); + return utxo; + }); + } + + get spendOutputs(): UTXO[] { return this.#spendOutputs; } + set spendOutputs(value: UTXOLike[]) { + this.#spendOutputs = value.map((utxo: UTXOLike) => UTXO.from(utxo)); + } + + get changeOutputs(): UTXO[] { return this.#changeOutputs; } + set changeOutputs(value: UTXOLike[]) { + this.#changeOutputs = value.map((utxo: UTXOLike) => UTXO.from(utxo)); + } + + /** + * Constructs a new AbstractCoinSelector instance with an empty UTXO array. + */ + constructor() { + this.#availableUXTOs = []; + this.#spendOutputs = []; + this.#changeOutputs = []; + } + + /** + * This method should be implemented by sub-classes to provide the actual + * coin selection logic. It should select UTXOs from the available UTXOs + * that sum to the target amount and return the selected UTXOs as well as + * the spend and change outputs. + * @param target The target amount to select UTXOs for. + */ + abstract performSelection(target: SpendTarget): SelectedCoinsResult; + + /** + * Validates the provided UTXO instance. In order to be valid for coin + * selection, the UTXO must have a valid address and denomination. + * @param utxo The UTXO instance to validate. + * @throws An error if the UTXO instance is invalid. + */ + protected _validateUTXO(utxo: UTXO): void { + if (utxo.address == null) { + throw new Error("UTXO address is required"); + } + + if (utxo.denomination == null) { + throw new Error("UTXO denomination is required"); + } + } + + + +} diff --git a/src.ts/transaction/coinselector-fewest.ts b/src.ts/transaction/coinselector-fewest.ts new file mode 100644 index 00000000..6c3b4db2 --- /dev/null +++ b/src.ts/transaction/coinselector-fewest.ts @@ -0,0 +1,118 @@ +import { bigIntAbs } from "../utils/maths.js"; +import { AbstractCoinSelector, SelectedCoinsResult, SpendTarget } from "./abstract-coinselector.js"; +import { UTXO, denominate } from "./utxo.js"; + + +/** + * The FewestCoinSelector class provides a coin selection algorithm that selects + * the fewest UTXOs required to meet the target amount. This algorithm is useful + * for minimizing the size of the transaction and the fees associated with it. + * + * This class is a sub-class of [[AbstractCoinSelector]] and implements the + * [[performSelection]] method to provide the actual coin selection logic. + */ +export class FewestCoinSelector extends AbstractCoinSelector { + + /** + * The largest first coin selection algorithm. + * + * This algorithm selects the largest UTXOs first, and continues to select UTXOs until the + * target amount is reached. If the total value of the selected UTXOs is greater than the + * target amount, the remaining value is returned as a change output. + * @param target The target amount to select UTXOs for. + */ + performSelection(target: SpendTarget): SelectedCoinsResult { + if (target.value <= BigInt(0)) { + throw new Error("Target amount must be greater than 0"); + } + + if (this.availableUXTOs.length === 0) { + throw new Error("No UTXOs available"); + } + + // Sort UTXOs in descending order based on their denomination + const sortedUTXOs = this.availableUXTOs.sort((a, b) => { + const diff = (b.denomination ?? BigInt(0)) - (a.denomination ?? BigInt(0)); + return diff > 0 ? 1 : diff < 0 ? -1 : 0; + }); + + let totalValue = BigInt(0); + const selectedUTXOs: UTXO[] = []; + + // Get UTXOs that meets or exceeds the target value + const UTXOsEqualOrGreaterThanTarget = sortedUTXOs.filter(utxo => utxo.denomination && utxo.denomination >= target.value); + + if (UTXOsEqualOrGreaterThanTarget.length > 0) { + // Find the smallest UTXO that meets or exceeds the target value + const optimalUTXO = UTXOsEqualOrGreaterThanTarget.reduce((minDenominationUTXO, currentUTXO) => { + if (!currentUTXO.denomination) return minDenominationUTXO; + return currentUTXO.denomination < minDenominationUTXO.denomination! ? currentUTXO : minDenominationUTXO; + }, UTXOsEqualOrGreaterThanTarget[0]); // Initialize with the first UTXO in the list + + selectedUTXOs.push(optimalUTXO); + totalValue += optimalUTXO.denomination!; + } else { + // If no single UTXO meets or exceeds the target, aggregate smaller denominations + // until the target is met/exceeded or there are no more UTXOs to aggregate + while (sortedUTXOs.length > 0 && totalValue < target.value) { + const nextOptimalUTXO = sortedUTXOs.reduce((closest, utxo) => { + if (!utxo.denomination) return closest; + + // Prioritize UTXOs that bring totalValue closer to target.value + const absThisDiff = bigIntAbs(target.value - (totalValue + utxo.denomination)); + const currentClosestDiff = closest && closest.denomination + ? bigIntAbs(target.value - (totalValue + closest.denomination)) + : BigInt(Infinity); + + return absThisDiff < currentClosestDiff ? utxo : closest; + + }, sortedUTXOs[0]); + + // Add the selected UTXO to the selection and update totalValue + selectedUTXOs.push(nextOptimalUTXO); + totalValue += nextOptimalUTXO.denomination!; + + // Remove the selected UTXO from the list of available UTXOs + const index = sortedUTXOs.findIndex(utxo => utxo.denomination === nextOptimalUTXO.denomination && utxo.address === nextOptimalUTXO.address); + sortedUTXOs.splice(index, 1); + } + } + + // 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 + const spendDenominations = denominate(target.value); + this.spendOutputs = spendDenominations.map(denomination => { + const utxo = new UTXO(); + utxo.denomination = denomination; + utxo.address = target.address; + return utxo; + }); + + // Calculate change to be returned + const change = totalValue - target.value; + + // If there's change, break it down into properly denominatated UTXOs + if (change > BigInt(0)) { + const changeDenominations = denominate(change); + this.changeOutputs = changeDenominations.map(denomination => { + const utxo = new UTXO(); + utxo.denomination = denomination; + // We do not have access to change addresses here so leave it null + return utxo; + }); + } else { + this.changeOutputs = []; + } + + return { + inputs: selectedUTXOs, + spendOutputs: this.spendOutputs, + changeOutputs: this.changeOutputs, + }; + } + +} diff --git a/src.ts/transaction/utxo.ts b/src.ts/transaction/utxo.ts index 563460e4..f3cec2d4 100644 --- a/src.ts/transaction/utxo.ts +++ b/src.ts/transaction/utxo.ts @@ -1,14 +1,13 @@ import { getAddress } from "../address/index.js"; import { getBigInt } from "../utils/index.js"; import type { BigNumberish } from "../utils/index.js"; -import { bigIntAbs } from "../utils/maths.js"; -type OutPoint = { +export type OutPoint = { txhash: string; index: number; }; -type UTXOTransactionInput = { +export type UTXOTransactionInput = { previousOutPoint: OutPoint; pubKey: Uint8Array; }; @@ -32,11 +31,6 @@ export interface UTXOLike extends UTXOEntry { index?: null | number; } -export type SpendTarget = { - address: string; - value: bigint; -}; - export const denominations: bigint[] = [ BigInt(1), // 0.001 Qi BigInt(5), // 0.005 Qi @@ -77,6 +71,37 @@ function handleBigInt(value: string, param: string): bigint { return getBigInt(value, param); } +/** + * Given a value, returns an array of supported denominations that sum to the value. + * @param value The value to denominate. + * @returns Array of supported denominations that sum to the value. + */ +export function denominate(value: bigint): bigint[] { + if (value <= BigInt(0)) { + throw new Error("Value must be greater than 0"); + } + + const result: bigint[] = []; + let remainingValue = value; + + // Iterate through denominations in descending order + for (let i = denominations.length - 1; i >= 0; i--) { + const denomination = denominations[i]; + + // Add the denomination to the result array as many times as possible + while (remainingValue >= denomination) { + result.push(denomination); + remainingValue -= denomination; + } + } + + if (remainingValue > 0) { + throw new Error("Unable to match the value with available denominations"); + } + + return result; +} + export class UTXO implements UTXOLike { #txhash: null | string; #index: null | number; @@ -153,198 +178,3 @@ export class UTXO implements UTXOLike { return result; } } - -type SelectedCoinsResult = { - inputs: UTXO[]; - spendOutputs: UTXO[]; - changeOutputs: UTXO[]; -}; - -/** - * The CoinSelector class is used to select available UTXOs for - * spending and to properly handle spend and change outputs. - */ -export class CoinSelector { - #availableUXTOs: UTXO[]; - #spendOutputs: UTXO[]; - #changeOutputs: UTXO[]; - - get availableUXTOs(): UTXO[] { return this.#availableUXTOs; } - set availableUXTOs(value: UTXOLike[]) { - this.#availableUXTOs = value.map((val: UTXOLike) => { - const utxo = UTXO.from(val); - this.#validateUTXO(utxo); - return utxo; - }); - } - - get spendOutputs(): UTXO[] { return this.#spendOutputs; } - set spendOutputs(value: UTXOLike[]) { - this.#spendOutputs = value.map((utxo: UTXOLike) => UTXO.from(utxo)); - } - - get changeOutputs(): UTXO[] { return this.#changeOutputs; } - set changeOutputs(value: UTXOLike[]) { - this.#changeOutputs = value.map((utxo: UTXOLike) => UTXO.from(utxo)); - } - - /** - * Constructs a new CoinSelector instance with an empty UTXO array. - */ - constructor() { - this.#availableUXTOs = []; - this.#spendOutputs = []; - this.#changeOutputs = []; - } - - - /** - * The largest first coin selection algorithm. - * - * This algorithm selects the largest UTXOs first, and continues to select UTXOs until the - * target amount is reached. If the total value of the selected UTXOs is greater than the - * target amount, the remaining value is returned as a change output. - * @param target The target amount to select UTXOs for. - */ - performSelection(target: SpendTarget): SelectedCoinsResult { - if (target.value <= BigInt(0)) { - throw new Error("Target amount must be greater than 0"); - } - - if (this.availableUXTOs.length === 0) { - throw new Error("No UTXOs available"); - } - - // Sort UTXOs in descending order based on their denomination - const sortedUTXOs = this.availableUXTOs.sort((a, b) => { - const diff = (b.denomination ?? BigInt(0)) - (a.denomination ?? BigInt(0)); - return diff > 0 ? 1 : diff < 0 ? -1 : 0; - }); - - let totalValue = BigInt(0); - const selectedUTXOs: UTXO[] = []; - - // Get UTXOs that meets or exceeds the target value - const UTXOsEqualOrGreaterThanTarget = sortedUTXOs.filter(utxo => utxo.denomination && utxo.denomination >= target.value); - - if (UTXOsEqualOrGreaterThanTarget.length > 0) { - // Find the smallest UTXO that meets or exceeds the target value - const optimalUTXO = UTXOsEqualOrGreaterThanTarget.reduce((minDenominationUTXO, currentUTXO) => { - if (!currentUTXO.denomination) return minDenominationUTXO; - return currentUTXO.denomination < minDenominationUTXO.denomination! ? currentUTXO : minDenominationUTXO; - }, UTXOsEqualOrGreaterThanTarget[0]); // Initialize with the first UTXO in the list - - selectedUTXOs.push(optimalUTXO); - totalValue += optimalUTXO.denomination!; - } else { - // If no single UTXO meets or exceeds the target, aggregate smaller denominations - // until the target is met/exceeded or there are no more UTXOs to aggregate - while (sortedUTXOs.length > 0 && totalValue < target.value) { - const nextOptimalUTXO = sortedUTXOs.reduce((closest, utxo) => { - if (!utxo.denomination) return closest; - - // Prioritize UTXOs that bring totalValue closer to target.value - const absThisDiff = bigIntAbs(target.value - (totalValue + utxo.denomination)); - const currentClosestDiff = closest && closest.denomination - ? bigIntAbs(target.value - (totalValue + closest.denomination)) - : BigInt(Infinity); - - return absThisDiff < currentClosestDiff ? utxo : closest; - - }, sortedUTXOs[0]); - - // Add the selected UTXO to the selection and update totalValue - selectedUTXOs.push(nextOptimalUTXO); - totalValue += nextOptimalUTXO.denomination!; - - // Remove the selected UTXO from the list of available UTXOs - const index = sortedUTXOs.findIndex(utxo => utxo.denomination === nextOptimalUTXO.denomination && utxo.address === nextOptimalUTXO.address); - sortedUTXOs.splice(index, 1); - } - } - - // 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 - const spendDenominations = this.#denominate(target.value); - this.spendOutputs = spendDenominations.map(denomination => { - const utxo = new UTXO(); - utxo.denomination = denomination; - utxo.address = target.address; - return utxo; - }); - - // Calculate change if the total value exceeds the target - const change = totalValue - target.value; - - // If there's change, break it down into properly denominatated UTXOs - if (change > BigInt(0)) { - const changeDenominations = this.#denominate(change); - this.changeOutputs = changeDenominations.map(denomination => { - const utxo = new UTXO(); - utxo.denomination = denomination; - // We do not have access to change addresses here so leave it null - return utxo; - }); - } else { - this.changeOutputs = []; - } - - return { - inputs: selectedUTXOs, - spendOutputs: this.spendOutputs, - changeOutputs: this.changeOutputs, - }; - } - - /** - * Validates the provided UTXO instance. In order to be valid for coin - * selection, the UTXO must have a valid address and denomination. - * @param utxo The UTXO instance to validate. - * @throws An error if the UTXO instance is invalid. - */ - #validateUTXO(utxo: UTXO): void { - if (utxo.address == null) { - throw new Error("UTXO address is required"); - } - - if (utxo.denomination == null) { - throw new Error("UTXO denomination is required"); - } - } - - /** - * Given a value, returns an array of supported denominations that sum to the value. - * @param value The value to denominate. - * @returns Array of supported denominations that sum to the value. - */ - #denominate(value: bigint): bigint[] { - if (value <= BigInt(0)) { - throw new Error("Value must be greater than 0"); - } - - const result: bigint[] = []; - let remainingValue = value; - - // Iterate through denominations in descending order - for (let i = denominations.length - 1; i >= 0; i--) { - const denomination = denominations[i]; - - // Add the denomination to the result array as many times as possible - while (remainingValue >= denomination) { - result.push(denomination); - remainingValue -= denomination; - } - } - - if (remainingValue > 0) { - throw new Error("Unable to match the value with available denominations"); - } - - return result; - } - -} From fe78574cabcb2bd6a3943ffc120618610fdfc48b Mon Sep 17 00:00:00 2001 From: rileystephens28 Date: Fri, 8 Mar 2024 17:01:16 -0600 Subject: [PATCH 4/6] Test coin selection --- src.ts/_tests/test-utxo-coinselection.ts | 119 ++++++++++++++++++++ src.ts/transaction/abstract-coinselector.ts | 14 +-- src.ts/transaction/utxo.ts | 10 +- 3 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 src.ts/_tests/test-utxo-coinselection.ts diff --git a/src.ts/_tests/test-utxo-coinselection.ts b/src.ts/_tests/test-utxo-coinselection.ts new file mode 100644 index 00000000..91979404 --- /dev/null +++ b/src.ts/_tests/test-utxo-coinselection.ts @@ -0,0 +1,119 @@ +import assert from "assert"; +import { FewestCoinSelector } from "../transaction/coinselector-fewest"; +import { UTXOLike, denominations } from "../transaction/utxo"; + +const TEST_SPEND_ADDRESS = "0x00539bc2CE3eD0FD039c582CB700EF5398bB0491"; +const TEST_RECEIVE_ADDRESS = "0x02b9B1D30B6cCdc7d908B82739ce891463c3FA19"; + +// Utility function to create UTXOs (adjust as necessary) +function createUTXOs(denominations: bigint[]): UTXOLike[] { + return denominations.map(denomination => ({ + denomination, + address: TEST_SPEND_ADDRESS + })); +} + +describe("FewestCoinSelector", function () { + describe("Selecting valid UTXOs", function () { + it("selects a single UTXO that exactly matches the target amount", function () { + const availableUTXOs = createUTXOs([denominations[1], denominations[2], denominations[3]]); // .065 Qi + const targetSpend = { value: denominations[3], address: TEST_RECEIVE_ADDRESS }; // .05 Qi + const selector = new FewestCoinSelector(availableUTXOs); + const result = selector.performSelection(targetSpend); + + // A single 0.05 Qi UTXO should have been selected + assert.strictEqual(result.inputs.length, 1); + assert.strictEqual(result.inputs[0].denomination, denominations[3]); + + // A single new 0.05 Qi UTXO should have been outputed + assert.strictEqual(result.spendOutputs.length, 1); + assert.strictEqual(result.spendOutputs[0].denomination, denominations[3]); + + // No change should be returned + assert.strictEqual(result.changeOutputs.length, 0); + }); + + it("selects multiple UTXOs whose combined value meets the target amount", function () { + const availableUTXOs = createUTXOs([denominations[1], denominations[2], denominations[2], denominations[3]]); // .075 Qi + const targetSpend = { value: denominations[2] + denominations[3], address: TEST_RECEIVE_ADDRESS }; // .06 Qi + const selector = new FewestCoinSelector(availableUTXOs); + const result = selector.performSelection(targetSpend); + + // 2 UTXOs should have been selected for a total of .06 Qi + assert.strictEqual(result.inputs.length, 2); + const inputValue = result.inputs[0].denomination! + result.inputs[1].denomination!; + assert.strictEqual(inputValue, denominations[2] + denominations[3]); + + // 2 new UTxOs should have been outputed for a total of .06 Qi + assert.strictEqual(result.spendOutputs.length, 2); + const spendValue = result.spendOutputs[0].denomination! + result.spendOutputs[1].denomination!; + assert.strictEqual(spendValue, denominations[2] + denominations[3]); + + // No change should be returned + assert.strictEqual(result.changeOutputs.length, 0); + }); + + it("selects a single UTXO that is larger than the target amount, ensuring change is correctly calculated", function () { + const availableUTXOs = createUTXOs([denominations[2], denominations[4]]); // .11 Qi + const targetSpend = { value: denominations[3], address: TEST_RECEIVE_ADDRESS }; // .05 Qi + const selector = new FewestCoinSelector(availableUTXOs); + const result = selector.performSelection(targetSpend); + + // A single 0.1 Qi UTXO should have been selected + assert.strictEqual(result.inputs.length, 1); + assert.strictEqual(result.inputs[0].denomination, denominations[4]); + + // A single new 0.05 Qi UTXO should have been outputed + assert.strictEqual(result.spendOutputs.length, 1); + assert.strictEqual(result.spendOutputs[0].denomination, denominations[3]); + + // 0.05 Qi should be returned in change + assert.strictEqual(result.changeOutputs.length, 1); + assert.strictEqual(result.changeOutputs[0].denomination, denominations[3]); + }); + + it("selects multiple UTXOs where the total exceeds the target amount, ensuring change is correctly calculated", function () { + const availableUTXOs = createUTXOs([ + denominations[2], + denominations[4], + denominations[4], + denominations[4], + denominations[5] + ]); // .56 Qi + const targetSpend = { value: denominations[6], address: TEST_RECEIVE_ADDRESS }; // .5 Qi + const selector = new FewestCoinSelector(availableUTXOs); + const result = selector.performSelection(targetSpend); + + // 4 UTXOs should have been selected for a total of .55 Qi + assert.strictEqual(result.inputs.length, 4); + const inputValue = result.inputs[0].denomination! + result.inputs[1].denomination! + result.inputs[2].denomination! + result.inputs[3].denomination!; + assert.strictEqual(inputValue, denominations[4] + denominations[4] + denominations[4] + denominations[5]); + + // A single new 0.5 Qi UTXO should have been outputed + assert.strictEqual(result.spendOutputs.length, 1); + assert.strictEqual(result.spendOutputs[0].denomination, denominations[6]); + + // 0.05 Qi should be returned in change + assert.strictEqual(result.changeOutputs.length, 1); + assert.strictEqual(result.changeOutputs[0].denomination, denominations[3]); + + }); + }); + + describe("Selecting valid UTXOs", function () { + it("throws an error when there are insufficient funds", function () { + const selector = new FewestCoinSelector(createUTXOs([denominations[0], denominations[0]])); + assert.throws(() => selector.performSelection({ value: denominations[3], address: TEST_RECEIVE_ADDRESS }), /Insufficient funds/); + }); + + it("throws an error when no UTXOs are available", function () { + const selector = new FewestCoinSelector([]); + assert.throws(() => selector.performSelection({ value: denominations[2], address: TEST_RECEIVE_ADDRESS }), /No UTXOs available/); + }); + + it("throws an error when the target amount is negative", function () { + const selector = new FewestCoinSelector(createUTXOs([denominations[2], denominations[2]])); + assert.throws(() => selector.performSelection({ value: -denominations[1], address: TEST_RECEIVE_ADDRESS }), /Target amount must be greater than 0/); + }); + }); +}); \ No newline at end of file diff --git a/src.ts/transaction/abstract-coinselector.ts b/src.ts/transaction/abstract-coinselector.ts index 26c3ff4c..a93f030e 100644 --- a/src.ts/transaction/abstract-coinselector.ts +++ b/src.ts/transaction/abstract-coinselector.ts @@ -1,8 +1,4 @@ -import { getAddress } from "../address/index.js"; -import { getBigInt } from "../utils/index.js"; -import type { BigNumberish } from "../utils/index.js"; -import { bigIntAbs } from "../utils/maths.js"; -import { UTXO, UTXOLike, denominations } from "./utxo.js"; +import { UTXO, UTXOLike } from "./utxo.js"; export type SpendTarget = { address: string; @@ -53,8 +49,12 @@ export abstract class AbstractCoinSelector { /** * Constructs a new AbstractCoinSelector instance with an empty UTXO array. */ - constructor() { - this.#availableUXTOs = []; + constructor(availableUXTOs: UTXOLike[] = []) { + this.#availableUXTOs = availableUXTOs.map((val: UTXOLike) => { + const utxo = UTXO.from(val); + this._validateUTXO(utxo); + return utxo; + }); this.#spendOutputs = []; this.#changeOutputs = []; } diff --git a/src.ts/transaction/utxo.ts b/src.ts/transaction/utxo.ts index f3cec2d4..32574956 100644 --- a/src.ts/transaction/utxo.ts +++ b/src.ts/transaction/utxo.ts @@ -1,6 +1,6 @@ -import { getAddress } from "../address/index.js"; -import { getBigInt } from "../utils/index.js"; -import type { BigNumberish } from "../utils/index.js"; +import { getAddress } from "../address/index"; +import { getBigInt } from "../utils/index"; +import type { BigNumberish } from "../utils/index"; export type OutPoint = { txhash: string; @@ -99,6 +99,8 @@ export function denominate(value: bigint): bigint[] { throw new Error("Unable to match the value with available denominations"); } + + return result; } @@ -157,7 +159,7 @@ export class UTXO implements UTXOLike { txhash: this.txhash, index: this.index, address: this.address, - denomination: this.denomination ? this.denomination.toString() : null, + denomination: this.denomination, }; } From 08309d583d2189541732e19ab6fc99eaf0fcf2e6 Mon Sep 17 00:00:00 2001 From: rileystephens28 Date: Fri, 8 Mar 2024 17:01:53 -0600 Subject: [PATCH 5/6] Update build files --- package-lock.json | 668 +++++++++++++++++++++- package.json | 5 +- src.ts/transaction/coinselector-fewest.ts | 67 ++- 3 files changed, 713 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84d20d1c..ad152ab6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,13 +22,15 @@ }, "devDependencies": { "@rollup/plugin-node-resolve": "15.0.2", - "@types/google-protobuf": "^3.15.12", - "@types/mocha": "9.1.1", + "@types/expect": "^24.3.0", + "@types/mocha": "^9.1.1", "@types/semver": "7.5.0", + "axios": "^1.6.7", "c8": "7.12.0", "mocha": "10.0.0", "rollup": "3.21.5", "semver": "7.5.4", + "ts-mocha": "^10.0.0", "typescript": "5.0.4", "uglify-js": "3.17.0" }, @@ -41,6 +43,184 @@ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -56,6 +236,47 @@ "node": ">=8" } }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -150,17 +371,27 @@ } } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, - "node_modules/@types/google-protobuf": { - "version": "3.15.12", - "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz", - "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==", - "dev": true + "node_modules/@types/expect": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/expect/-/expect-24.3.0.tgz", + "integrity": "sha512-aq5Z+YFBz5o2b6Sp1jigx5nsmoZMK5Ceurjwy6PZmRv7dEi1jLtkARfvB1ME+OXJUG+7TZUDcv3WoCr/aor6dQ==", + "deprecated": "This is a stub types definition. expect provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "expect": "*" + } }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -168,6 +399,31 @@ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "optional": true + }, "node_modules/@types/mocha": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", @@ -191,6 +447,27 @@ "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", "dev": true }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, "node_modules/@ungap/promise-all-settled": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", @@ -254,15 +531,26 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/axios": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dev": true, "dependencies": { "follow-redirects": "^1.15.4", "form-data": "^4.0.0", @@ -311,6 +599,12 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -404,6 +698,21 @@ "fsevents": "~2.3.2" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -437,6 +746,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -518,6 +828,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -531,6 +842,15 @@ "node": ">=0.3.1" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -575,6 +895,22 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -616,6 +952,7 @@ "version": "1.15.5", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "dev": true, "funding": [ { "type": "individual", @@ -648,6 +985,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -749,10 +1087,11 @@ "node": "*" } }, - "node_modules/google-protobuf": { - "version": "3.21.2", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", - "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "node_modules/has-flag": { "version": "4.0.0", @@ -953,6 +1292,88 @@ "node": ">=8" } }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -965,6 +1386,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "optional": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1023,10 +1457,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -1035,6 +1489,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -1054,6 +1509,27 @@ "node": ">=10" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mocha": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", @@ -1230,10 +1706,37 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true }, "node_modules/randombytes": { "version": "2.1.0", @@ -1244,6 +1747,12 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1384,6 +1893,55 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1410,6 +1968,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -1494,6 +2062,71 @@ "node": ">=8.0" } }, + "node_modules/ts-mocha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-10.0.0.tgz", + "integrity": "sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw==", + "dev": true, + "dependencies": { + "ts-node": "7.0.1" + }, + "bin": { + "ts-mocha": "bin/ts-mocha" + }, + "engines": { + "node": ">= 6.X.X" + }, + "optionalDependencies": { + "tsconfig-paths": "^3.5.0" + }, + "peerDependencies": { + "mocha": "^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X" + } + }, + "node_modules/ts-node": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", + "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", + "dev": true, + "dependencies": { + "arrify": "^1.0.0", + "buffer-from": "^1.1.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.6", + "yn": "^2.0.0" + }, + "bin": { + "ts-node": "dist/bin.js" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "optional": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", @@ -1659,6 +2292,15 @@ "node": ">=10" } }, + "node_modules/yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index fe669516..14944972 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,15 @@ "devDependencies": { "@rollup/plugin-node-resolve": "15.0.2", "@types/google-protobuf": "^3.15.12", - "@types/mocha": "9.1.1", + "@types/expect": "^24.3.0", + "@types/mocha": "^9.1.1", "@types/semver": "7.5.0", + "axios": "^1.6.7", "c8": "7.12.0", "mocha": "10.0.0", "rollup": "3.21.5", "semver": "7.5.4", + "ts-mocha": "^10.0.0", "typescript": "5.0.4", "uglify-js": "3.17.0" }, diff --git a/src.ts/transaction/coinselector-fewest.ts b/src.ts/transaction/coinselector-fewest.ts index 6c3b4db2..deea4c03 100644 --- a/src.ts/transaction/coinselector-fewest.ts +++ b/src.ts/transaction/coinselector-fewest.ts @@ -22,22 +22,13 @@ export class FewestCoinSelector extends AbstractCoinSelector { * @param target The target amount to select UTXOs for. */ performSelection(target: SpendTarget): SelectedCoinsResult { - if (target.value <= BigInt(0)) { - throw new Error("Target amount must be greater than 0"); - } - - if (this.availableUXTOs.length === 0) { - throw new Error("No UTXOs available"); - } + this.validateTarget(target); + this.validateUTXOs(); - // Sort UTXOs in descending order based on their denomination - const sortedUTXOs = this.availableUXTOs.sort((a, b) => { - const diff = (b.denomination ?? BigInt(0)) - (a.denomination ?? BigInt(0)); - return diff > 0 ? 1 : diff < 0 ? -1 : 0; - }); + const sortedUTXOs = this.sortUTXOsByDenomination(this.availableUXTOs, "desc"); let totalValue = BigInt(0); - const selectedUTXOs: UTXO[] = []; + let selectedUTXOs: UTXO[] = []; // Get UTXOs that meets or exceeds the target value const UTXOsEqualOrGreaterThanTarget = sortedUTXOs.filter(utxo => utxo.denomination && utxo.denomination >= target.value); @@ -83,6 +74,31 @@ export class FewestCoinSelector extends AbstractCoinSelector { 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"); + + 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++) { + const utxo = selectedUTXOs[i]; + if (utxo.denomination) { + if (runningTotal - utxo.denomination >= target.value) { + runningTotal -= utxo.denomination; + lastRemovableIndex = i; + } else { + // Once a UTXO makes the total less than target.value, stop the loop + break; + } + } + } + + if (lastRemovableIndex >= 0) { + totalValue -= selectedUTXOs[lastRemovableIndex].denomination!; + selectedUTXOs.splice(lastRemovableIndex, 1); + } + // Break down the total spend into properly denominatated UTXOs const spendDenominations = denominate(target.value); this.spendOutputs = spendDenominations.map(denomination => { @@ -115,4 +131,29 @@ export class FewestCoinSelector extends AbstractCoinSelector { }; } + private sortUTXOsByDenomination(utxos: UTXO[], direction: "asc" | "desc"): UTXO[] { + if (direction === "asc") { + return [...utxos].sort((a, b) => { + const diff = (a.denomination ?? BigInt(0)) - (b.denomination ?? BigInt(0)); + return diff > 0 ? 1 : diff < 0 ? -1 : 0; + }); + } + return [...utxos].sort((a, b) => { + const diff = (b.denomination ?? BigInt(0)) - (a.denomination ?? BigInt(0)); + return diff > 0 ? 1 : diff < 0 ? -1 : 0; + }); + } + + private validateTarget(target: SpendTarget) { + if (target.value <= BigInt(0)) { + throw new Error("Target amount must be greater than 0"); + } + } + + private validateUTXOs() { + if (this.availableUXTOs.length === 0) { + throw new Error("No UTXOs available"); + } + } + } From 8a1cdcc7aea38957aa753d1da68cd17bc212e370 Mon Sep 17 00:00:00 2001 From: DenisIvanov26 Date: Thu, 14 Mar 2024 16:13:08 -0500 Subject: [PATCH 6/6] clean install and build --- package-lock.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/package-lock.json b/package-lock.json index ad152ab6..05c774f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "devDependencies": { "@rollup/plugin-node-resolve": "15.0.2", "@types/expect": "^24.3.0", + "@types/google-protobuf": "^3.15.12", "@types/mocha": "^9.1.1", "@types/semver": "7.5.0", "axios": "^1.6.7", @@ -393,6 +394,12 @@ "expect": "*" } }, + "node_modules/@types/google-protobuf": { + "version": "3.15.12", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz", + "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==", + "dev": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1087,6 +1094,11 @@ "node": "*" } }, + "node_modules/google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",