Skip to content

Commit

Permalink
Create new conversion tx coin selector class and add few minor optimi…
Browse files Browse the repository at this point in the history
…zations to syncing logic
  • Loading branch information
rileystephens28 committed Nov 27, 2024
1 parent 84f23d6 commit 2f4e6f2
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 56 deletions.
31 changes: 31 additions & 0 deletions src/transaction/coinselector-conversion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FewestCoinSelector } from './coinselector-fewest.js';
import { UTXO, denominate, denominations } from './utxo.js';

/**
* The ConversionSelector 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 modified version of {@link FewestCoinSelector | **FewestCoinSelector** } and implements the
* {@link FewestCoinSelector.createSpendOutputs | **createSpendOutputs** } method to provide the actual coin selection
* logic.
*
* @category Transaction
*/
export class ConversionCoinSelector extends FewestCoinSelector {
/**
* Creates spend outputs based on the target amount and input denominations.
*
* @param {bigint} amount - The target amount to spend.
* @returns {UTXO[]} The spend outputs.
*/
protected override createSpendOutputs(amount: bigint): UTXO[] {
// Spend outpoints are not limited to max input denomination
const spendDenominations = denominate(amount);

return spendDenominations.map((denominationValue) => {
const utxo = new UTXO();
utxo.denomination = denominations.indexOf(denominationValue);
return utxo;
});
}
}
16 changes: 8 additions & 8 deletions src/transaction/coinselector-fewest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class FewestCoinSelector extends AbstractCoinSelector {
* @param {bigint} totalRequired - The total amount required (target + fee).
* @returns {UTXO[]} The minimal set of UTXOs.
*/
private findMinimalUTXOSet(sortedUTXOs: UTXO[], totalRequired: bigint): UTXO[] {
protected findMinimalUTXOSet(sortedUTXOs: UTXO[], totalRequired: bigint): UTXO[] {
// First, try to find the smallest single UTXO that covers the total required amount
const singleUTXO = sortedUTXOs.find((utxo) => BigInt(denominations[utxo.denomination!]) >= totalRequired);
if (singleUTXO) {
Expand Down Expand Up @@ -136,7 +136,7 @@ export class FewestCoinSelector extends AbstractCoinSelector {
* @param {UTXO[]} inputs - The selected inputs.
* @returns {UTXO[]} The spend outputs.
*/
private createSpendOutputs(amount: bigint): UTXO[] {
protected createSpendOutputs(amount: bigint): UTXO[] {
const maxInputDenomination = this.getMaxInputDenomination();

// Denominate the amount using available denominations up to the max input denomination
Expand All @@ -156,7 +156,7 @@ export class FewestCoinSelector extends AbstractCoinSelector {
* @param {UTXO[]} inputs - The selected inputs.
* @returns {UTXO[]} The change outputs.
*/
private createChangeOutputs(change: bigint): UTXO[] {
protected createChangeOutputs(change: bigint): UTXO[] {
if (change <= BigInt(0)) {
return [];
}
Expand All @@ -178,7 +178,7 @@ export class FewestCoinSelector extends AbstractCoinSelector {
*
* @returns {bigint} The total output value.
*/
private calculateTotalOutputValue(): bigint {
protected calculateTotalOutputValue(): bigint {
const spendValue = this.spendOutputs.reduce(
(sum, output) => sum + BigInt(denominations[output.denomination!]),
BigInt(0),
Expand All @@ -197,7 +197,7 @@ export class FewestCoinSelector extends AbstractCoinSelector {
*
* @returns {bigint} The maximum input denomination value.
*/
private getMaxInputDenomination(): bigint {
protected getMaxInputDenomination(): bigint {
const inputs = [...this.selectedUTXOs];
return this.getMaxDenomination(inputs);
}
Expand All @@ -207,7 +207,7 @@ export class FewestCoinSelector extends AbstractCoinSelector {
*
* @returns {bigint} The maximum output denomination value.
*/
private getMaxOutputDenomination(): bigint {
protected getMaxOutputDenomination(): bigint {
const outputs = [...this.spendOutputs, ...this.changeOutputs];
return this.getMaxDenomination(outputs);
}
Expand All @@ -218,7 +218,7 @@ export class FewestCoinSelector extends AbstractCoinSelector {
* @param {UTXO[]} utxos - The list of UTXOs.
* @returns {bigint} The maximum denomination value.
*/
private getMaxDenomination(utxos: UTXO[]): bigint {
protected getMaxDenomination(utxos: UTXO[]): bigint {
return utxos.reduce((max, utxo) => {
const denomValue = BigInt(denominations[utxo.denomination!]);
return denomValue > max ? denomValue : max;
Expand Down Expand Up @@ -323,7 +323,7 @@ export class FewestCoinSelector extends AbstractCoinSelector {
*
* @param {bigint} changeAmount - The amount to adjust change outputs by.
*/
private adjustChangeOutputs(changeAmount: bigint): void {
protected adjustChangeOutputs(changeAmount: bigint): void {
if (changeAmount <= BigInt(0)) {
this.changeOutputs = [];
return;
Expand Down
91 changes: 43 additions & 48 deletions src/wallet/qi-hdwallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { type BIP32API, HDNodeBIP32Adapter } from './bip32/types.js';
import ecc from '@bitcoinerlab/secp256k1';
import { SelectedCoinsResult } from '../transaction/abstract-coinselector.js';
import { QiPerformActionTransaction } from '../providers/abstract-provider.js';
import { ConversionCoinSelector } from '../transaction/coinselector-conversion.js';

/**
* @property {Outpoint} outpoint - The outpoint object.
Expand Down Expand Up @@ -576,22 +577,10 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
* Converts outpoints for a specific zone to UTXO format.
*
* @param {Zone} zone - The zone to filter outpoints for.
* @param {number} [minDenominationToUse] - The minimum denomination to allow for the UTXOs.
* @returns {UTXO[]} An array of UTXO objects.
*/
private outpointsToUTXOs(zone: Zone, minDenominationToUse?: number): UTXO[] {
this.validateZone(zone);
let zoneOutpoints = this.getOutpoints(zone);

// Filter outpoints by minimum denomination if specified
// This will likely only be used for converting to Quai
// as the min denomination for converting is 10 (100 Qi)
if (minDenominationToUse !== undefined) {
zoneOutpoints = zoneOutpoints.filter(
(outpointInfo) => outpointInfo.outpoint.denomination >= minDenominationToUse,
);
}
return zoneOutpoints.map((outpointInfo) => {
private outpointsToUTXOs(zone: Zone): UTXO[] {
return this.getOutpoints(zone).map((outpointInfo) => {
const utxo = new UTXO();
utxo.txhash = outpointInfo.outpoint.txhash;
utxo.index = outpointInfo.outpoint.index;
Expand Down Expand Up @@ -628,7 +617,12 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
return Array(count).fill(destinationAddress);
};

return this.prepareAndSendTransaction(amount, zone, getDestinationAddresses, 10);
return this.prepareAndSendTransaction(
amount,
zone,
getDestinationAddresses,
(utxos) => new ConversionCoinSelector(utxos),
);
}

/**
Expand Down Expand Up @@ -668,7 +662,12 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
return addresses;
};

return this.prepareAndSendTransaction(amount, originZone, getDestinationAddresses);
return this.prepareAndSendTransaction(
amount,
originZone,
getDestinationAddresses,
(utxos) => new FewestCoinSelector(utxos),
);
}

/**
Expand Down Expand Up @@ -730,7 +729,6 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
* @param {Zone} originZone - The zone where the transaction originates.
* @param {Function} getDestinationAddresses - A function that returns a promise resolving to an array of
* destination addresses.
* @param {number} [minDenominationToUse] - Optional minimum denomination of Qi to use for the transaction.
* @returns {Promise<TransactionResponse>} A promise that resolves to the transaction response.
* @throws {Error} If provider is not set, insufficient balance, no available UTXOs, or insufficient spendable
* balance.
Expand All @@ -739,7 +737,7 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
amount: bigint,
originZone: Zone,
getDestinationAddresses: (count: number) => Promise<string[]>,
minDenominationToUse?: number,
coinSelectorCreator: (utxos: UTXO[]) => FewestCoinSelector | ConversionCoinSelector,
): Promise<TransactionResponse> {
if (!this.provider) {
throw new Error('Provider is not set');
Expand All @@ -755,26 +753,21 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
}

// 2. Select the UXTOs from the specified zone to use as inputs, and generate the spend and change outputs
const zoneUTXOs = this.outpointsToUTXOs(originZone, minDenominationToUse);
const zoneUTXOs = this.outpointsToUTXOs(originZone);
if (zoneUTXOs.length === 0) {
if (minDenominationToUse === 10) {
throw new Error('Qi denominations too small to convert.');
} else {
throw new Error('No Qi available in zone.');
}
throw new Error('No Qi available in zone.');
}

const unlockedUTXOs = zoneUTXOs.filter(
(utxo) => utxo.lock === 0 || utxo.lock! < currentBlock?.woHeader.number!,
);
if (unlockedUTXOs.length === 0) {
throw new Error('Insufficient spendable balance in zone.');
}

const fewestCoinSelector = new FewestCoinSelector(unlockedUTXOs);
const coinSelector = coinSelectorCreator(unlockedUTXOs);

const spendTarget: bigint = amount;
let selection = fewestCoinSelector.performSelection({ target: spendTarget });
let selection = coinSelector.performSelection({ target: spendTarget });

// 3. Generate as many unused addresses as required to populate the spend outputs
const sendAddresses = await getDestinationAddresses(selection.spendOutputs.length);
Expand Down Expand Up @@ -844,8 +837,7 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
const estimatedFee = await this.provider.estimateFeeForQi(feeEstimationTx);

// Get new selection with updated fee 2x
selection = fewestCoinSelector.performSelection({ target: spendTarget, fee: estimatedFee * 3n });

selection = coinSelector.performSelection({ target: spendTarget, fee: estimatedFee * 3n });
// Determine if new addresses are needed for the change outputs
const changeAddressesNeeded = selection.changeOutputs.length - changeAddresses.length;
if (changeAddressesNeeded > 0) {
Expand Down Expand Up @@ -904,7 +896,6 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {

// Sign the transaction
const signedTx = await this.signTransaction(tx);

// Broadcast the transaction to the network using the provider
return this.provider.broadcastTransaction(originZone, signedTx);
}
Expand Down Expand Up @@ -1223,29 +1214,30 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {

const derivationPaths: DerivationPath[] = ['BIP44:external', 'BIP44:change', ...this.openChannels];
const currentBlock = (await this.provider!.getBlock(toShard(zone), 'latest')) as Block;

await Promise.all([
...derivationPaths.map((path) =>
this._scanDerivationPath(
path,
zone,
account,
currentBlock,
false,
onOutpointsCreated,
onOutpointsDeleted,
),
),
this._scanDerivationPath(
QiHDWallet.PRIVATE_KEYS_PATH,
for (const path of derivationPaths) {
await this._scanDerivationPath(
path,
zone,
account,
currentBlock,
true,
false,
onOutpointsCreated,
onOutpointsDeleted,
),
]);
);

// Yield control back to the event loop
await new Promise((resolve) => setTimeout(resolve, 0));
}

await this._scanDerivationPath(
QiHDWallet.PRIVATE_KEYS_PATH,
zone,
account,
currentBlock,
true,
onOutpointsCreated,
onOutpointsDeleted,
);
}

/**
Expand Down Expand Up @@ -1459,6 +1451,9 @@ export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
break;
}
}

// Yield control back to the event loop after each iteration
await new Promise((resolve) => setTimeout(resolve, 0));
}
}

Expand Down

0 comments on commit 2f4e6f2

Please sign in to comment.