@indigo-labs/indigo-sdk 0.3.9 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigo-labs/indigo-sdk",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Indigo SDK for interacting with Indigo endpoints via lucid-evolution",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -17,6 +17,18 @@ import {
17
17
  rationalSub,
18
18
  } from '../../types/rational';
19
19
 
20
+ /**
21
+ * Amount of iasset equal in value to the given number of collateral amount.
22
+ */
23
+ export function iassetValueOfCollateral(
24
+ collateralAmt: bigint,
25
+ oraclePrice: Rational,
26
+ ): bigint {
27
+ return rationalFloor(
28
+ rationalDiv(rationalFromInt(collateralAmt), oraclePrice),
29
+ );
30
+ }
31
+
20
32
  /**
21
33
  * This is mostly for debugging purposes.
22
34
  */
@@ -252,11 +252,7 @@ export async function createProposal(
252
252
  ),
253
253
  )
254
254
  .validFrom(Number(currentTime) - ONE_SECOND)
255
- .validTo(
256
- Number(currentTime) +
257
- Number(sysParams.govParams.gBiasTime) -
258
- ONE_SECOND,
259
- )
255
+ .validTo(Number(currentTime + sysParams.govParams.gBiasTime) - ONE_SECOND)
260
256
  .addSigner(ownAddr),
261
257
  newPollId,
262
258
  ];
