Skip to content

Commit 0ad8a48

Browse files
fix(input-selection): greedy input selector now produces a correct selection under all conditions
1 parent 222b4b4 commit 0ad8a48

File tree

3 files changed

+33
-18
lines changed

3 files changed

+33
-18
lines changed

Diff for: packages/input-selection/src/GreedySelection/GreedyInputSelector.ts

+24-12
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const adjustOutputsForFee = async (
5050
outputs: Set<Cardano.TxOut>,
5151
changeOutputs: Array<Cardano.TxOut>,
5252
currentFee: bigint
53-
): Promise<{ fee: bigint; change: Array<Cardano.TxOut> }> => {
53+
): Promise<{ fee: bigint; change: Array<Cardano.TxOut>; feeAccountedFor: boolean }> => {
5454
const totalOutputs = new Set([...outputs, ...changeOutputs]);
5555
const fee = await constraints.computeMinimumCost({
5656
change: [],
@@ -59,7 +59,7 @@ const adjustOutputsForFee = async (
5959
outputs: totalOutputs
6060
});
6161

62-
if (fee === changeLovelace) return { change: [], fee };
62+
if (fee === changeLovelace) return { change: [], fee, feeAccountedFor: true };
6363

6464
if (changeLovelace < fee) throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient);
6565

@@ -78,11 +78,7 @@ const adjustOutputsForFee = async (
7878
}
7979
}
8080

81-
if (!feeAccountedFor) {
82-
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
83-
}
84-
85-
return { change: [...updatedOutputs], fee };
81+
return { change: [...updatedOutputs], fee, feeAccountedFor };
8682
};
8783

8884
/**
@@ -104,7 +100,7 @@ const splitChangeAndComputeFee = async (
104100
constraints: SelectionConstraints,
105101
getChangeAddresses: () => Promise<Map<Cardano.PaymentAddress, number>>,
106102
fee: bigint
107-
): Promise<{ fee: bigint; change: Array<Cardano.TxOut> }> => {
103+
): Promise<{ fee: bigint; change: Array<Cardano.TxOut>; feeAccountedFor: boolean }> => {
108104
const changeOutputs = await splitChange(
109105
getChangeAddresses,
110106
changeLovelace,
@@ -114,7 +110,7 @@ const splitChangeAndComputeFee = async (
114110
fee
115111
);
116112

117-
const adjustedChangeOutputs = await adjustOutputsForFee(
113+
let adjustedChangeOutputs = await adjustOutputsForFee(
118114
changeLovelace,
119115
constraints,
120116
inputs,
@@ -126,7 +122,7 @@ const splitChangeAndComputeFee = async (
126122
// If the newly computed fee is higher than tha available balance for change,
127123
// but there are unallocated native assets, return the assets as change with 0n coins.
128124
if (adjustedChangeOutputs.fee >= changeLovelace) {
129-
return {
125+
const result = {
130126
change: [
131127
{
132128
address: stubMaxSizeAddress,
@@ -136,12 +132,18 @@ const splitChangeAndComputeFee = async (
136132
}
137133
}
138134
],
139-
fee: adjustedChangeOutputs.fee
135+
fee: adjustedChangeOutputs.fee,
136+
feeAccountedFor: true
140137
};
138+
139+
if (result.change[0].value.coins < constraints.computeMinimumCoinQuantity(result.change[0]))
140+
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
141+
142+
return result;
141143
}
142144

143145
if (fee < adjustedChangeOutputs.fee) {
144-
return splitChangeAndComputeFee(
146+
adjustedChangeOutputs = await splitChangeAndComputeFee(
145147
inputs,
146148
outputs,
147149
changeLovelace,
@@ -150,8 +152,18 @@ const splitChangeAndComputeFee = async (
150152
getChangeAddresses,
151153
adjustedChangeOutputs.fee
152154
);
155+
156+
if (adjustedChangeOutputs.change.length === 0)
157+
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
158+
}
159+
160+
for (const out of adjustedChangeOutputs.change) {
161+
if (out.value.coins < constraints.computeMinimumCoinQuantity(out))
162+
throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
153163
}
154164

165+
if (!adjustedChangeOutputs.feeAccountedFor) throw new InputSelectionError(InputSelectionFailure.UtxoFullyDepleted);
166+
155167
return adjustedChangeOutputs;
156168
};
157169

Diff for: packages/input-selection/src/GreedySelection/util.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,12 @@ const getMinUtxoAmount = (amount: bigint): bigint => {
102102
*
103103
* @param output The output to be split.
104104
* @param computeMinimumCoinQuantity ComputeMinimumCoinQuantity.
105+
* @param fee current expected fee.
105106
*/
106107
const splitChangeOutput = (
107108
output: Cardano.TxOut,
108-
computeMinimumCoinQuantity: ComputeMinimumCoinQuantity
109+
computeMinimumCoinQuantity: ComputeMinimumCoinQuantity,
110+
fee: bigint
109111
): Array<Cardano.TxOut> => {
110112
const amount = output.value.coins;
111113
const minUtxoAdaAmount = getMinUtxoAmount(amount);
@@ -115,7 +117,7 @@ const splitChangeOutput = (
115117
const amounts = new Array<bigint>();
116118
const divisor = new BigNumber(2);
117119

118-
while (remaining > minUtxoAdaAmount) {
120+
while (remaining >= minUtxoAdaAmount) {
119121
const val = BigInt(new BigNumber(remaining.toString()).dividedBy(divisor).toFixed(0, 0));
120122

121123
const updatedRemaining = remaining - val;
@@ -125,7 +127,8 @@ const splitChangeOutput = (
125127
computeMinimumCoinQuantity({
126128
address: output.address,
127129
value: { assets: output.value.assets, coins: amount - runningAmount }
128-
})
130+
}) +
131+
fee
129132
) {
130133
amounts.push(amount - runningAmount); // Add all that remains to account for rounding errors
131134
break;
@@ -207,7 +210,7 @@ export const splitChange = async (
207210
changeOutputs[changeOutputs.length - 1].value.coins += missingAllocation;
208211
}
209212

210-
const splitOutputs = changeOutputs.flatMap((output) => splitChangeOutput(output, computeMinimumCoinQuantity));
213+
const splitOutputs = changeOutputs.flatMap((output) => splitChangeOutput(output, computeMinimumCoinQuantity, fee));
211214
const sortedOutputs = splitOutputs.sort(sortByCoins).filter((out) => out.value.coins > 0n);
212215

213216
if (sortedOutputs && sortedOutputs.length > 0) sortedOutputs[0].value.assets = totalChangeAssets; // Add all assets to the 'biggest' output.

Diff for: packages/input-selection/test/GreedySelection/util.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('splitChange', () => {
1616
undefined,
1717
() => 10n,
1818
() => false,
19-
2_000_000n
19+
0n
2020
);
2121

2222
expect(getCoinValueForAddress('A', change)).toEqual(50n);
@@ -46,7 +46,7 @@ describe('splitChange', () => {
4646
undefined,
4747
() => 10n,
4848
() => false,
49-
2_000_000n
49+
0n
5050
);
5151

5252
expect(getCoinValueForAddress('A', change)).toEqual(34n);

0 commit comments

Comments
 (0)