Skip to content

Commit 07ac410

Browse files
Merge pull request #1621 from input-output-hk/feat/lw-12753-large-first-input-selection
feat(input-selection): add large first input selection strategy
2 parents ebd539d + 05e6c45 commit 07ac410

File tree

12 files changed

+1008
-188
lines changed

12 files changed

+1008
-188
lines changed

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

Lines changed: 3 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
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';
4+
import { InputSelectionParameters, InputSelector, SelectionResult } from '../types';
55
import {
66
addTokenMaps,
77
getCoinQuantity,
88
hasNegativeAssetValue,
9-
sortByCoins,
10-
stubMaxSizeAddress,
9+
sortUtxoByTxIn,
1110
subtractTokenMaps,
1211
toValues
1312
} from '../util';
14-
import { sortUtxoByTxIn, splitChange } from './util';
13+
import { splitChangeAndComputeFee } from './util';
1514

1615
/** Greedy selection initialization properties. */
1716
export interface GreedySelectorProps {
@@ -30,146 +29,6 @@ export interface GreedySelectorProps {
3029
getChangeAddresses: () => Promise<Map<Cardano.PaymentAddress, number>>;
3130
}
3231

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-
17332
/** Selects all UTXOs to fulfill the amount required for the given outputs and return the remaining balance as change. */
17433
export class GreedyInputSelector implements InputSelector {
17534
#props: GreedySelectorProps;

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

Lines changed: 132 additions & 13 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

@@ -227,22 +227,141 @@ export const splitChange = async (
227227
};
228228

229229
/**
230-
* Sorts the given TxIn set first by txId and then by index.
230+
* Given a set of input and outputs, compute the fee. Then extract the fee from the change output
231+
* with the highest value.
231232
*
232-
* @param lhs The left-hand side of the comparison operation.
233-
* @param rhs The left-hand side of the comparison operation.
233+
* @param changeLovelace The available amount of lovelace to be used as change.
234+
* @param constraints The selection constraints.
235+
* @param inputs The inputs of the transaction.
236+
* @param outputs The outputs of the transaction.
237+
* @param changeOutputs The list of change outputs.
238+
* @param currentFee The current computed fee for this selection.
234239
*/
235-
export const sortTxIn = (lhs: Cardano.TxIn, rhs: Cardano.TxIn) => {
236-
const txIdComparison = lhs.txId.localeCompare(rhs.txId);
237-
if (txIdComparison !== 0) return txIdComparison;
240+
export const adjustOutputsForFee = async (
241+
changeLovelace: bigint,
242+
constraints: SelectionConstraints,
243+
inputs: Set<Cardano.Utxo>,
244+
outputs: Set<Cardano.TxOut>,
245+
changeOutputs: Array<Cardano.TxOut>,
246+
currentFee: bigint
247+
): Promise<{
248+
fee: bigint;
249+
change: Array<Cardano.TxOut>;
250+
feeAccountedFor: boolean;
251+
redeemers?: Array<Cardano.Redeemer>;
252+
}> => {
253+
const totalOutputs = new Set([...outputs, ...changeOutputs]);
254+
const { fee, redeemers } = await constraints.computeMinimumCost({
255+
change: [],
256+
fee: currentFee,
257+
inputs,
258+
outputs: totalOutputs
259+
});
260+
261+
if (fee === changeLovelace) return { change: [], fee, feeAccountedFor: true, redeemers };
262+
263+
if (changeLovelace < fee) throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient);
264+
265+
const updatedOutputs = [...changeOutputs];
266+
267+
updatedOutputs.sort(sortByCoins);
268+
269+
let feeAccountedFor = false;
270+
for (const output of updatedOutputs) {
271+
const adjustedCoins = output.value.coins - fee;
272+
273+
if (adjustedCoins >= constraints.computeMinimumCoinQuantity(output)) {
274+
output.value.coins = adjustedCoins;
275+
feeAccountedFor = true;
276+
break;
277+
}
278+
}
238279

239-
return lhs.index - rhs.index;
280+
return { change: [...updatedOutputs], fee, feeAccountedFor, redeemers };
240281
};
241282

242283
/**
243-
* Sorts the given Utxo set first by TxIn.
284+
* Recursively compute the fee and compute change outputs until it finds a set of change outputs that satisfies the fee.
244285
*
245-
* @param lhs The left-hand side of the comparison operation.
246-
* @param rhs The left-hand side of the comparison operation.
286+
* @param inputs The inputs of the transaction.
287+
* @param outputs The outputs of the transaction.
288+
* @param changeLovelace The total amount of lovelace in the change.
289+
* @param changeAssets The total assets to be distributed as change.
290+
* @param constraints The selection constraints.
291+
* @param getChangeAddresses A callback that returns a list of addresses and their proportions.
292+
* @param fee The current computed fee for this selection.
247293
*/
248-
export const sortUtxoByTxIn = (lhs: Cardano.Utxo, rhs: Cardano.Utxo) => sortTxIn(lhs[0], rhs[0]);
294+
export const splitChangeAndComputeFee = async (
295+
inputs: Set<Cardano.Utxo>,
296+
outputs: Set<Cardano.TxOut>,
297+
changeLovelace: bigint,
298+
changeAssets: Cardano.TokenMap | undefined,
299+
constraints: SelectionConstraints,
300+
getChangeAddresses: () => Promise<Map<Cardano.PaymentAddress, number>>,
301+
fee: bigint
302+
): Promise<{ fee: bigint; change: Array<Cardano.TxOut>; feeAccountedFor: boolean }> => {
303+
const changeOutputs = await splitChange(
304+
getChangeAddresses,
305+
changeLovelace,
306+
changeAssets,
307+
constraints.computeMinimumCoinQuantity,
308+
constraints.tokenBundleSizeExceedsLimit,
309+
fee
310+
);
311+
312+
let adjustedChangeOutputs = await adjustOutputsForFee(
313+
changeLovelace,
314+
constraints,
315+
inputs,
316+
outputs,
317+
changeOutputs,
318+
fee
319+
);
320+
321+
// If the newly computed fee is higher than the available balance for change,
322+
// but there are unallocated native assets, return the assets as change with 0n coins.
323+
if (adjustedChangeOutputs.fee >= changeLovelace) {
324+
const result = {
325+
change: [
326+
{
327+
address: stubMaxSizeAddress,
328+
value: {
329+
assets: changeAssets,
330+
coins: 0n
331+
}
332+
}
333+
],
334+
fee: adjustedChangeOutputs.fee,
335+
feeAccountedFor: true
336+
};
337+
338+
if (result.change[0].value.coins < constraints.computeMinimumCoinQuantity(result.change[0]))
339+
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
340+
341+
return result;
342+
}
343+
344+
if (fee < adjustedChangeOutputs.fee) {
345+
adjustedChangeOutputs = await splitChangeAndComputeFee(
346+
inputs,
347+
outputs,
348+
changeLovelace,
349+
changeAssets,
350+
constraints,
351+
getChangeAddresses,
352+
adjustedChangeOutputs.fee
353+
);
354+
355+
if (adjustedChangeOutputs.change.length === 0)
356+
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
357+
}
358+
359+
for (const out of adjustedChangeOutputs.change) {
360+
if (out.value.coins < constraints.computeMinimumCoinQuantity(out))
361+
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
362+
}
363+
364+
if (!adjustedChangeOutputs.feeAccountedFor) throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
365+
366+
return adjustedChangeOutputs;
367+
};

0 commit comments

Comments
 (0)