1
1
import { Cardano } from '@cardano-sdk/core' ;
2
2
import { ComputeMinimumCoinQuantity , TokenBundleSizeExceedsLimit } from '../types' ;
3
3
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' ;
5
6
import orderBy from 'lodash/orderBy' ;
6
7
import pick from 'lodash/pick' ;
7
8
@@ -10,8 +11,8 @@ type EstimateTxFeeWithOriginalOutputs = (utxo: Cardano.Utxo[], change: Cardano.V
10
11
interface ChangeComputationArgs {
11
12
utxoSelection : UtxoSelection ;
12
13
outputValues : Cardano . Value [ ] ;
13
- uniqueOutputAssetIDs : Cardano . AssetId [ ] ;
14
- implicitCoin : Required < Cardano . util . ImplicitCoin > ;
14
+ uniqueTxAssetIDs : Cardano . AssetId [ ] ;
15
+ implicitValue : RequiredImplicitValue ;
15
16
estimateTxFee : EstimateTxFeeWithOriginalOutputs ;
16
17
computeMinimumCoinQuantity : ComputeMinimumCoinQuantity ;
17
18
tokenBundleSizeExceedsLimit : TokenBundleSizeExceedsLimit ;
@@ -25,7 +26,7 @@ interface ChangeComputationResult {
25
26
fee : Cardano . Lovelace ;
26
27
}
27
28
28
- const getLeftoverAssets = ( utxoSelected : Cardano . Utxo [ ] , uniqueOutputAssetIDs : Cardano . AssetId [ ] ) => {
29
+ const getLeftoverAssets = ( utxoSelected : Cardano . Utxo [ ] , uniqueTxAssetIDs : Cardano . AssetId [ ] ) => {
29
30
const leftovers : Map < Cardano . AssetId , Array < bigint > > = new Map ( ) ;
30
31
for ( const [
31
32
_ ,
@@ -34,7 +35,7 @@ const getLeftoverAssets = (utxoSelected: Cardano.Utxo[], uniqueOutputAssetIDs: C
34
35
}
35
36
] of utxoSelected ) {
36
37
if ( assets ) {
37
- const leftoverAssetKeys = [ ...assets . keys ( ) ] . filter ( ( id ) => ! uniqueOutputAssetIDs . includes ( id ) ) ;
38
+ const leftoverAssetKeys = [ ...assets . keys ( ) ] . filter ( ( id ) => ! uniqueTxAssetIDs . includes ( id ) ) ;
38
39
for ( const assetKey of leftoverAssetKeys ) {
39
40
const quantity = assets . get ( assetKey ) ! ;
40
41
if ( quantity === 0n ) continue ;
@@ -56,9 +57,9 @@ const getLeftoverAssets = (utxoSelected: Cardano.Utxo[], uniqueOutputAssetIDs: C
56
57
const redistributeLeftoverAssets = (
57
58
utxoSelected : Cardano . Utxo [ ] ,
58
59
requestedAssetChangeBundles : Cardano . Value [ ] ,
59
- uniqueOutputAssetIDs : Cardano . AssetId [ ]
60
+ uniqueTxAssetIDs : Cardano . AssetId [ ]
60
61
) => {
61
- const leftovers = getLeftoverAssets ( utxoSelected , uniqueOutputAssetIDs ) ;
62
+ const leftovers = getLeftoverAssets ( utxoSelected , uniqueTxAssetIDs ) ;
62
63
// Distribute leftovers to result bundles
63
64
const resultBundles = [ ...requestedAssetChangeBundles ] ;
64
65
for ( const assetId of leftovers . keys ( ) ) {
@@ -110,6 +111,23 @@ const createBundlePerOutput = (
110
111
return { bundles, totalAssetsBundled, totalCoinBundled } ;
111
112
} ;
112
113
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
+
113
131
/**
114
132
* Divide any excess token quantities (inputs − outputs) into change bundles, where:
115
133
* - there is exactly one change bundle for each output.
@@ -121,16 +139,16 @@ const createBundlePerOutput = (
121
139
const computeRequestedAssetChangeBundles = (
122
140
utxoSelected : Cardano . Utxo [ ] ,
123
141
outputValues : Cardano . Value [ ] ,
124
- uniqueOutputAssetIDs : Cardano . AssetId [ ] ,
125
- implicitCoin : Required < Cardano . util . ImplicitCoin > ,
142
+ uniqueTxAssetIDs : Cardano . AssetId [ ] ,
143
+ { implicitCoin, implicitTokens } : RequiredImplicitValue ,
126
144
fee : Cardano . Lovelace
127
145
) : Cardano . Value [ ] => {
128
146
const assetTotals : Map < Cardano . AssetId , { selected : bigint ; requested : bigint } > = new Map ( ) ;
129
147
const utxoSelectedValues = toValues ( utxoSelected ) ;
130
- for ( const assetId of uniqueOutputAssetIDs ) {
148
+ for ( const assetId of uniqueTxAssetIDs ) {
131
149
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 )
134
152
} ) ;
135
153
}
136
154
const coinTotalSelected = getCoinQuantity ( utxoSelectedValues ) + implicitCoin . input ;
@@ -153,12 +171,15 @@ const computeRequestedAssetChangeBundles = (
153
171
bundles [ 0 ] . coins += coinLost ;
154
172
}
155
173
}
156
- for ( const assetId of uniqueOutputAssetIDs ) {
174
+ for ( const assetId of uniqueTxAssetIDs ) {
157
175
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 ;
159
178
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 ) ;
162
183
}
163
184
}
164
185
@@ -227,29 +248,29 @@ const coalesceChangeBundlesForMinCoinRequirement = (
227
248
const computeChangeBundles = ( {
228
249
utxoSelection,
229
250
outputValues,
230
- uniqueOutputAssetIDs ,
231
- implicitCoin ,
251
+ uniqueTxAssetIDs ,
252
+ implicitValue ,
232
253
computeMinimumCoinQuantity,
233
254
fee = 0n
234
255
} : {
235
256
utxoSelection : UtxoSelection ;
236
257
outputValues : Cardano . Value [ ] ;
237
- uniqueOutputAssetIDs : Cardano . AssetId [ ] ;
238
- implicitCoin : Required < Cardano . util . ImplicitCoin > ;
258
+ uniqueTxAssetIDs : Cardano . AssetId [ ] ;
259
+ implicitValue : RequiredImplicitValue ;
239
260
computeMinimumCoinQuantity : ComputeMinimumCoinQuantity ;
240
261
fee ?: bigint ;
241
262
} ) : ( UtxoSelection & { changeBundles : Cardano . Value [ ] } ) | false => {
242
263
const requestedAssetChangeBundles = computeRequestedAssetChangeBundles (
243
264
utxoSelection . utxoSelected ,
244
265
outputValues ,
245
- uniqueOutputAssetIDs ,
246
- implicitCoin ,
266
+ uniqueTxAssetIDs ,
267
+ implicitValue ,
247
268
fee
248
269
) ;
249
270
const requestedAssetChangeBundlesWithLeftoverAssets = redistributeLeftoverAssets (
250
271
utxoSelection . utxoSelected ,
251
272
requestedAssetChangeBundles ,
252
- uniqueOutputAssetIDs
273
+ uniqueTxAssetIDs
253
274
) ;
254
275
const changeBundles = coalesceChangeBundlesForMinCoinRequirement (
255
276
requestedAssetChangeBundlesWithLeftoverAssets ,
@@ -290,8 +311,8 @@ export const computeChangeAndAdjustForFee = async ({
290
311
tokenBundleSizeExceedsLimit,
291
312
estimateTxFee,
292
313
outputValues,
293
- uniqueOutputAssetIDs ,
294
- implicitCoin ,
314
+ uniqueTxAssetIDs ,
315
+ implicitValue ,
295
316
random,
296
317
utxoSelection
297
318
} : ChangeComputationArgs ) : Promise < ChangeComputationResult > => {
@@ -300,11 +321,11 @@ export const computeChangeAndAdjustForFee = async ({
300
321
return computeChangeAndAdjustForFee ( {
301
322
computeMinimumCoinQuantity,
302
323
estimateTxFee,
303
- implicitCoin ,
324
+ implicitValue ,
304
325
outputValues,
305
326
random,
306
327
tokenBundleSizeExceedsLimit,
307
- uniqueOutputAssetIDs ,
328
+ uniqueTxAssetIDs ,
308
329
utxoSelection : pickExtraRandomUtxo ( currentUtxoSelection , random )
309
330
} ) ;
310
331
}
@@ -317,9 +338,9 @@ export const computeChangeAndAdjustForFee = async ({
317
338
318
339
const selectionWithChangeAndFee = computeChangeBundles ( {
319
340
computeMinimumCoinQuantity,
320
- implicitCoin ,
341
+ implicitValue ,
321
342
outputValues,
322
- uniqueOutputAssetIDs ,
343
+ uniqueTxAssetIDs ,
323
344
utxoSelection
324
345
} ) ;
325
346
if ( ! selectionWithChangeAndFee ) return recomputeChangeAndAdjustForFeeWithExtraUtxo ( utxoSelection ) ;
@@ -333,8 +354,9 @@ export const computeChangeAndAdjustForFee = async ({
333
354
) ;
334
355
335
356
// 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 ;
338
360
if ( totalOutputCoin > totalInputCoin ) {
339
361
if ( selectionWithChangeAndFee . utxoRemaining . length === 0 ) {
340
362
throw new InputSelectionError ( InputSelectionFailure . UtxoBalanceInsufficient ) ;
@@ -346,9 +368,9 @@ export const computeChangeAndAdjustForFee = async ({
346
368
const finalSelection = computeChangeBundles ( {
347
369
computeMinimumCoinQuantity,
348
370
fee,
349
- implicitCoin ,
371
+ implicitValue ,
350
372
outputValues,
351
- uniqueOutputAssetIDs ,
373
+ uniqueTxAssetIDs ,
352
374
utxoSelection : pick ( selectionWithChangeAndFee , [ 'utxoRemaining' , 'utxoSelected' ] )
353
375
} ) ;
354
376
0 commit comments