@@ -325,9 +321,7 @@ export async function createShardsChunks(
325
321
  .newTx()
326
322
  .validFrom(Number(currentTime) - ONE_SECOND)
327
323
  .validTo(
328
- Number(currentTime) +
329
- Number(sysParams.pollManagerParams.pBiasTime) -
330
- ONE_SECOND,
324
+ Number(currentTime + sysParams.pollManagerParams.pBiasTime) - ONE_SECOND,
331
325
  )
332
326
  .mintAssets(mkAssetsOf(pollNft, shardsCount), Data.void())
333
327
  // Ref scripts
@@ -580,9 +574,7 @@ export async function mergeShards(
580
574
  .newTx()
581
575
  .validFrom(Number(currentTime) - ONE_SECOND)
582
576
  .validTo(
583
- Number(currentTime) +
584
- Number(sysParams.pollManagerParams.pBiasTime) -
585
- ONE_SECOND,
577
+ Number(currentTime + sysParams.pollManagerParams.pBiasTime) - ONE_SECOND,
586
578
  )
587
579
  .readFrom([
588
580
  pollShardRefScriptUtxo,
@@ -1561,9 +1553,7 @@ export async function executeProposal(
1561
1553
 
1562
1554
  tx.readFrom([upgradeTokenPolicyRefScriptUtxo, executeRefScriptUtxo])
1563
1555
  .validFrom(Number(currentTime) - ONE_SECOND)
1564
- .validTo(
1565
- Number(currentTime) + Number(sysParams.govParams.gBiasTime) - ONE_SECOND,
1566
- )
1556
+ .validTo(Number(currentTime + sysParams.govParams.gBiasTime) - ONE_SECOND)
1567
1557
  .collectFrom([executeUtxo], Data.void())
1568
1558
  .mintAssets(
1569
1559
  mkAssetsOf(fromSystemParamsAsset(sysParams.govParams.upgradeToken), -1n),
@@ -95,6 +95,7 @@ export function attachOracle(
95
95
  .with(
96
96
  { DeferredValidation: { feedValHash: P.select() } },
97
97
  async (feedValHash) => {
98
+ if (priceOracleOref) throw new Error('Cannot pass price oracle oref');
98
99
  if (!pythMessage) throw new Error('Missing Pyth message');
99
100
  if (!pythStateOref) throw new Error('Missing pyth state out ref');
100
101
 
@@ -20,7 +20,7 @@ export const OracleIdxSchema = TSchema.Union(
20
20
  TSchema.Literal('OracleVoid', { flatInUnion: true }),
21
21
  );
22
22
 
23
- export type OracleIdx = typeof OracleIdxSchema;
23
+ export type OracleIdx = typeof OracleIdxSchema.Type;
24
24
 
25
25
  const PriceOracleDatumSchema = TSchema.Struct({
26
26
  price: RationalSchema,
@@ -13,14 +13,15 @@ import {
13
13
  serialiseRobRedeemer,
14
14
  } from './types-new';
15
15
  import { calculateFeeFromRatio } from '../../utils/indigo-helpers';
16
- import { zeroNegatives } from '../../utils/bigint-utils';
16
+ import { BigIntOrd, sum, zeroNegatives } from '../../utils/bigint-utils';
17
17
  import {
18
18
  readonlyArray as RA,
19
19
  array as A,
20
20
  function as F,
21
21
  option as O,
22
+ ord as Ord,
22
23
  } from 'fp-ts';
23
- import { SystemParams } from '../../types/system-params';
24
+ import { CurrencySymbolSP, SystemParams } from '../../types/system-params';
24
25
  import { match, P } from 'ts-pattern';
25
26
  import { getInlineDatumOrThrow } from '../../utils/lucid-utils';
26
27
  import {
@@ -33,19 +34,22 @@ import {
33
34
  } from '@3rd-eye-labs/cardano-offchain-common';
34
35
  import {
35
36
  Rational,
36
- rationalDiv,
37
37
  rationalFloor,
38
38
  rationalFromInt,
39
39
  rationalMul,
40
+ rationalToFloat,
40
41
  } from '../../types/rational';
42
+ import { insertSorted, shuffle } from '../../utils/array-utils';
43
+ import { iassetValueOfCollateral } from '../cdp/helpers';
44
+ import { OracleIdx } from '../price-oracle/types-new';
41
45
 
42
46
  export const MIN_ROB_COLLATERAL_AMT = 3_000_000n;
43
47
 
44
- export function robAmountToSpend(
45
- utxo: UTxO,
46
- datum: RobDatum,
47
- sysParams: SystemParams,
48
- ): bigint {
48
+ /**
49
+ * The amount of collateral asset available in the ROB when buy order. In case of ADA, take
50
+ * into account the min UTXO collateral.
51
+ */
52
+ export function robCollateralAmtToSpend(utxo: UTxO, datum: RobDatum): bigint {
49
53
  return match(datum.orderType)
50
54
  .returnType<bigint>()
51
55
  .with({ BuyIAssetOrder: P.select() }, (content) => {
@@ -57,55 +61,118 @@ export function robAmountToSpend(
57
61
  return assetClassValueOf(utxo.assets, content.collateralAsset);
58
62
  }
59
63
  })
64
+ .otherwise(() => {
65
+ throw new Error('Collateral to spend is relevant only for Buy orders.');
66
+ });
67
+ }
68
+
69
+ /**
70
+ * The amount if iassets available in ROB when sell order.
71
+ */
72
+ export function robIAssetAmtToSpend(
73
+ utxo: UTxO,
74
+ datum: RobDatum,
75
+ iassetCurrencySymbol: CurrencySymbolSP,
76
+ ) {
77
+ return match(datum.orderType)
78
+ .returnType<bigint>()
60
79
  .with({ SellIAssetOrder: P.any }, (_) => {
61
80
  return assetClassValueOf(utxo.assets, {
62
- currencySymbol: fromHex(
63
- sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
64
- ),
81
+ currencySymbol: fromHex(iassetCurrencySymbol.unCurrencySymbol),
65
82
  tokenName: datum.iasset,
66
83
  });
67
84
  })
85
+ .otherwise(() => {
86
+ throw new Error('IAssets to spend is relevant only for Sell orders.');
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Amount to spend from the ROB universal for Buy and sell orders.
92
+ */
93
+ export function robAmtToSpend(
94
+ utxo: UTxO,
95
+ datum: RobDatum,
96
+ iassetCurrencySymbol: CurrencySymbolSP,
97
+ ): bigint {
98
+ return match(datum.orderType)
99
+ .with({ BuyIAssetOrder: P.any }, () => robCollateralAmtToSpend(utxo, datum))
100
+ .with({ SellIAssetOrder: P.any }, () =>
101
+ robIAssetAmtToSpend(utxo, datum, iassetCurrencySymbol),
102
+ )
68
103
  .exhaustive();
69
104
  }
70
105
 
71
- export function isFullyRedeemed(
106
+ export function robBuyOrderSummary(
72
107
  utxo: UTxO,
73
108
  datum: RobDatum,
74
- sysParams: SystemParams,
109
+ oraclePrice: Rational,
110
+ ): {
111
+ /**
112
+ * The amount that can be spent from the ROB.
113
+ */
114
+ redeemableCollateral: bigint;
115
+ /**
116
+ * The amount paid to the ROB when everything redeemed.
117
+ */
118
+ payoutIAsset: bigint;
119
+ } {
120
+ const redeemable = robCollateralAmtToSpend(utxo, datum);
121
+
122
+ const payoutAmt = iassetValueOfCollateral(redeemable, oraclePrice);
123
+
124
+ return {
125
+ redeemableCollateral: redeemable,
126
+ payoutIAsset: payoutAmt,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * In case it's applied to a sell order instead, it will throw an error.
132
+ */
133
+ export function isBuyOrderFullyRedeemed(
134
+ utxo: UTxO,
135
+ datum: RobDatum,
136
+ oraclePrice: Rational,
75
137
  ): boolean {
76
- // TODO: or when the price doesn't allow single token redemption
77
- return robAmountToSpend(utxo, datum, sysParams) === 0n;
138
+ const summary = robBuyOrderSummary(utxo, datum, oraclePrice);
139
+
140
+ return summary.redeemableCollateral <= 0n || summary.payoutIAsset <= 0n;
78
141
  }
79
142
 
80
- // /**
81
- // * Calculate the actually redeemable lovelaces taking into account:
82
- // * - LRP datum
83
- // * - UTXO's value
84
- // * - min redemption
85
- // *
86
- // * This helps to handle incorrectly initialised LRPs, too.
87
- // */
88
- // export function lrpRedeemableLovelacesInclReimb(
89
- // lrp: [UTxO, RobDatum],
90
- // lrpParams: RobParamsSP,
91
- // ): bigint {
92
- // const datum = lrp[1];
93
- // const utxo = lrp[0];
94
-
95
- // let res = 0n;
96
- // // When incorrectly initialised
97
- // if (datum.lovelacesToSpend > lovelacesAmt(utxo.assets)) {
98
- // res = bigintMax(lovelacesAmt(utxo.assets) - MIN_ROB_COLLATERAL_AMT, 0n);
99
- // } else {
100
- // res = datum.lovelacesToSpend;
101
- // }
102
-
103
- // if (res < lrpParams.minRedemptionLovelacesAmt) {
104
- // return 0n;
105
- // }
106
-
107
- // return res;
108
- // }
143
+ /**
144
+ * Use the limit prices to decide fully redeemed.
145
+ */
146
+ export function isFullyRedeemed(
147
+ utxo: UTxO,
148
+ datum: RobDatum,
149
+ iassetCurrencySymbol: CurrencySymbolSP,
150
+ ): boolean {
151
+ return match(datum.orderType)
152
+ .returnType<boolean>()
153
+ .with({ BuyIAssetOrder: P.select() }, (content) =>
154
+ isBuyOrderFullyRedeemed(utxo, datum, content.maxPrice),
155
+ )
156
+
157
+ .with({ SellIAssetOrder: P.select() }, (content) => {
158
+ const iassetToSpend = robIAssetAmtToSpend(
159
+ utxo,
160
+ datum,
161
+ iassetCurrencySymbol,
162
+ );
163
+
164
+ const payoutAmts = content.allowedCollateralAssets.map((c) =>
165
+ rationalFloor(rationalMul(rationalFromInt(iassetToSpend), c[1])),
166
+ );
167
+
168
+ return (
169
+ iassetToSpend <= 0n ||
170
+ // When for every allowed collateral asset the payout would be 0
171
+ payoutAmts.every((amt) => amt <= 0n)
172
+ );
173
+ })
174
+ .exhaustive();
175
+ }
109
176
 
110
177
  /**
111
178
  * Right now we allow multi redemptions when the collateral asset, iasset pair is the same.
@@ -129,13 +196,13 @@ export function buildRedemptionsTx(
129
196
  txOutputsBeforeCount: bigint,
130
197
  collateralAssetRefInputIdx: bigint,
131
198
  iassetRefInputIdx: bigint,
132
- oracleIdx: bigint | null,
199
+ oracleIdx: OracleIdx,
133
200
  ): TxBuilder {
134
201
  return F.pipe(
135
202
  redemptions,
136
203
  A.reduceWithIndex<[UTxO, bigint], TxBuilder>(
137
204
  tx,
138
- (idx, acc, [robUtxo, spendAmt]) => {
205
+ (idx, acc, [robUtxo, payoutAmt]) => {
139
206
  const robDatum = parseRobDatumOrThrow(getInlineDatumOrThrow(robUtxo));
140
207
 
141
208
  if (toHex(robDatum.iasset) !== toHex(iasset)) {
@@ -151,7 +218,7 @@ export function buildRedemptionsTx(
151
218
  throw new Error('Only same collateral asset');
152
219
  }
153
220
 
154
- const payoutIAssetAmt = spendAmt;
221
+ const payoutIAssetAmt = payoutAmt;
155
222
 
156
223
  const reimburstmentIAsset = calculateFeeFromRatio(
157
224
  redemptionReimbursementRatio,
@@ -192,18 +259,16 @@ export function buildRedemptionsTx(
192
259
  }),
193
260
  );
194
261
 
195
- const payoutCollateralAmt = spendAmt;
262
+ const payoutCollateralAmt = payoutAmt;
196
263
 
197
264
  const reimbursementCollateral = calculateFeeFromRatio(
198
265
  redemptionReimbursementRatio,
199
266
  payoutCollateralAmt,
200
267
  );
201
268
 
202
- const redeemedIAssetAmt = rationalFloor(
203
- rationalDiv(
204
- rationalFromInt(payoutCollateralAmt - reimbursementCollateral),
205
- price,
206
- ),
269
+ const redeemedIAssetAmt = iassetValueOfCollateral(
270
+ payoutCollateralAmt - reimbursementCollateral,
271
+ price,
207
272
  );
208
273
 
209
274
  const resultVal = addAssets(
@@ -225,7 +290,9 @@ export function buildRedemptionsTx(
225
290
  .exhaustive();
226
291
 
227
292
  if (lovelacesAmt(robOutputVal) < MIN_ROB_COLLATERAL_AMT) {
228
- throw new Error('ROB was incorrectly initialised.');
293
+ throw new Error(
294
+ 'Redeeming more than available or selected ROB was incorrectly initialised.',
295
+ );
229
296
  }
230
297
 
231
298
  return acc
@@ -239,10 +306,7 @@ export function buildRedemptionsTx(
239
306
  iassetRefInputIdx: iassetRefInputIdx,
240
307
  continuingOutputIdx: txOutputsBeforeCount + BigInt(idx),
241
308
  sellOrderAllowedAssetsIdx: sellOrderAllowedAssetsIdx,
242
- priceOracleIdx:
243
- oracleIdx != null
244
- ? { OracleRefInputIdx: oracleIdx }
245
- : 'OracleVoid',
309
+ priceOracleIdx: oracleIdx,
246
310
  },
247
311
  }),
248
312
  })
@@ -265,141 +329,155 @@ export function buildRedemptionsTx(
265
329
  );
266
330
  }
267
331
 
268
- // /**
269
- // * Given all available LRP UTXOs, calculate total available ADA that can be redeemed (including reimbursement fee).
270
- // * Taking into account ncorrectly initialised LRPs (without base collateral).
271
- // */
272
- // export function calculateTotalAdaForRedemption(
273
- // iasset: Uint8Array<ArrayBufferLike>,
274
- // iassetPrice: OnChainDecimal,
275
- // lrpParams: RobParamsSP,
276
- // allLrps: [UTxO, RobDatum][],
277
- // /**
278
- // * How many LRPs can be redeemed in a single Tx.
279
- // */
280
- // maxLrpsInTx: number,
281
- // ): bigint {
282
- // return F.pipe(
283
- // allLrps,
284
- // A.filterMap(([utxo, datum]) => {
285
- // if (
286
- // toHex(datum.iasset) !== toHex(iasset) ||
287
- // datum.maxPrice.getOnChainInt < iassetPrice.getOnChainInt
288
- // ) {
289
- // return O.none;
290
- // }
291
-
292
- // const lovelacesToSpend = lrpRedeemableLovelacesInclReimb(
293
- // [utxo, datum],
294
- // lrpParams,
295
- // );
296
-
297
- // if (lovelacesToSpend === 0n) {
298
- // return O.none;
299
- // }
300
-
301
- // // Subtract the reimbursement fee here on each iteration to simulate real redemptions.
302
- // return O.some(lovelacesToSpend);
303
- // }),
304
- // // From largest to smallest
305
- // A.sort(Ord.reverse(BigIntOrd)),
306
- // // We can fit only this number of redemptions with CDP open into a single Tx.
307
- // A.takeLeft(maxLrpsInTx),
308
- // sum,
309
- // );
310
- // }
311
-
312
- // export function randomLrpsSubsetSatisfyingTargetLovelaces(
313
- // iasset: Uint8Array<ArrayBufferLike>,
314
- // // Including the reimbursement percentage
315
- // targetLovelacesToSpend: bigint,
316
- // iassetPrice: OnChainDecimal,
317
- // allLrps: [UTxO, RobDatum][],
318
- // lrpParams: RobParamsSP,
319
- // /**
320
- // * How many LRPs can be redeemed in a single Tx.
321
- // */
322
- // maxLrpsInTx: number,
323
- // randomiseFn: (arr: [UTxO, RobDatum][]) => [UTxO, RobDatum][] = shuffle,
324
- // ): [UTxO, RobDatum][] {
325
- // if (targetLovelacesToSpend < lrpParams.minRedemptionLovelacesAmt) {
326
- // throw new Error("Can't redeem less than the minimum.");
327
- // }
328
-
329
- // const shuffled = randomiseFn(
330
- // F.pipe(
331
- // allLrps,
332
- // A.filter(
333
- // ([_, datum]) =>
334
- // toHex(datum.iasset) === toHex(iasset) &&
335
- // datum.maxPrice.getOnChainInt >= iassetPrice.getOnChainInt,
336
- // ),
337
- // ),
338
- // );
339
-
340
- // // Sorted from highest to lowest by lovelaces to spend
341
- // let result: [UTxO, RobDatum][] = [];
342
- // let runningSum = 0n;
343
-
344
- // for (let i = 0; i < shuffled.length; i++) {
345
- // const element = shuffled[i];
346
-
347
- // const lovelacesToSpend = lrpRedeemableLovelacesInclReimb(
348
- // element,
349
- // lrpParams,
350
- // );
351
-
352
- // // Do not add LRPs with smaller lovelacesToSpend than the minRedemption
353
- // // to the random subset.
354
- // if (lovelacesToSpend < lrpParams.minRedemptionLovelacesAmt) {
355
- // continue;
356
- // }
357
-
358
- // // When we can't add a new redemption because otherwise the min redemption
359
- // // wouldn't be satisfied.
360
- // // Try to replace the smallest collected with a following larger one when available.
361
- // if (
362
- // result.length > 0 &&
363
- // targetLovelacesToSpend - runningSum < lrpParams.minRedemptionLovelacesAmt
364
- // ) {
365
- // const last = result[result.length - 1];
366
-
367
- // // Pop the smallest collected when the current is larger.
368
- // if (lrpRedeemableLovelacesInclReimb(last, lrpParams) < lovelacesToSpend) {
369
- // const popped = result.pop()!;
370
- // runningSum -= lrpRedeemableLovelacesInclReimb(popped, lrpParams);
371
- // } else {
372
- // continue;
373
- // }
374
- // }
375
-
376
- // result = insertSorted(
377
- // result,
378
- // element,
379
- // Ord.contramap<bigint, [UTxO, RobDatum]>(
380
- // ([_, dat]) => dat.lovelacesToSpend,
381
- // // From highest to lowest
382
- // )(Ord.reverse(BigIntOrd)),
383
- // );
384
- // runningSum += lovelacesToSpend;
385
-
386
- // // When more items than max allowed, pop the one with smallest value
387
- // if (result.length > maxLrpsInTx) {
388
- // const popped = result.pop()!;
389
- // runningSum -= lrpRedeemableLovelacesInclReimb(popped, lrpParams);
390
- // }
391
-
392
- // if (runningSum >= targetLovelacesToSpend) {
393
- // return result;
394
- // }
395
- // }
396
-
397
- // if (
398
- // targetLovelacesToSpend - runningSum >=
399
- // lrpParams.minRedemptionLovelacesAmt
400
- // ) {
401
- // throw new Error("Couldn't achieve target lovelaces");
402
- // }
403
-
404
- // return result;
405
- // }
332
+ /**
333
+ * Given all available LRP UTXOs, calculate total available collateral that can be redeemed.
334
+ * Taking into account incorrectly initialised LRPs (without base collateral) and max number of ROBs.
335
+ */
336
+ export function calculateTotalCollateralForRedemption(
337
+ iasset: Uint8Array<ArrayBufferLike>,
338
+ collateralAsset: AssetClass,
339
+ iassetPrice: Rational,
340
+ allRobs: [UTxO, RobDatum][],
341
+ /**
342
+ * How many LRPs can be redeemed in a single Tx.
343
+ */
344
+ maxRobsInTx: number,
345
+ ): bigint {
346
+ return F.pipe(
347
+ allRobs,
348
+ A.filterMap(([utxo, datum]) => {
349
+ const isCorrectOrder = match(datum.orderType)
350
+ .returnType<boolean>()
351
+ .with(
352
+ { BuyIAssetOrder: P.select() },
353
+ (content) =>
354
+ isSameAssetClass(content.collateralAsset, collateralAsset) &&
355
+ rationalToFloat(content.maxPrice) >= rationalToFloat(iassetPrice) &&
356
+ !isBuyOrderFullyRedeemed(utxo, datum, iassetPrice),
357
+ )
358
+ .otherwise(() => false);
359
+
360
+ if (toHex(datum.iasset) !== toHex(iasset) || !isCorrectOrder) {
361
+ return O.none;
362
+ }
363
+
364
+ // We constrained in the logic above that the ROB is a buy order and is not yet fully redeemed.
365
+ const collateralToSpend = robCollateralAmtToSpend(utxo, datum);
366
+
367
+ return O.some(collateralToSpend);
368
+ }),
369
+ // From largest to smallest
370
+ A.sort(Ord.reverse(BigIntOrd)),
371
+ // We can fit only this number of redemptions with CDP open into a single Tx.
372
+ A.takeLeft(maxRobsInTx),
373
+ sum,
374
+ );
375
+ }
376
+
377
+ /**
378
+ * Pick random subset from all the ROBs (it does the necessary filtering) satisfying the target collateral to spend.
379
+ * It's relevant for BUY orders only.
380
+ */
381
+ export function randomRobsSubsetSatisfyingTargetCollateral(
382
+ iasset: Uint8Array<ArrayBufferLike>,
383
+ collateralAsset: AssetClass,
384
+ targetCollateralToSpend: bigint,
385
+ iassetPrice: Rational,
386
+ allLrps: [UTxO, RobDatum][],
387
+ /**
388
+ * How many LRPs can be redeemed in a single Tx.
389
+ */
390
+ maxLrpsInTx: number,
391
+ randomiseFn: (arr: [UTxO, RobDatum][]) => [UTxO, RobDatum][] = shuffle,
392
+ ): [UTxO, RobDatum][] {
393
+ if (
394
+ targetCollateralToSpend <= 0n ||
395
+ iassetValueOfCollateral(targetCollateralToSpend, iassetPrice) <= 0n
396
+ ) {
397
+ throw new Error('Must redeem and payout more than 0.');
398
+ }
399
+
400
+ const shuffled = randomiseFn(
401
+ F.pipe(
402
+ allLrps,
403
+ A.filter(
404
+ ([utxo, datum]) =>
405
+ toHex(datum.iasset) === toHex(iasset) &&
406
+ match(datum.orderType)
407
+ .with(
408
+ { BuyIAssetOrder: P.select() },
409
+ (content) =>
410
+ isSameAssetClass(collateralAsset, content.collateralAsset) &&
411
+ rationalToFloat(content.maxPrice) >=
412
+ rationalToFloat(iassetPrice),
413
+ )
414
+ // Only buy order types
415
+ .otherwise(() => false) &&
416
+ !isBuyOrderFullyRedeemed(utxo, datum, iassetPrice),
417
+ ),
418
+ ),
419
+ );
420
+
421
+ // Sorted from highest to lowest by lovelaces to spend
422
+ let result: [UTxO, RobDatum][] = [];
423
+ let runningSum = 0n;
424
+
425
+ for (let i = 0; i < shuffled.length; i++) {
426
+ const element = shuffled[i];
427
+
428
+ const lovelacesToSpend = robCollateralAmtToSpend(element[0], element[1]);
429
+
430
+ const remainingToRedeem = targetCollateralToSpend - runningSum;
431
+ const remainingToPayout = iassetValueOfCollateral(
432
+ remainingToRedeem,
433
+ iassetPrice,
434
+ );
435
+
436
+ // When we can't add a new redemption because otherwise there would be no payout.
437
+ // Try to replace the smallest collected with a following larger one when available.
438
+ if (result.length > 0 && remainingToPayout <= 0n) {
439
+ const last = result[result.length - 1];
440
+
441
+ const lastSummary = robBuyOrderSummary(last[0], last[1], iassetPrice);
442
+
443
+ // Pop the smallest collected when the current is larger.
444
+ if (lastSummary.redeemableCollateral < lovelacesToSpend) {
445
+ result.pop()!;
446
+ runningSum -= lastSummary.redeemableCollateral;
447
+ } else {
448
+ continue;
449
+ }
450
+ }
451
+
452
+ result = insertSorted(
453
+ result,
454
+ element,
455
+ Ord.contramap<bigint, [UTxO, RobDatum]>(
456
+ ([utxo, dat]) => robCollateralAmtToSpend(utxo, dat),
457
+ // From highest to lowest
458
+ )(Ord.reverse(BigIntOrd)),
459
+ );
460
+ runningSum += lovelacesToSpend;
461
+
462
+ // When more items than max allowed, pop the one with smallest value
463
+ if (result.length > maxLrpsInTx) {
464
+ const popped = result.pop()!;
465
+ runningSum -= robCollateralAmtToSpend(popped[0], popped[1]);
466
+ }
467
+
468
+ if (runningSum >= targetCollateralToSpend) {
469
+ return result;
470
+ }
471
+ }
472
+
473
+ const remainingToSpend = targetCollateralToSpend - runningSum;
474
+
475
+ if (
476
+ remainingToSpend > 0n &&
477
+ iassetValueOfCollateral(remainingToSpend, iassetPrice) > 0n
478
+ ) {
479
+ throw new Error("Couldn't achieve target lovelaces");
480
+ }
481
+
482
+ return result;
483
+ }