Skip to content

Commit 6548c9f

Browse files
feat(input-selection): add large first input selection strategy
feat(input-selection): add Large first input selection strategy
1 parent ebd539d commit 6548c9f

File tree

9 files changed

+754
-156
lines changed

9 files changed

+754
-156
lines changed

packages/input-selection/src/GreedySelection/GreedyInputSelector.ts

Lines changed: 3 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
/* eslint-disable max-params */
22
import { Cardano, coalesceValueQuantities } from '@cardano-sdk/core';
33
import { InputSelectionError, InputSelectionFailure } from '../InputSelectionError';
4-
import { InputSelectionParameters, InputSelector, SelectionConstraints, SelectionResult } from '../types';
5-
import {
6-
addTokenMaps,
7-
getCoinQuantity,
8-
hasNegativeAssetValue,
9-
sortByCoins,
10-
stubMaxSizeAddress,
11-
subtractTokenMaps,
12-
toValues
13-
} from '../util';
14-
import { sortUtxoByTxIn, splitChange } from './util';
4+
import { InputSelectionParameters, InputSelector, SelectionResult } from '../types';
5+
import { addTokenMaps, getCoinQuantity, hasNegativeAssetValue, subtractTokenMaps, toValues } from '../util';
6+
import { sortUtxoByTxIn, splitChangeAndComputeFee } from './util';
157

