Skip to content

Commit 47f6bd2

Browse files
committed
feat(cip2): add support for implicit coin
1 parent 5f63db0 commit 47f6bd2

File tree

6 files changed

+192
-80
lines changed

6 files changed

+192
-80
lines changed

Diff for: packages/cip2/src/RoundRobinRandomImprove/change.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
assetWithValueQuantitySelector,
88
getCoinQuantity,
99
getWithValuesCoinQuantity,
10+
ImplicitCoinBigint,
1011
UtxoSelection,
1112
UtxoWithValue
1213
} from './util';
@@ -18,6 +19,7 @@ interface ChangeComputationArgs {
1819
utxoSelection: UtxoSelection;
1920
outputValues: Ogmios.util.OgmiosValue[];
2021
uniqueOutputAssetIDs: string[];
22+
implicitCoin: ImplicitCoinBigint;
2123
estimateTxFee: EstimateTxFeeWithOriginalOutputs;
2224
computeMinimumCoinQuantity: ComputeMinimumCoinQuantity;
2325
tokenBundleSizeExceedsLimit: TokenBundleSizeExceedsLimit;
@@ -124,6 +126,7 @@ const computeRequestedAssetChangeBundles = (
124126
utxoSelected: UtxoWithValue[],
125127
outputValues: Ogmios.util.OgmiosValue[],
126128
uniqueOutputAssetIDs: string[],
129+
implicitCoin: ImplicitCoinBigint,
127130
fee: bigint
128131
): Ogmios.util.OgmiosValue[] => {
129132
const assetTotals: Record<string, { selected: bigint; requested: bigint }> = {};
@@ -133,8 +136,8 @@ const computeRequestedAssetChangeBundles = (
133136
requested: assetQuantitySelector(assetId)(outputValues)
134137
};
135138
}
136-
const coinTotalSelected = getWithValuesCoinQuantity(utxoSelected);
137-
const coinTotalRequested = getCoinQuantity(outputValues) + fee;
139+
const coinTotalSelected = getWithValuesCoinQuantity(utxoSelected) + implicitCoin.input;
140+
const coinTotalRequested = getCoinQuantity(outputValues) + fee + implicitCoin.deposit;
138141
const coinChangeTotal = coinTotalSelected - coinTotalRequested;
139142

140143
const { totalCoinBundled, bundles, totalAssetsBundled } = createBundlePerOutput(
@@ -214,20 +217,23 @@ const computeChangeBundles = ({
214217
utxoSelection,
215218
outputValues,
216219
uniqueOutputAssetIDs,
220+
implicitCoin,
217221
computeMinimumCoinQuantity,
218222
fee = 0n
219223
}: {
220224
csl: CardanoSerializationLib;
221225
utxoSelection: UtxoSelection;
222226
outputValues: Ogmios.util.OgmiosValue[];
223227
uniqueOutputAssetIDs: string[];
228+
implicitCoin: ImplicitCoinBigint;
224229
computeMinimumCoinQuantity: ComputeMinimumCoinQuantity;
225230
fee?: bigint;
226231
}): UtxoSelection & { changeBundles: Ogmios.util.OgmiosValue[] } => {
227232
const requestedAssetChangeBundles = computeRequestedAssetChangeBundles(
228233
utxoSelection.utxoSelected,
229234
outputValues,
230235
uniqueOutputAssetIDs,
236+
implicitCoin,
231237
fee
232238
);
233239
const requestedAssetChangeBundlesWithLeftoverAssets = redistributeLeftoverAssets(
@@ -248,6 +254,7 @@ const computeChangeBundles = ({
248254
utxoSelection: pickExtraRandomUtxo(utxoSelection),
249255
outputValues,
250256
uniqueOutputAssetIDs,
257+
implicitCoin,
251258
computeMinimumCoinQuantity,
252259
fee
253260
});
@@ -295,13 +302,15 @@ export const computeChangeAndAdjustForFee = async ({
295302
estimateTxFee,
296303
outputValues,
297304
uniqueOutputAssetIDs,
305+
implicitCoin,
298306
utxoSelection
299307
}: ChangeComputationArgs): Promise<ChangeComputationResult> => {
300308
const changeInclFee = computeChangeBundles({
301309
csl,
302310
utxoSelection,
303311
outputValues,
304312
uniqueOutputAssetIDs,
313+
implicitCoin,
305314
computeMinimumCoinQuantity
306315
});
307316

@@ -314,8 +323,9 @@ export const computeChangeAndAdjustForFee = async ({
314323
);
315324

316325
// Ensure fee quantity is covered by current selection
317-
const outputValuesWithFee = [...outputValues, { coins: fee }];
318-
if (getCoinQuantity(outputValuesWithFee) > getWithValuesCoinQuantity(changeInclFee.utxoSelected)) {
326+
const totalOutputCoin = getCoinQuantity(outputValues) + fee + implicitCoin.deposit;
327+
const totalInputCoin = getWithValuesCoinQuantity(changeInclFee.utxoSelected) + implicitCoin.input;
328+
if (totalOutputCoin > totalInputCoin) {
319329
if (changeInclFee.utxoRemaining.length === 0) {
320330
throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient);
321331
}
@@ -327,6 +337,7 @@ export const computeChangeAndAdjustForFee = async ({
327337
outputValues,
328338
uniqueOutputAssetIDs,
329339
estimateTxFee,
340+
implicitCoin,
330341
utxoSelection: pickExtraRandomUtxo(changeInclFee)
331342
});
332343
}
@@ -336,6 +347,7 @@ export const computeChangeAndAdjustForFee = async ({
336347
utxoSelection: { utxoRemaining: changeInclFee.utxoRemaining, utxoSelected: changeInclFee.utxoSelected },
337348
outputValues,
338349
uniqueOutputAssetIDs,
350+
implicitCoin,
339351
computeMinimumCoinQuantity,
340352
fee
341353
});

Diff for: packages/cip2/src/RoundRobinRandomImprove/index.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,33 @@ export const roundRobinRandomImprove = (csl: CardanoSerializationLib): InputSele
99
select: async ({
1010
utxo,
1111
outputs,
12-
constraints: { computeMinimumCost, computeSelectionLimit, computeMinimumCoinQuantity, tokenBundleSizeExceedsLimit }
12+
constraints: { computeMinimumCost, computeSelectionLimit, computeMinimumCoinQuantity, tokenBundleSizeExceedsLimit },
13+
implicitCoin: implicitCoinAsNumber
1314
}: InputSelectionParameters): Promise<SelectionResult> => {
14-
const { uniqueOutputAssetIDs, utxosWithValue, outputsWithValue } = preprocessArgs(utxo, outputs);
15+
const { utxosWithValue, outputsWithValue, uniqueOutputAssetIDs, implicitCoin } = preprocessArgs(
16+
utxo,
17+
outputs,
18+
implicitCoinAsNumber
19+
);
1520

1621
const utxoValues = withValuesToValues(utxosWithValue);
1722
const outputValues = withValuesToValues(outputsWithValue);
18-
assertIsBalanceSufficient(uniqueOutputAssetIDs, utxoValues, outputValues);
23+
assertIsBalanceSufficient(uniqueOutputAssetIDs, utxoValues, outputValues, implicitCoin);
1924

20-
const roundRobinSelectionResult = roundRobinSelection(utxosWithValue, outputsWithValue, uniqueOutputAssetIDs);
25+
const roundRobinSelectionResult = roundRobinSelection({
26+
implicitCoin,
27+
outputsWithValue,
28+
utxosWithValue,
29+
uniqueOutputAssetIDs
30+
});
2131

2232
const result = await computeChangeAndAdjustForFee({
2333
csl,
2434
computeMinimumCoinQuantity,
2535
tokenBundleSizeExceedsLimit,
2636
outputValues,
2737
uniqueOutputAssetIDs,
38+
implicitCoin,
2839
utxoSelection: roundRobinSelectionResult,
2940
estimateTxFee: (utxos, changeValues) =>
3041
computeMinimumCost({

Diff for: packages/cip2/src/RoundRobinRandomImprove/roundRobin.ts

+22-16
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ import {
33
assetWithValueQuantitySelector,
44
getWithValuesCoinQuantity,
55
OutputWithValue,
6-
WithValue,
76
UtxoSelection,
8-
UtxoWithValue
7+
UtxoWithValue,
8+
RoundRobinRandomImproveArgs,
9+
ImplicitCoinBigint
910
} from './util';
1011

1112
const improvesSelection = (
1213
utxoAlreadySelected: UtxoWithValue[],
1314
input: UtxoWithValue,
1415
minimumTarget: bigint,
15-
getQuantity: (totals: WithValue[]) => bigint
16+
getQuantity: (utxo: UtxoWithValue[]) => bigint
1617
): boolean => {
1718
const oldQuantity = getQuantity(utxoAlreadySelected);
1819
// We still haven't reached the minimum target of
@@ -34,19 +35,23 @@ const improvesSelection = (
3435
return false;
3536
};
3637

37-
const listTokensWithin = (uniqueOutputAssetIDs: string[], outputs: OutputWithValue[]) => [
38+
const listTokensWithin = (
39+
uniqueOutputAssetIDs: string[],
40+
outputs: OutputWithValue[],
41+
implicitCoin: ImplicitCoinBigint
42+
) => [
3843
...uniqueOutputAssetIDs.map((id) => {
3944
const getQuantity = assetWithValueQuantitySelector(id);
4045
return {
41-
getQuantity,
46+
getTotalSelectedQuantity: (utxo: UtxoWithValue[]) => getQuantity(utxo),
4247
minimumTarget: getQuantity(outputs),
4348
filterUtxo: (utxo: UtxoWithValue[]) => utxo.filter(({ value: { assets } }) => assets?.[id])
4449
};
4550
}),
4651
{
4752
// Lovelace
48-
getQuantity: (totals: WithValue[]) => getWithValuesCoinQuantity(totals),
49-
minimumTarget: getWithValuesCoinQuantity(outputs),
53+
getTotalSelectedQuantity: (utxo: UtxoWithValue[]) => getWithValuesCoinQuantity(utxo) + implicitCoin.input,
54+
minimumTarget: getWithValuesCoinQuantity(outputs) + implicitCoin.deposit,
5055
filterUtxo: (utxo: UtxoWithValue[]) => utxo
5156
}
5257
];
@@ -57,27 +62,28 @@ const listTokensWithin = (uniqueOutputAssetIDs: string[], outputs: OutputWithVal
5762
* Assumes we have already checked that the available UTxO balance is sufficient to cover all tokens in the outputs.
5863
* Considers all outputs collectively, as a combined output bundle.
5964
*/
60-
export const roundRobinSelection = (
61-
availableUtxo: UtxoWithValue[],
62-
outputs: OutputWithValue[],
63-
uniqueOutputAssetIDs: string[]
64-
): UtxoSelection => {
65+
export const roundRobinSelection = ({
66+
utxosWithValue,
67+
outputsWithValue,
68+
uniqueOutputAssetIDs,
69+
implicitCoin
70+
}: RoundRobinRandomImproveArgs): UtxoSelection => {
6571
// The subset of the UTxO that has already been selected:
6672
const utxoSelected: UtxoWithValue[] = [];
6773
// The subset of the UTxO that remains available for selection:
68-
const utxoRemaining = [...availableUtxo];
74+
const utxoRemaining = [...utxosWithValue];
6975
// The set of tokens that we still need to cover:
70-
const tokensRemaining = listTokensWithin(uniqueOutputAssetIDs, outputs);
76+
const tokensRemaining = listTokensWithin(uniqueOutputAssetIDs, outputsWithValue, implicitCoin);
7177
while (tokensRemaining.length > 0) {
7278
// Consider each token in round-robin fashion:
73-
for (const [tokenIdx, { filterUtxo, minimumTarget, getQuantity }] of tokensRemaining.entries()) {
79+
for (const [tokenIdx, { filterUtxo, minimumTarget, getTotalSelectedQuantity }] of tokensRemaining.entries()) {
7480
// Attempt to select at random an input that includes
7581
// this token from the remaining UTxO set:
7682
const utxo = filterUtxo(utxoRemaining);
7783
if (utxo.length > 0) {
7884
const inputIdx = Math.floor(Math.random() * utxo.length);
7985
const input = utxo[inputIdx];
80-
if (improvesSelection(utxoSelected, input, minimumTarget, getQuantity)) {
86+
if (improvesSelection(utxoSelected, input, minimumTarget, getTotalSelectedQuantity)) {
8187
utxoSelected.push(input);
8288
utxoRemaining.splice(utxoRemaining.indexOf(input), 1);
8389
} else {

Diff for: packages/cip2/src/RoundRobinRandomImprove/util.ts

+25-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BigIntMath, CSL, Ogmios } from '@cardano-sdk/core';
22
import { uniq } from 'lodash-es';
3+
import { ImplicitCoin } from '../types';
34
import { InputSelectionError, InputSelectionFailure } from '../InputSelectionError';
45

56
export interface WithValue {
@@ -14,20 +15,32 @@ export interface OutputWithValue extends WithValue {
1415
output: CSL.TransactionOutput;
1516
}
1617

18+
export interface ImplicitCoinBigint {
19+
input: bigint;
20+
deposit: bigint;
21+
}
22+
1723
export interface RoundRobinRandomImproveArgs {
1824
utxosWithValue: UtxoWithValue[];
1925
outputsWithValue: OutputWithValue[];
2026
uniqueOutputAssetIDs: string[];
27+
implicitCoin: ImplicitCoinBigint;
2128
}
2229

2330
export interface UtxoSelection {
2431
utxoSelected: UtxoWithValue[];
2532
utxoRemaining: UtxoWithValue[];
2633
}
2734

35+
const noImplicitCoin = {
36+
deposit: 0,
37+
input: 0
38+
};
39+
2840
export const preprocessArgs = (
2941
availableUtxo: Set<CSL.TransactionUnspentOutput>,
30-
outputs: Set<CSL.TransactionOutput>
42+
outputs: Set<CSL.TransactionOutput>,
43+
implicitCoinAsNumber: ImplicitCoin = noImplicitCoin
3144
): RoundRobinRandomImproveArgs => {
3245
const utxosWithValue = [...availableUtxo].map((utxo) => ({
3346
utxo,
@@ -40,7 +53,11 @@ export const preprocessArgs = (
4053
const uniqueOutputAssetIDs = uniq(
4154
outputsWithValue.flatMap(({ value: { assets } }) => (assets && Object.keys(assets)) || [])
4255
);
43-
return { uniqueOutputAssetIDs, utxosWithValue, outputsWithValue };
56+
const implicitCoin: ImplicitCoinBigint = {
57+
deposit: BigInt(implicitCoinAsNumber.deposit || 0),
58+
input: BigInt(implicitCoinAsNumber.input || 0)
59+
};
60+
return { uniqueOutputAssetIDs, utxosWithValue, outputsWithValue, implicitCoin };
4461
};
4562

4663
export const withValuesToValues = (totals: WithValue[]) => totals.map((t) => t.value);
@@ -58,11 +75,12 @@ export const getWithValuesCoinQuantity = (totals: WithValue[]): bigint => getCoi
5875

5976
export const assertIsCoinBalanceSufficient = (
6077
utxoValues: Ogmios.util.OgmiosValue[],
61-
outputValues: Ogmios.util.OgmiosValue[]
78+
outputValues: Ogmios.util.OgmiosValue[],
79+
implicitCoin: ImplicitCoinBigint
6280
) => {
6381
const utxoCoinTotal = getCoinQuantity(utxoValues);
6482
const outputsCoinTotal = getCoinQuantity(outputValues);
65-
if (outputsCoinTotal > utxoCoinTotal) {
83+
if (outputsCoinTotal + implicitCoin.deposit > utxoCoinTotal + implicitCoin.input) {
6684
throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient);
6785
}
6886
};
@@ -76,7 +94,8 @@ export const assertIsCoinBalanceSufficient = (
7694
export const assertIsBalanceSufficient = (
7795
uniqueOutputAssetIDs: string[],
7896
utxoValues: Ogmios.util.OgmiosValue[],
79-
outputValues: Ogmios.util.OgmiosValue[]
97+
outputValues: Ogmios.util.OgmiosValue[],
98+
implicitCoin: ImplicitCoinBigint
8099
): void => {
81100
for (const assetId of uniqueOutputAssetIDs) {
82101
const getAssetQuantity = assetQuantitySelector(assetId);
@@ -86,5 +105,5 @@ export const assertIsBalanceSufficient = (
86105
throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient);
87106
}
88107
}
89-
assertIsCoinBalanceSufficient(utxoValues, outputValues);
108+
assertIsCoinBalanceSufficient(utxoValues, outputValues, implicitCoin);
90109
};

Diff for: packages/cip2/test/RoundRobinRandomImprove.test.ts

+25-21
Original file line numberDiff line numberDiff line change
@@ -156,30 +156,34 @@ describe('RoundRobinRandomImprove', () => {
156156
const algorithm = getRoundRobinRandomImprove(csl);
157157

158158
await fc.assert(
159-
fc.asyncProperty(generateSelectionParams(), async ({ utxoAmounts, outputsAmounts, constraints }) => {
160-
// Run input selection
161-
const utxo = new Set(
162-
utxoAmounts.map((valueQuantities) => CslTestUtil.createUnspentTxOutput(csl, valueQuantities))
163-
);
164-
const outputs = new Set(
165-
outputsAmounts.map((valueQuantities) => CslTestUtil.createOutput(csl, valueQuantities))
166-
);
159+
fc.asyncProperty(
160+
generateSelectionParams(),
161+
async ({ utxoAmounts, outputsAmounts, constraints, implicitCoin }) => {
162+
// Run input selection
163+
const utxo = new Set(
164+
utxoAmounts.map((valueQuantities) => CslTestUtil.createUnspentTxOutput(csl, valueQuantities))
165+
);
166+
const outputs = new Set(
167+
outputsAmounts.map((valueQuantities) => CslTestUtil.createOutput(csl, valueQuantities))
168+
);
167169

168-
try {
169-
const results = await algorithm.select({
170-
utxo: new Set(utxo),
171-
outputs,
172-
constraints: SelectionConstraints.mockConstraintsToConstraints(constraints)
173-
});
174-
assertInputSelectionProperties({ results, outputs, utxo, constraints });
175-
} catch (error) {
176-
if (error instanceof InputSelectionError) {
177-
assertFailureProperties({ error, utxoAmounts, outputsAmounts, constraints });
178-
} else {
179-
throw error;
170+
try {
171+
const results = await algorithm.select({
172+
utxo: new Set(utxo),
173+
outputs,
174+
constraints: SelectionConstraints.mockConstraintsToConstraints(constraints),
175+
implicitCoin
176+
});
177+
assertInputSelectionProperties({ results, outputs, utxo, constraints, implicitCoin });
178+
} catch (error) {
179+
if (error instanceof InputSelectionError) {
180+
assertFailureProperties({ error, utxoAmounts, outputsAmounts, constraints, implicitCoin });
181+
} else {
182+
throw error;
183+
}
180184
}
181185
}
182-
})
186+
)
183187
);
184188
});
185189
});

0 commit comments

Comments
 (0)