Skip to content

Commit 3361855

Browse files
committed
feat(cip2): add implicit tokens support (mint/burn) for input selection
1 parent 3242a0d commit 3361855

File tree

8 files changed

+266
-133
lines changed

8 files changed

+266
-133
lines changed

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

+55-33
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Cardano } from '@cardano-sdk/core';
22
import { ComputeMinimumCoinQuantity, TokenBundleSizeExceedsLimit } from '../types';
33
import { InputSelectionError, InputSelectionFailure } from '../InputSelectionError';
4-
import { UtxoSelection, assetQuantitySelector, getCoinQuantity, toValues } from './util';
4+
import { RequiredImplicitValue, UtxoSelection, assetQuantitySelector, getCoinQuantity, toValues } from './util';
5+
import minBy from 'lodash/minBy';
56
import orderBy from 'lodash/orderBy';
67
import pick from 'lodash/pick';
78

@@ -10,8 +11,8 @@ type EstimateTxFeeWithOriginalOutputs = (utxo: Cardano.Utxo[], change: Cardano.V
1011
interface ChangeComputationArgs {
1112
utxoSelection: UtxoSelection;
1213
outputValues: Cardano.Value[];
13-
uniqueOutputAssetIDs: Cardano.AssetId[];
14-
implicitCoin: Required<Cardano.util.ImplicitCoin>;
14+
uniqueTxAssetIDs: Cardano.AssetId[];
15+
implicitValue: RequiredImplicitValue;
1516
estimateTxFee: EstimateTxFeeWithOriginalOutputs;
1617
computeMinimumCoinQuantity: ComputeMinimumCoinQuantity;
1718
tokenBundleSizeExceedsLimit: TokenBundleSizeExceedsLimit;
@@ -25,7 +26,7 @@ interface ChangeComputationResult {
2526
fee: Cardano.Lovelace;
2627
}
2728

28-
const getLeftoverAssets = (utxoSelected: Cardano.Utxo[], uniqueOutputAssetIDs: Cardano.AssetId[]) => {
29+
const getLeftoverAssets = (utxoSelected: Cardano.Utxo[], uniqueTxAssetIDs: Cardano.AssetId[]) => {
2930
const leftovers: Map<Cardano.AssetId, Array<bigint>> = new Map();
3031
for (const [
3132
_,
@@ -34,7 +35,7 @@ const getLeftoverAssets = (utxoSelected: Cardano.Utxo[], uniqueOutputAssetIDs: C
3435
}
3536
] of utxoSelected) {
3637
if (assets) {
37-
const leftoverAssetKeys = [...assets.keys()].filter((id) => !uniqueOutputAssetIDs.includes(id));
38+
const leftoverAssetKeys = [...assets.keys()].filter((id) => !uniqueTxAssetIDs.includes(id));
3839
for (const assetKey of leftoverAssetKeys) {
3940
const quantity = assets.get(assetKey)!;
4041
if (quantity === 0n) continue;
@@ -56,9 +57,9 @@ const getLeftoverAssets = (utxoSelected: Cardano.Utxo[], uniqueOutputAssetIDs: C
5657
const redistributeLeftoverAssets = (
5758
utxoSelected: Cardano.Utxo[],
5859
requestedAssetChangeBundles: Cardano.Value[],
59-
uniqueOutputAssetIDs: Cardano.AssetId[]
60+
uniqueTxAssetIDs: Cardano.AssetId[]
6061
) => {
61-
const leftovers = getLeftoverAssets(utxoSelected, uniqueOutputAssetIDs);
62+
const leftovers = getLeftoverAssets(utxoSelected, uniqueTxAssetIDs);
6263
// Distribute leftovers to result bundles
6364
const resultBundles = [...requestedAssetChangeBundles];
6465
for (const assetId of leftovers.keys()) {
@@ -110,6 +111,23 @@ const createBundlePerOutput = (
110111
return { bundles, totalAssetsBundled, totalCoinBundled };
111112
};
112113

114+
/**
115+
* Creates a new bundle if there are none (mutates 'bundles' object passed as arg).
116+
* Creates a new token map if there is none (mutates bundle.assets).
117+
*
118+
* @returns bundle with smallest token bundle.
119+
*/
120+
const smallestBundleTokenMap = (bundles: Cardano.Value[]) => {
121+
if (bundles.length === 0) {
122+
const bundle = { assets: new Map(), coins: 0n };
123+
bundles.push(bundle);
124+
return bundle.assets!;
125+
}
126+
const bundle = minBy(bundles, ({ assets }) => assets?.size || 0)!;
127+
if (!bundle.assets) bundle.assets = new Map();
128+
return bundle.assets!;
129+
};
130+
113131
/**
114132
* Divide any excess token quantities (inputs − outputs) into change bundles, where:
115133
* - there is exactly one change bundle for each output.
@@ -121,16 +139,16 @@ const createBundlePerOutput = (
121139
const computeRequestedAssetChangeBundles = (
122140
utxoSelected: Cardano.Utxo[],
123141
outputValues: Cardano.Value[],
124-
uniqueOutputAssetIDs: Cardano.AssetId[],
125-
implicitCoin: Required<Cardano.util.ImplicitCoin>,
142+
uniqueTxAssetIDs: Cardano.AssetId[],
143+
{ implicitCoin, implicitTokens }: RequiredImplicitValue,
126144
fee: Cardano.Lovelace
127145
): Cardano.Value[] => {
128146
const assetTotals: Map<Cardano.AssetId, { selected: bigint; requested: bigint }> = new Map();
129147
const utxoSelectedValues = toValues(utxoSelected);
130-
for (const assetId of uniqueOutputAssetIDs) {
148+
for (const assetId of uniqueTxAssetIDs) {
131149
assetTotals.set(assetId, {
132-
requested: assetQuantitySelector(assetId)(outputValues),
133-
selected: assetQuantitySelector(assetId)(utxoSelectedValues)
150+
requested: assetQuantitySelector(assetId)(outputValues) + implicitTokens.spend(assetId),
151+
selected: assetQuantitySelector(assetId)(utxoSelectedValues) + implicitTokens.input(assetId)
134152
});
135153
}
136154
const coinTotalSelected = getCoinQuantity(utxoSelectedValues) + implicitCoin.input;
@@ -153,12 +171,15 @@ const computeRequestedAssetChangeBundles = (
153171
bundles[0].coins += coinLost;
154172
}
155173
}
156-
for (const assetId of uniqueOutputAssetIDs) {
174+
for (const assetId of uniqueTxAssetIDs) {
157175
const assetTotal = assetTotals.get(assetId)!;
158-
const assetLost = assetTotal.selected - assetTotal.requested - totalAssetsBundled.get(assetId)!;
176+
const bundled = totalAssetsBundled.get(assetId) || 0n;
177+
const assetLost = assetTotal.selected - assetTotal.requested - bundled;
159178
if (assetLost > 0n) {
160-
const anyBundle = bundles.find(({ assets }) => assets?.has(assetId))!;
161-
anyBundle.assets?.set(assetId, anyBundle.assets!.get(assetId)! + assetLost);
179+
const anyChangeTokenBundle =
180+
bundles.find(({ assets }) => assets?.has(assetId))?.assets || smallestBundleTokenMap(bundles);
181+
const assetQuantityAlreadyInBundle = anyChangeTokenBundle.get(assetId) || 0n;
182+
anyChangeTokenBundle.set(assetId, assetQuantityAlreadyInBundle + assetLost);
162183
}
163184
}
164185

@@ -227,29 +248,29 @@ const coalesceChangeBundlesForMinCoinRequirement = (
227248
const computeChangeBundles = ({
228249
utxoSelection,
229250
outputValues,
230-
uniqueOutputAssetIDs,
231-
implicitCoin,
251+
uniqueTxAssetIDs,
252+
implicitValue,
232253
computeMinimumCoinQuantity,
233254
fee = 0n
234255
}: {
235256
utxoSelection: UtxoSelection;
236257
outputValues: Cardano.Value[];
237-
uniqueOutputAssetIDs: Cardano.AssetId[];
238-
implicitCoin: Required<Cardano.util.ImplicitCoin>;
258+
uniqueTxAssetIDs: Cardano.AssetId[];
259+
implicitValue: RequiredImplicitValue;
239260
computeMinimumCoinQuantity: ComputeMinimumCoinQuantity;
240261
fee?: bigint;
241262
}): (UtxoSelection & { changeBundles: Cardano.Value[] }) | false => {
242263
const requestedAssetChangeBundles = computeRequestedAssetChangeBundles(
243264
utxoSelection.utxoSelected,
244265
outputValues,
245-
uniqueOutputAssetIDs,
246-
implicitCoin,
266+
uniqueTxAssetIDs,
267+
implicitValue,
247268
fee
248269
);
249270
const requestedAssetChangeBundlesWithLeftoverAssets = redistributeLeftoverAssets(
250271
utxoSelection.utxoSelected,
251272
requestedAssetChangeBundles,
252-
uniqueOutputAssetIDs
273+
uniqueTxAssetIDs
253274
);
254275
const changeBundles = coalesceChangeBundlesForMinCoinRequirement(
255276
requestedAssetChangeBundlesWithLeftoverAssets,
@@ -290,8 +311,8 @@ export const computeChangeAndAdjustForFee = async ({
290311
tokenBundleSizeExceedsLimit,
291312
estimateTxFee,
292313
outputValues,
293-
uniqueOutputAssetIDs,
294-
implicitCoin,
314+
uniqueTxAssetIDs,
315+
implicitValue,
295316
random,
296317
utxoSelection
297318
}: ChangeComputationArgs): Promise<ChangeComputationResult> => {
@@ -300,11 +321,11 @@ export const computeChangeAndAdjustForFee = async ({
300321
return computeChangeAndAdjustForFee({
301322
computeMinimumCoinQuantity,
302323
estimateTxFee,
303-
implicitCoin,
324+
implicitValue,
304325
outputValues,
305326
random,
306327
tokenBundleSizeExceedsLimit,
307-
uniqueOutputAssetIDs,
328+
uniqueTxAssetIDs,
308329
utxoSelection: pickExtraRandomUtxo(currentUtxoSelection, random)
309330
});
310331
}
@@ -317,9 +338,9 @@ export const computeChangeAndAdjustForFee = async ({
317338

318339
const selectionWithChangeAndFee = computeChangeBundles({
319340
computeMinimumCoinQuantity,
320-
implicitCoin,
341+
implicitValue,
321342
outputValues,
322-
uniqueOutputAssetIDs,
343+
uniqueTxAssetIDs,
323344
utxoSelection
324345
});
325346
if (!selectionWithChangeAndFee) return recomputeChangeAndAdjustForFeeWithExtraUtxo(utxoSelection);
@@ -333,8 +354,9 @@ export const computeChangeAndAdjustForFee = async ({
333354
);
334355

335356
// Ensure fee quantity is covered by current selection
336-
const totalOutputCoin = getCoinQuantity(outputValues) + fee + implicitCoin.deposit;
337-
const totalInputCoin = getCoinQuantity(toValues(selectionWithChangeAndFee.utxoSelected)) + implicitCoin.input;
357+
const totalOutputCoin = getCoinQuantity(outputValues) + fee + implicitValue.implicitCoin.deposit;
358+
const totalInputCoin =
359+
getCoinQuantity(toValues(selectionWithChangeAndFee.utxoSelected)) + implicitValue.implicitCoin.input;
338360
if (totalOutputCoin > totalInputCoin) {
339361
if (selectionWithChangeAndFee.utxoRemaining.length === 0) {
340362
throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient);
@@ -346,9 +368,9 @@ export const computeChangeAndAdjustForFee = async ({
346368
const finalSelection = computeChangeBundles({
347369
computeMinimumCoinQuantity,
348370
fee,
349-
implicitCoin,
371+
implicitValue,
350372
outputValues,
351-
uniqueOutputAssetIDs,
373+
uniqueTxAssetIDs,
352374
utxoSelection: pick(selectionWithChangeAndFee, ['utxoRemaining', 'utxoSelected'])
353375
});
354376

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

+8-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { InputSelectionError, InputSelectionFailure } from '../InputSelectionError';
22
import { InputSelectionParameters, InputSelector, SelectionResult } from '../types';
3-
import { assertIsBalanceSufficient, preprocessArgs, toValues } from './util';
3+
import { assertIsBalanceSufficient, preProcessArgs, toValues } from './util';
44
import { computeChangeAndAdjustForFee } from './change';
55
import { cslUtil } from '@cardano-sdk/core';
66
import { roundRobinSelection } from './roundRobin';
@@ -16,21 +16,17 @@ export const roundRobinRandomImprove = ({
1616
utxo: utxoSet,
1717
outputs: outputSet,
1818
constraints: { computeMinimumCost, computeSelectionLimit, computeMinimumCoinQuantity, tokenBundleSizeExceedsLimit },
19-
implicitValue = {}
19+
implicitValue: partialImplicitValue = {}
2020
}: InputSelectionParameters): Promise<SelectionResult> => {
21-
const { utxo, outputs, uniqueOutputAssetIDs, implicitCoin } = preprocessArgs(
22-
utxoSet,
23-
outputSet,
24-
implicitValue.coin
25-
);
21+
const { utxo, outputs, uniqueTxAssetIDs, implicitValue } = preProcessArgs(utxoSet, outputSet, partialImplicitValue);
2622

27-
assertIsBalanceSufficient(uniqueOutputAssetIDs, utxo, outputs, implicitCoin);
23+
assertIsBalanceSufficient(uniqueTxAssetIDs, utxo, outputs, implicitValue);
2824

2925
const roundRobinSelectionResult = roundRobinSelection({
30-
implicitCoin,
26+
implicitValue,
3127
outputs,
3228
random,
33-
uniqueOutputAssetIDs,
29+
uniqueTxAssetIDs,
3430
utxo
3531
});
3632

@@ -43,11 +39,11 @@ export const roundRobinRandomImprove = ({
4339
inputs: new Set(utxos),
4440
outputs: outputSet
4541
}),
46-
implicitCoin,
42+
implicitValue,
4743
outputValues: toValues(outputs),
4844
random,
4945
tokenBundleSizeExceedsLimit,
50-
uniqueOutputAssetIDs,
46+
uniqueTxAssetIDs,
5147
utxoSelection: roundRobinSelectionResult
5248
});
5349

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

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { BigIntMath } from '@cardano-sdk/util';
22
import { Cardano } from '@cardano-sdk/core';
3-
import { RoundRobinRandomImproveArgs, UtxoSelection, assetQuantitySelector, getCoinQuantity, toValues } from './util';
3+
import {
4+
RequiredImplicitValue,
5+
RoundRobinRandomImproveArgs,
6+
UtxoSelection,
7+
assetQuantitySelector,
8+
getCoinQuantity,
9+
toValues
10+
} from './util';
411

512
const improvesSelection = (
613
utxoAlreadySelected: Cardano.Utxo[],
@@ -31,10 +38,12 @@ const improvesSelection = (
3138
const listTokensWithin = (
3239
uniqueOutputAssetIDs: Cardano.AssetId[],
3340
outputs: Cardano.TxOut[],
34-
implicitCoin: Required<Cardano.util.ImplicitCoin>
41+
{ implicitCoin, implicitTokens }: RequiredImplicitValue
3542
) => [
3643
...uniqueOutputAssetIDs.map((id) => {
3744
const getQuantity = assetQuantitySelector(id);
45+
const implicitInput = implicitTokens.input(id);
46+
const implicitSpend = implicitTokens.spend(id);
3847
return {
3948
filterUtxo: (utxo: Cardano.Utxo[]) =>
4049
utxo.filter(
@@ -45,8 +54,8 @@ const listTokensWithin = (
4554
}
4655
]) => assets?.get(id)
4756
),
48-
getTotalSelectedQuantity: (utxo: Cardano.Utxo[]) => getQuantity(toValues(utxo)),
49-
minimumTarget: getQuantity(toValues(outputs))
57+
getTotalSelectedQuantity: (utxo: Cardano.Utxo[]) => getQuantity(toValues(utxo)) + implicitInput,
58+
minimumTarget: getQuantity(toValues(outputs)) + implicitSpend
5059
};
5160
}),
5261
{
@@ -66,16 +75,16 @@ const listTokensWithin = (
6675
export const roundRobinSelection = ({
6776
utxo: utxosWithValue,
6877
outputs: outputsWithValue,
69-
uniqueOutputAssetIDs,
78+
uniqueTxAssetIDs,
7079
random,
71-
implicitCoin
80+
implicitValue
7281
}: RoundRobinRandomImproveArgs): UtxoSelection => {
7382
// The subset of the UTxO that has already been selected:
7483
const utxoSelected: Cardano.Utxo[] = [];
7584
// The subset of the UTxO that remains available for selection:
7685
const utxoRemaining = [...utxosWithValue];
7786
// The set of tokens that we still need to cover:
78-
const tokensRemaining = listTokensWithin(uniqueOutputAssetIDs, outputsWithValue, implicitCoin);
87+
const tokensRemaining = listTokensWithin(uniqueTxAssetIDs, outputsWithValue, implicitValue);
7988
while (tokensRemaining.length > 0) {
8089
// Consider each token in round-robin fashion:
8190
for (const [tokenIdx, { filterUtxo, minimumTarget, getTotalSelectedQuantity }] of tokensRemaining.entries()) {

0 commit comments

Comments
 (0)