168
/** Greedy selection initialization properties. */
179
export interface GreedySelectorProps {
@@ -30,146 +22,6 @@ export interface GreedySelectorProps {
3022
getChangeAddresses: () => Promise<Map<Cardano.PaymentAddress, number>>;
3123
}
3224

33-
/**
34-
* Given a set of input and outputs, compute the fee. Then extract the fee from the change output
35-
* with the highest value.
36-
*
37-
* @param changeLovelace The available amount of lovelace to be used as change.
38-
* @param constraints The selection constraints.
39-
* @param inputs The inputs of the transaction.
40-
* @param outputs The outputs of the transaction.
41-
* @param changeOutputs The list of change outputs.
42-
* @param currentFee The current computed fee for this selection.
43-
*/
44-
const adjustOutputsForFee = async (
45-
changeLovelace: bigint,
46-
constraints: SelectionConstraints,
47-
inputs: Set<Cardano.Utxo>,
48-
outputs: Set<Cardano.TxOut>,
49-
changeOutputs: Array<Cardano.TxOut>,
50-
currentFee: bigint
51-
): Promise<{
52-
fee: bigint;
53-
change: Array<Cardano.TxOut>;
54-
feeAccountedFor: boolean;
55-
redeemers?: Array<Cardano.Redeemer>;
56-
}> => {
57-
const totalOutputs = new Set([...outputs, ...changeOutputs]);
58-
const { fee, redeemers } = await constraints.computeMinimumCost({
59-
change: [],
60-
fee: currentFee,
61-
inputs,
62-
outputs: totalOutputs
63-
});
64-
65-
if (fee === changeLovelace) return { change: [], fee, feeAccountedFor: true, redeemers };
66-
67-
if (changeLovelace < fee) throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient);
68-
69-
const updatedOutputs = [...changeOutputs];
70-
71-
updatedOutputs.sort(sortByCoins);
72-
73-
let feeAccountedFor = false;
74-
for (const output of updatedOutputs) {
75-
const adjustedCoins = output.value.coins - fee;
76-
77-
if (adjustedCoins >= constraints.computeMinimumCoinQuantity(output)) {
78-
output.value.coins = adjustedCoins;
79-
feeAccountedFor = true;
80-
break;
81-
}
82-
}
83-
84-
return { change: [...updatedOutputs], fee, feeAccountedFor, redeemers };
85-
};
86-
87-
/**
88-
* Recursively compute the fee and compute change outputs until it finds a set of change outputs that satisfies the fee.
89-
*
90-
* @param inputs The inputs of the transaction.
91-
* @param outputs The outputs of the transaction.
92-
* @param changeLovelace The total amount of lovelace in the change.
93-
* @param changeAssets The total assets to be distributed as change.
94-
* @param constraints The selection constraints.
95-
* @param getChangeAddresses A callback that returns a list of addresses and their proportions.
96-
* @param fee The current computed fee for this selection.
97-
*/
98-
const splitChangeAndComputeFee = async (
99-
inputs: Set<Cardano.Utxo>,
100-
outputs: Set<Cardano.TxOut>,
101-
changeLovelace: bigint,
102-
changeAssets: Cardano.TokenMap | undefined,
103-
constraints: SelectionConstraints,
104-
getChangeAddresses: () => Promise<Map<Cardano.PaymentAddress, number>>,
105-
fee: bigint
106-
): Promise<{ fee: bigint; change: Array<Cardano.TxOut>; feeAccountedFor: boolean }> => {
107-
const changeOutputs = await splitChange(
108-
getChangeAddresses,
109-
changeLovelace,
110-
changeAssets,
111-
constraints.computeMinimumCoinQuantity,
112-
constraints.tokenBundleSizeExceedsLimit,
113-
fee
114-
);
115-
116-
let adjustedChangeOutputs = await adjustOutputsForFee(
117-
changeLovelace,
118-
constraints,
119-
inputs,
120-
outputs,
121-
changeOutputs,
122-
fee
123-
);
124-
125-
// If the newly computed fee is higher than tha available balance for change,
126-
// but there are unallocated native assets, return the assets as change with 0n coins.
127-
if (adjustedChangeOutputs.fee >= changeLovelace) {
128-
const result = {
129-
change: [
130-
{
131-
address: stubMaxSizeAddress,
132-
value: {
133-
assets: changeAssets,
134-
coins: 0n
135-
}
136-
}
137-
],
138-
fee: adjustedChangeOutputs.fee,
139-
feeAccountedFor: true
140-
};
141-
142-
if (result.change[0].value.coins < constraints.computeMinimumCoinQuantity(result.change[0]))
143-
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
144-
145-
return result;
146-
}
147-
148-
if (fee < adjustedChangeOutputs.fee) {
149-
adjustedChangeOutputs = await splitChangeAndComputeFee(
150-
inputs,
151-
outputs,
152-
changeLovelace,
153-
changeAssets,
154-
constraints,
155-
getChangeAddresses,
156-
adjustedChangeOutputs.fee
157-
);
158-
159-
if (adjustedChangeOutputs.change.length === 0)
160-
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
161-
}
162-
163-
for (const out of adjustedChangeOutputs.change) {
164-
if (out.value.coins < constraints.computeMinimumCoinQuantity(out))
165-
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
166-
}
167-
168-
if (!adjustedChangeOutputs.feeAccountedFor) throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
169-
170-
return adjustedChangeOutputs;
171-
};
172-
17325
/** Selects all UTXOs to fulfill the amount required for the given outputs and return the remaining balance as change. */
17426
export class GreedyInputSelector implements InputSelector {
17527
#props: GreedySelectorProps;

packages/input-selection/src/GreedySelection/util.ts

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* eslint-disable func-style, max-params */
22
import { BigNumber } from 'bignumber.js';
33
import { Cardano, coalesceValueQuantities } from '@cardano-sdk/core';
4-
import { ComputeMinimumCoinQuantity, TokenBundleSizeExceedsLimit } from '../types';
4+
import { ComputeMinimumCoinQuantity, SelectionConstraints, TokenBundleSizeExceedsLimit } from '../types';
55
import { InputSelectionError, InputSelectionFailure } from '../InputSelectionError';
6-
import { addTokenMaps, isValidValue, sortByCoins } from '../util';
6+
import { addTokenMaps, isValidValue, sortByCoins, stubMaxSizeAddress } from '../util';
77

88
const PERCENTAGE_TOLERANCE = 0.05;
99

@@ -246,3 +246,143 @@ export const sortTxIn = (lhs: Cardano.TxIn, rhs: Cardano.TxIn) => {
246246
* @param rhs The left-hand side of the comparison operation.
247247
*/
248248
export const sortUtxoByTxIn = (lhs: Cardano.Utxo, rhs: Cardano.Utxo) => sortTxIn(lhs[0], rhs[0]);
249+
250+
/**
251+
* Given a set of input and outputs, compute the fee. Then extract the fee from the change output
252+
* with the highest value.
253+
*
254+
* @param changeLovelace The available amount of lovelace to be used as change.
255+
* @param constraints The selection constraints.
256+
* @param inputs The inputs of the transaction.
257+
* @param outputs The outputs of the transaction.
258+
* @param changeOutputs The list of change outputs.
259+
* @param currentFee The current computed fee for this selection.
260+
*/
261+
export const adjustOutputsForFee = async (
262+
changeLovelace: bigint,
263+
constraints: SelectionConstraints,
264+
inputs: Set<Cardano.Utxo>,
265+
outputs: Set<Cardano.TxOut>,
266+
changeOutputs: Array<Cardano.TxOut>,
267+
currentFee: bigint
268+
): Promise<{
269+
fee: bigint;
270+
change: Array<Cardano.TxOut>;
271+
feeAccountedFor: boolean;
272+
redeemers?: Array<Cardano.Redeemer>;
273+
}> => {
274+
const totalOutputs = new Set([...outputs, ...changeOutputs]);
275+
const { fee, redeemers } = await constraints.computeMinimumCost({
276+
change: [],
277+
fee: currentFee,
278+
inputs,
279+
outputs: totalOutputs
280+
});
281+
282+
if (fee === changeLovelace) return { change: [], fee, feeAccountedFor: true, redeemers };
283+
284+
if (changeLovelace < fee) throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient);
285+
286+
const updatedOutputs = [...changeOutputs];
287+
288+
updatedOutputs.sort(sortByCoins);
289+
290+
let feeAccountedFor = false;
291+
for (const output of updatedOutputs) {
292+
const adjustedCoins = output.value.coins - fee;
293+
294+
if (adjustedCoins >= constraints.computeMinimumCoinQuantity(output)) {
295+
output.value.coins = adjustedCoins;
296+
feeAccountedFor = true;
297+
break;
298+
}
299+
}
300+
301+
return { change: [...updatedOutputs], fee, feeAccountedFor, redeemers };
302+
};
303+
304+
/**
305+
* Recursively compute the fee and compute change outputs until it finds a set of change outputs that satisfies the fee.
306+
*
307+
* @param inputs The inputs of the transaction.
308+
* @param outputs The outputs of the transaction.
309+
* @param changeLovelace The total amount of lovelace in the change.
310+
* @param changeAssets The total assets to be distributed as change.
311+
* @param constraints The selection constraints.
312+
* @param getChangeAddresses A callback that returns a list of addresses and their proportions.
313+
* @param fee The current computed fee for this selection.
314+
*/
315+
export const splitChangeAndComputeFee = async (
316+
inputs: Set<Cardano.Utxo>,
317+
outputs: Set<Cardano.TxOut>,
318+
changeLovelace: bigint,
319+
changeAssets: Cardano.TokenMap | undefined,
320+
constraints: SelectionConstraints,
321+
getChangeAddresses: () => Promise<Map<Cardano.PaymentAddress, number>>,
322+
fee: bigint
323+
): Promise<{ fee: bigint; change: Array<Cardano.TxOut>; feeAccountedFor: boolean }> => {
324+
const changeOutputs = await splitChange(
325+
getChangeAddresses,
326+
changeLovelace,
327+
changeAssets,
328+
constraints.computeMinimumCoinQuantity,
329+
constraints.tokenBundleSizeExceedsLimit,
330+
fee
331+
);
332+
333+
let adjustedChangeOutputs = await adjustOutputsForFee(
334+
changeLovelace,
335+
constraints,
336+
inputs,
337+
outputs,
338+
changeOutputs,
339+
fee
340+
);
341+
342+
// If the newly computed fee is higher than tha available balance for change,
343+
// but there are unallocated native assets, return the assets as change with 0n coins.
344+
if (adjustedChangeOutputs.fee >= changeLovelace) {
345+
const result = {
346+
change: [
347+
{
348+
address: stubMaxSizeAddress,
349+
value: {
350+
assets: changeAssets,
351+
coins: 0n
352+
}
353+
}
354+
],
355+
fee: adjustedChangeOutputs.fee,
356+
feeAccountedFor: true
357+
};
358+
359+
if (result.change[0].value.coins < constraints.computeMinimumCoinQuantity(result.change[0]))
360+
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
361+
362+
return result;
363+
}
364+
365+
if (fee < adjustedChangeOutputs.fee) {
366+
adjustedChangeOutputs = await splitChangeAndComputeFee(
367+
inputs,
368+
outputs,
369+
changeLovelace,
370+
changeAssets,
371+
constraints,
372+
getChangeAddresses,
373+
adjustedChangeOutputs.fee
374+
);
375+
376+
if (adjustedChangeOutputs.change.length === 0)
377+
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
378+
}
379+
380+
for (const out of adjustedChangeOutputs.change) {
381+
if (out.value.coins < constraints.computeMinimumCoinQuantity(out))
382+
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
383+
}
384+
385+
if (!adjustedChangeOutputs.feeAccountedFor) throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
386+
387+
return adjustedChangeOutputs;
388+
};

0 commit comments

Comments
 (0)