Skip to content

Commit af21c08

Browse files
AngelCastilloBmirceahasegan
authored andcommitted
fix(input-selection): roundRobinSelection now ensures all change bundles meet minRequiredAda
1 parent 1318d6d commit af21c08

File tree

2 files changed

+136
-9
lines changed

2 files changed

+136
-9
lines changed

Diff for: packages/input-selection/src/RoundRobinRandomImprove/change.ts

+31-9
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,22 @@ const pickExtraRandomUtxo = (
207207
return { utxoRemaining: newUtxoRemaining, utxoSelected: newUtxoSelected };
208208
};
209209

210-
const coalesceChangeBundlesForMinCoinRequirement = (
210+
const mergeWithSmallestBundle = (values: Cardano.Value[], index: number): Cardano.Value[] => {
211+
let result = [...values];
212+
const toBeMerged = result.splice(index, 1)[0];
213+
214+
if (result.length === 0) return [toBeMerged];
215+
216+
const last = result.splice(-1, 1)[0];
217+
const merged = coalesceValueQuantities([toBeMerged, last]);
218+
219+
result = [...result, merged];
220+
result = orderBy(result, ({ coins }) => coins, 'desc');
221+
222+
return result;
223+
};
224+
225+
export const coalesceChangeBundlesForMinCoinRequirement = (
211226
changeBundles: Cardano.Value[],
212227
computeMinimumCoinQuantity: ComputeMinimumCoinQuantity
213228
): Cardano.Value[] | undefined => {
@@ -230,15 +245,22 @@ const coalesceChangeBundlesForMinCoinRequirement = (
230245
return value.coins >= computeMinimumCoinQuantity(stubTxOut);
231246
};
232247

233-
while (sortedBundles.length > 1 && !satisfiesMinCoinRequirement(sortedBundles[sortedBundles.length - 1])) {
234-
const smallestBundle = sortedBundles.pop()!;
235-
sortedBundles[sortedBundles.length - 1] = coalesceValueQuantities([
236-
sortedBundles[sortedBundles.length - 1],
237-
smallestBundle
238-
]);
239-
// Re-sort because last bundle is not necessarily the smallest one after merging it
240-
sortedBundles = orderBy(sortedBundles, ({ coins }) => coins, 'desc');
248+
let allBundlesSatisfyMinCoin = false;
249+
250+
while (sortedBundles.length > 1 && !allBundlesSatisfyMinCoin) {
251+
allBundlesSatisfyMinCoin = true;
252+
for (let i = sortedBundles.length - 1; i >= 0; --i) {
253+
const satisfies = satisfiesMinCoinRequirement(sortedBundles[i]);
254+
255+
allBundlesSatisfyMinCoin = allBundlesSatisfyMinCoin && satisfies;
256+
257+
if (!satisfies) {
258+
sortedBundles = mergeWithSmallestBundle(sortedBundles, i);
259+
break;
260+
}
261+
}
241262
}
263+
242264
if (!satisfiesMinCoinRequirement(sortedBundles[0])) {
243265
// Coalesced all bundles to 1 and it's still less than min utxo value
244266
return undefined;

Diff for: packages/input-selection/test/change.test.ts

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { Cardano } from '@cardano-sdk/core';
2+
import { coalesceChangeBundlesForMinCoinRequirement } from '../src/RoundRobinRandomImprove/change';
3+
4+
const TOKEN1_ASSET_ID = Cardano.AssetId('5c677ba4dd295d9286e0e22786fea9ed735a6ae9c07e7a45ae4d95c84249530000');
5+
const TOKEN2_ASSET_ID = Cardano.AssetId('5c677ba4dd295d9286e0e22786fea9ed735a6ae9c07e7a45ae4d95c84249530001');
6+
const TOKEN3_ASSET_ID = Cardano.AssetId('5c677ba4dd295d9286e0e22786fea9ed735a6ae9c07e7a45ae4d95c84249530002');
7+
8+
const MIN_ADA_COIN_VAL = 5_000_000n;
9+
const computeMinimumCoinQuantity = (utxo: Cardano.TxOut): bigint =>
10+
MIN_ADA_COIN_VAL + (utxo.value.assets ? BigInt(utxo.value.assets.size) : 0n) * 5_000_000n;
11+
12+
describe('coalesceChangeBundlesForMinCoinRequirement', () => {
13+
it('given empty change bundle list, returns the empty list', async () => {
14+
const changeBundles: Cardano.Value[] = [];
15+
const result = coalesceChangeBundlesForMinCoinRequirement(changeBundles, computeMinimumCoinQuantity);
16+
17+
expect(result).toBeDefined();
18+
expect(result?.length).toBe(0);
19+
});
20+
21+
it('when given 3 bundles with valid min ADA coin, return the three original bundles', async () => {
22+
const changeBundles: Cardano.Value[] = [{ coins: 5_000_000n }, { coins: 5_000_000n }, { coins: 5_000_000n }];
23+
const result = coalesceChangeBundlesForMinCoinRequirement(changeBundles, computeMinimumCoinQuantity);
24+
25+
expect(result).toBeDefined();
26+
expect(result?.length).toBe(3);
27+
28+
expect(result![0].coins).toBe(MIN_ADA_COIN_VAL);
29+
expect(result![1].coins).toBe(MIN_ADA_COIN_VAL);
30+
expect(result![2].coins).toBe(MIN_ADA_COIN_VAL);
31+
});
32+
33+
it('when the last bundle has less than min ADA coin, coalesce it with the second last', async () => {
34+
const changeBundles: Cardano.Value[] = [{ coins: 5_000_000n }, { coins: 5_000_000n }, { coins: 4_000_000n }];
35+
const result = coalesceChangeBundlesForMinCoinRequirement(changeBundles, computeMinimumCoinQuantity);
36+
37+
expect(result).toBeDefined();
38+
expect(result?.length).toBe(2);
39+
40+
expect(result![0].coins).toBe(9_000_000n);
41+
expect(result![1].coins).toBe(5_000_000n);
42+
});
43+
44+
it('when the middle bundle has less than min ADA coin, coalesce it with the last', async () => {
45+
const changeBundles: Cardano.Value[] = [
46+
{ coins: 10_000_000n },
47+
{ assets: new Map([[TOKEN1_ASSET_ID, 2333n]]), coins: 7_000_000n },
48+
{ coins: 5_000_000n }
49+
];
50+
51+
const result = coalesceChangeBundlesForMinCoinRequirement(changeBundles, computeMinimumCoinQuantity);
52+
53+
expect(result).toBeDefined();
54+
expect(result?.length).toBe(2);
55+
56+
expect(result![0].coins).toBe(12_000_000n);
57+
expect(result![0].assets!.get(TOKEN1_ASSET_ID)).toBe(2333n);
58+
expect(result![1].coins).toBe(10_000_000n);
59+
});
60+
61+
it('when the first bundle has less than min ADA coin, coalesce it with the last', async () => {
62+
const changeBundles: Cardano.Value[] = [
63+
{ assets: new Map([[TOKEN1_ASSET_ID, 2333n]]), coins: 7_000_000n },
64+
{ coins: 5_000_000n },
65+
{ coins: 5_000_000n }
66+
];
67+
68+
const result = coalesceChangeBundlesForMinCoinRequirement(changeBundles, computeMinimumCoinQuantity);
69+
70+
expect(result).toBeDefined();
71+
expect(result?.length).toBe(2);
72+
73+
expect(result![0].coins).toBe(12_000_000n);
74+
expect(result![0].assets!.get(TOKEN1_ASSET_ID)).toBe(2333n);
75+
expect(result![1].coins).toBe(5_000_000n);
76+
});
77+
78+
it('when the three bundle have less than min ADA coin, coalesce them together', async () => {
79+
const changeBundles: Cardano.Value[] = [
80+
{ assets: new Map([[TOKEN1_ASSET_ID, 2333n]]), coins: 7_000_000n },
81+
{ assets: new Map([[TOKEN1_ASSET_ID, 2333n]]), coins: 7_000_000n },
82+
{ assets: new Map([[TOKEN1_ASSET_ID, 2333n]]), coins: 7_000_000n }
83+
];
84+
85+
const result = coalesceChangeBundlesForMinCoinRequirement(changeBundles, computeMinimumCoinQuantity);
86+
87+
expect(result).toBeDefined();
88+
expect(result?.length).toBe(1);
89+
90+
expect(result![0].coins).toBe(21_000_000n);
91+
expect(result![0].assets!.get(TOKEN1_ASSET_ID)).toBe(6999n);
92+
});
93+
94+
it('when coalescing the three bundles do not reach the min ADA coin return undefined', async () => {
95+
const changeBundles: Cardano.Value[] = [
96+
{ assets: new Map([[TOKEN1_ASSET_ID, 2333n]]), coins: 2_000_000n },
97+
{ assets: new Map([[TOKEN2_ASSET_ID, 2333n]]), coins: 2_000_000n },
98+
{ assets: new Map([[TOKEN3_ASSET_ID, 2333n]]), coins: 2_000_000n }
99+
];
100+
101+
const result = coalesceChangeBundlesForMinCoinRequirement(changeBundles, computeMinimumCoinQuantity);
102+
103+
expect(result).toBeUndefined();
104+
});
105+
});

0 commit comments

Comments
 (0)