@avail-project/ca-common 2.2.1 → 2.3.0

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AutoSelectionError = void 0;
3
+ exports.AggregateAggregatorsMode = exports.AutoSelectionError = void 0;
4
4
  exports.aggregateAggregators = aggregateAggregators;
5
5
  exports.autoSelectSourcesV2 = autoSelectSourcesV2;
6
6
  exports.autoSelectSourcesV2ByRecipient = autoSelectSourcesV2ByRecipient;
@@ -22,6 +22,11 @@ class AutoSelectionError extends Error {
22
22
  }
23
23
  exports.AutoSelectionError = AutoSelectionError;
24
24
  const safetyMultiplier = new decimal_js_1.default("1.025");
25
+ var AggregateAggregatorsMode;
26
+ (function (AggregateAggregatorsMode) {
27
+ AggregateAggregatorsMode[AggregateAggregatorsMode["MaximizeOutput"] = 0] = "MaximizeOutput";
28
+ AggregateAggregatorsMode[AggregateAggregatorsMode["MinimizeInput"] = 1] = "MinimizeInput";
29
+ })(AggregateAggregatorsMode || (exports.AggregateAggregatorsMode = AggregateAggregatorsMode = {}));
25
30
  async function aggregateAggregators(requests, aggregators, mode) {
26
31
  const responses = await Promise.all(aggregators.map(async (agg) => {
27
32
  let quotes;
@@ -39,7 +44,7 @@ async function aggregateAggregators(requests, aggregators, mode) {
39
44
  }));
40
45
  const final = new Array(requests.length);
41
46
  switch (mode) {
42
- case 0 /* AggregateAggregatorsMode.MaximizeOutput */: {
47
+ case AggregateAggregatorsMode.MaximizeOutput: {
43
48
  for (let i = 0; i < requests.length; i++) {
44
49
  const best = (0, data_1.maxByBigInt)(responses.map((ra) => ({ quote: ra.quotes[i], aggregator: ra.agg })), (r) => r.quote?.output.amountRaw ?? 0n);
45
50
  if (best != null) {
@@ -54,7 +59,7 @@ async function aggregateAggregators(requests, aggregators, mode) {
54
59
  }
55
60
  break;
56
61
  }
57
- case 1 /* AggregateAggregatorsMode.MinimizeInput */: {
62
+ case AggregateAggregatorsMode.MinimizeInput: {
58
63
  for (let i = 0; i < requests.length; i++) {
59
64
  const best = (0, data_1.minByBigInt)(responses.map((ra) => ({ quote: ra.quotes[i], aggregator: ra.agg })),
60
65
  // Default null quotes to MAX so they never win as the minimum
@@ -231,7 +236,7 @@ async function autoSelectSourcesV2ByRecipient(holdings, outputRequired, aggregat
231
236
  }
232
237
  // Sort by original index to maintain priority
233
238
  processingQueue.sort((a, b) => a.idx - b.idx);
234
- const responses = await aggregateAggregators(fullLiquidationQuotes.map((fq) => fq.req), aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
239
+ const responses = await aggregateAggregators(fullLiquidationQuotes.map((fq) => fq.req), aggregators, AggregateAggregatorsMode.MaximizeOutput);
235
240
  console.debug("AutoSelectSources:Quotes", responses);
236
241
  const final = [];
237
242
  const usedCOTs = [];
@@ -293,7 +298,7 @@ async function autoSelectSourcesV2ByRecipient(holdings, outputRequired, aggregat
293
298
  seriousness: iface_1.QuoteSeriousness.SERIOUS,
294
299
  inputAmount: (0, data_1.convertDecimalToBigInt)(expectedInput),
295
300
  },
296
- ], aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
301
+ ], aggregators, AggregateAggregatorsMode.MaximizeOutput);
297
302
  if (adequateQuoteResult.length !== 1) {
298
303
  throw new AutoSelectionError("Unexpected response length from aggregateAggregators");
299
304
  }
@@ -371,7 +376,7 @@ async function determineDestinationSwaps(userAddress, requirement, aggregators,
371
376
  inputAmount: requirement.amountRaw,
372
377
  seriousness: iface_1.QuoteSeriousness.PRICE_SURVEY,
373
378
  };
374
- const fullLiquidationResult = await aggregateAggregators([fullLiquidationQR], aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
379
+ const fullLiquidationResult = await aggregateAggregators([fullLiquidationQR], aggregators, AggregateAggregatorsMode.MaximizeOutput);
375
380
  if (fullLiquidationResult.length !== 1) {
376
381
  throw new AutoSelectionError("Unexpected response length from aggregateAggregators");
377
382
  }
@@ -396,7 +401,7 @@ async function determineDestinationSwaps(userAddress, requirement, aggregators,
396
401
  inputAmount: (0, data_1.convertDecimalToBigInt)(curAmount),
397
402
  seriousness: iface_1.QuoteSeriousness.SERIOUS,
398
403
  },
399
- ], aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
404
+ ], aggregators, AggregateAggregatorsMode.MaximizeOutput);
400
405
  if (buyQuoteResult.length !== 1) {
401
406
  throw new AutoSelectionError("Unexpected response length from aggregateAggregators");
402
407
  }
@@ -473,7 +478,7 @@ async function liquidateInputHoldingsByRecipient(holdings, aggregators, commonCu
473
478
  });
474
479
  }
475
480
  }
476
- const responses = await aggregateAggregators(fullLiquidationQuotes.map((fq) => fq.req), aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
481
+ const responses = await aggregateAggregators(fullLiquidationQuotes.map((fq) => fq.req), aggregators, AggregateAggregatorsMode.MaximizeOutput);
477
482
  console.debug("XCS | LIH | Responses:", responses);
478
483
  const quotes = [];
479
484
  for (const [i, response] of responses.entries()) {
@@ -512,7 +517,7 @@ async function destinationSwapWithExactIn(userAddress, omniChainID, inputAmount,
512
517
  inputAmount: inputAmount,
513
518
  seriousness: iface_1.QuoteSeriousness.SERIOUS,
514
519
  },
515
- ], aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
520
+ ], aggregators, AggregateAggregatorsMode.MaximizeOutput);
516
521
  if (fullLiquidationResult.length !== 1) {
517
522
  throw new AutoSelectionError("Unexpected response length from aggregateAggregators");
518
523
  }
@@ -119,7 +119,7 @@ class BebopAggregator {
119
119
  amountRaw: BigInt(sellT.amount),
120
120
  contractAddress: inputTokenAddr,
121
121
  decimals: sellT.decimals,
122
- value: decimal_js_1.default.mul(inputAmountInDecimal, sellT.priceUsd).toNumber(),
122
+ value: decimal_js_1.default.mul(inputAmountInDecimal, sellT.priceUsd ?? 0).toNumber(),
123
123
  symbol: sellT.symbol,
124
124
  },
125
125
  output: {
@@ -127,7 +127,7 @@ class BebopAggregator {
127
127
  amountRaw: BigInt(buyT.minimumAmount),
128
128
  contractAddress: outputTokenAddr,
129
129
  decimals: buyT.decimals,
130
- value: decimal_js_1.default.mul(buyT.priceUsd, outputAmountInDecimal).toNumber(),
130
+ value: decimal_js_1.default.mul(outputAmountInDecimal, buyT.priceUsd ?? 0).toNumber(),
131
131
  symbol: buyT.symbol,
132
132
  },
133
133
  txData: {
@@ -6,3 +6,4 @@ tslib_1.__exportStar(require("./lifi-agg"), exports);
6
6
  tslib_1.__exportStar(require("./bebop-agg"), exports);
7
7
  tslib_1.__exportStar(require("./fibrous-agg"), exports);
8
8
  tslib_1.__exportStar(require("./autochoice"), exports);
9
+ tslib_1.__exportStar(require("./selectsources"), exports);
@@ -23,11 +23,31 @@ const ALLOWED_CHAINS = new Set([
23
23
  ]);
24
24
  class LiFiAggregator {
25
25
  static BASE_URL_V1 = "https://li.quest/v1";
26
+ // Params that don't depend on chain — spread into every quote request.
27
+ // `denyExchanges` is built per-call via `denyExchangesFor(chainId)`.
26
28
  static COMMON_OPTIONS = {
27
- denyExchanges: "openocean",
28
29
  slippage: "0.01",
29
30
  skipSimulation: true,
30
31
  };
32
+ // Tools denied on every chain.
33
+ static GLOBAL_DENY = ["openocean"];
34
+ // Tools denied only on specific chains. Add an entry here when a tool quotes
35
+ // well on most chains but mis-quotes on a specific one — global-denying it
36
+ // would needlessly disable healthy routes elsewhere.
37
+ //
38
+ // 999 (HyperEVM): fly/hyperflow/liquidswap all share the same on-chain entry
39
+ // (0x0a0758d937d1059c356d4714e57f5df0239bce1a) and systematically over-quote
40
+ // native HYPE -> USDC routes by 5-11% (LI.FI /quote toAmountMin vs on-chain
41
+ // delivery, observed via InsufficientAmountOut(0xe52970aa) failures in
42
+ // HyperEVM Safe-mode swaps). They quote within ±0.2% of other tools on
43
+ // Ethereum/Base/Arbitrum/Polygon/Optimism, so the deny is chain-local.
44
+ static PER_CHAIN_DENY = {
45
+ 999: ["fly", "hyperflow", "liquidswap"],
46
+ };
47
+ denyExchangesFor(chainId) {
48
+ const perChain = LiFiAggregator.PER_CHAIN_DENY[chainId] ?? [];
49
+ return [...LiFiAggregator.GLOBAL_DENY, ...perChain].join(",");
50
+ }
31
51
  axios;
32
52
  constructor(apiKey) {
33
53
  this.axios = axios_1.default.create({
@@ -68,6 +88,7 @@ class LiFiAggregator {
68
88
  toAddress: receiverAddrHex,
69
89
  fromAmount: r.inputAmount.toString(),
70
90
  ...LiFiAggregator.COMMON_OPTIONS,
91
+ denyExchanges: this.denyExchangesFor(Number(r.chain.chainID)),
71
92
  },
72
93
  });
73
94
  break;
@@ -85,6 +106,7 @@ class LiFiAggregator {
85
106
  toAddress: receiverAddrHex,
86
107
  toAmount: r.outputAmount.toString(),
87
108
  ...LiFiAggregator.COMMON_OPTIONS,
109
+ denyExchanges: this.denyExchangesFor(Number(r.chain.chainID)),
88
110
  },
89
111
  });
90
112
  break;
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectSources = selectSources;
4
+ const tslib_1 = require("tslib");
5
+ const decimal_js_1 = tslib_1.__importDefault(require("decimal.js"));
6
+ const iface_1 = require("./iface");
7
+ const autochoice_1 = require("./autochoice");
8
+ const data_1 = require("../data");
9
+ const safetyMultiplier = new decimal_js_1.default("1.025");
10
+ async function selectSources(args) {
11
+ const { sources, outputRequired, aggregators, commonCurrencyID = data_1.CurrencyID.USDC, prefixHeadroom = new decimal_js_1.default("1.25"), } = args;
12
+ console.debug("XCS | SS:", {
13
+ sources,
14
+ outputRequired: outputRequired.toFixed(),
15
+ prefixHeadroom: prefixHeadroom.toFixed(),
16
+ });
17
+ const cotList = [];
18
+ const nonCOTQuotes = [];
19
+ for (const [idx, src] of sources.entries()) {
20
+ const chain = data_1.ChaindataMap.get(src.chainID);
21
+ if (chain == null) {
22
+ throw new autochoice_1.AutoSelectionError("Chain not found");
23
+ }
24
+ const cot = chain.Currencies.find((c) => c.currencyID === commonCurrencyID);
25
+ if (cot == null) {
26
+ console.debug("XCS | SS | Skipping source — no COT on chain", { chain });
27
+ continue;
28
+ }
29
+ const originalHolding = {
30
+ chainID: src.chainID,
31
+ tokenAddress: src.tokenAddress,
32
+ amountRaw: src.amountRaw,
33
+ };
34
+ if (Buffer.compare(src.tokenAddress, cot.tokenAddress) === 0) {
35
+ const normalizedAmount = new decimal_js_1.default(src.amountRaw.toString()).div(decimal_js_1.default.pow(10, cot.decimals));
36
+ cotList.push({
37
+ amount: normalizedAmount,
38
+ idx,
39
+ chainID: src.chainID,
40
+ currency: cot,
41
+ originalHolding,
42
+ });
43
+ }
44
+ else {
45
+ nonCOTQuotes.push({
46
+ req: {
47
+ userAddress: src.takerAddress,
48
+ receiverAddress: src.receiverAddress,
49
+ type: iface_1.QuoteType.EXACT_IN,
50
+ chain: chain.ChainID,
51
+ inputToken: src.tokenAddress,
52
+ inputAmount: src.amountRaw,
53
+ outputToken: cot.tokenAddress,
54
+ seriousness: iface_1.QuoteSeriousness.PRICE_SURVEY,
55
+ },
56
+ originalHolding,
57
+ cur: cot,
58
+ idx,
59
+ value: src.value,
60
+ });
61
+ }
62
+ }
63
+ // Continuous-COT fast path — covers outputRequired without touching any aggregator.
64
+ if (cotList.length > 0 && cotList[0].idx === 0) {
65
+ let continuousAmount = new decimal_js_1.default(0);
66
+ let continuousCount = 0;
67
+ for (const cot of cotList) {
68
+ if (cot.idx !== continuousCount)
69
+ break;
70
+ continuousAmount = continuousAmount.add(cot.amount);
71
+ continuousCount++;
72
+ if (continuousAmount.gte(outputRequired)) {
73
+ console.log("XCS | SS | Continuous COTs cover requirement, no quotes needed");
74
+ const usedCOTs = [];
75
+ let r = outputRequired;
76
+ for (let i = 0; i < continuousCount; i++) {
77
+ const c = cotList[i];
78
+ const amountToUse = decimal_js_1.default.min(r, c.amount);
79
+ usedCOTs.push({
80
+ originalHolding: c.originalHolding,
81
+ amountUsed: amountToUse,
82
+ idx: c.idx,
83
+ cur: c.currency,
84
+ });
85
+ r = r.minus(amountToUse);
86
+ if (r.lte(0))
87
+ break;
88
+ }
89
+ return { quoteResponses: [], usedCOTs };
90
+ }
91
+ }
92
+ }
93
+ const queue = [];
94
+ for (const c of cotList) {
95
+ queue.push({ idx: c.idx, isCOT: true, cotData: c });
96
+ }
97
+ for (const q of nonCOTQuotes) {
98
+ queue.push({ idx: q.idx, isCOT: false, quoteData: q });
99
+ }
100
+ queue.sort((a, b) => a.idx - b.idx);
101
+ // Smallest priority-ordered prefix whose cumulative value covers
102
+ // `outputRequired × prefixHeadroom`. Quote only that prefix initially; the batch
103
+ // is extended below if realised aggregator output under-delivers.
104
+ const target = outputRequired.mul(prefixHeadroom);
105
+ let cumulative = new decimal_js_1.default(0);
106
+ let prefixCutoffIdx = null;
107
+ for (const item of queue) {
108
+ if (item.isCOT) {
109
+ cumulative = cumulative.add(item.cotData.amount);
110
+ }
111
+ else {
112
+ cumulative = cumulative.add(item.quoteData.value);
113
+ }
114
+ if (cumulative.gte(target)) {
115
+ prefixCutoffIdx = item.idx;
116
+ break;
117
+ }
118
+ }
119
+ console.debug("XCS | SS | Prefix selected", {
120
+ prefixCutoffIdx,
121
+ cumulative: cumulative.toFixed(),
122
+ target: target.toFixed(),
123
+ });
124
+ const responseByIdx = new Map();
125
+ const quoteAndStore = async (batch) => {
126
+ if (batch.length === 0)
127
+ return;
128
+ const r = await (0, autochoice_1.aggregateAggregators)(batch.map((b) => b.req), aggregators, autochoice_1.AggregateAggregatorsMode.MaximizeOutput);
129
+ for (let i = 0; i < batch.length; i++) {
130
+ responseByIdx.set(batch[i].idx, r[i]);
131
+ }
132
+ };
133
+ const initialBatch = prefixCutoffIdx == null
134
+ ? nonCOTQuotes
135
+ : nonCOTQuotes.filter((q) => q.idx <= prefixCutoffIdx);
136
+ console.debug("XCS | SS | Initial quote batch", {
137
+ initialCount: initialBatch.length,
138
+ totalNonCOT: nonCOTQuotes.length,
139
+ });
140
+ await quoteAndStore(initialBatch);
141
+ const final = [];
142
+ const usedCOTs = [];
143
+ let remainder = outputRequired;
144
+ for (const item of queue) {
145
+ if (remainder.lte(0))
146
+ break;
147
+ if (item.isCOT) {
148
+ const { cotData } = item;
149
+ const amountToUse = decimal_js_1.default.min(remainder, cotData.amount);
150
+ usedCOTs.push({
151
+ originalHolding: cotData.originalHolding,
152
+ amountUsed: amountToUse,
153
+ idx: cotData.idx,
154
+ cur: cotData.currency,
155
+ });
156
+ remainder = remainder.minus(amountToUse);
157
+ console.debug("XCS | SS | cot", {
158
+ idx: cotData.idx,
159
+ amountToUse: amountToUse.toFixed(),
160
+ remainder: remainder.toFixed(),
161
+ });
162
+ continue;
163
+ }
164
+ const { quoteData } = item;
165
+ let lookup = responseByIdx.get(quoteData.idx);
166
+ if (lookup == null) {
167
+ // Prefix under-delivered: quote every non-COT we haven't yet and continue.
168
+ const unquoted = nonCOTQuotes.filter((q) => !responseByIdx.has(q.idx));
169
+ console.log("XCS | SS | Prefix under-delivered, extending batch", {
170
+ remaining: unquoted.length,
171
+ });
172
+ await quoteAndStore(unquoted);
173
+ lookup = responseByIdx.get(quoteData.idx);
174
+ }
175
+ if (lookup == null)
176
+ continue;
177
+ const { quote: resp, aggregator } = lookup;
178
+ if (resp == null)
179
+ continue;
180
+ console.debug("XCS | SS | quote-eval", {
181
+ idx: quoteData.idx,
182
+ remainder: remainder.toFixed(),
183
+ input: resp.input,
184
+ output: resp.output,
185
+ });
186
+ const divisor = decimal_js_1.default.pow(10, quoteData.cur.decimals);
187
+ const oamD = new decimal_js_1.default(resp.output.amount);
188
+ if (oamD.gt(remainder)) {
189
+ const indicativePrice = decimal_js_1.default.div(resp.input.amountRaw.toString(), resp.output.amountRaw.toString());
190
+ const userBal = new decimal_js_1.default(quoteData.originalHolding.amountRaw.toString());
191
+ let expectedInput = decimal_js_1.default.min(remainder.mul(divisor).mul(indicativePrice).mul(safetyMultiplier), userBal);
192
+ let attempts = 0;
193
+ while (true) {
194
+ if (++attempts > 10) {
195
+ throw new autochoice_1.AutoSelectionError("Partial quote did not converge");
196
+ }
197
+ console.debug("XCS | SS | partial_quote_loop", {
198
+ indicativePrice: indicativePrice.toFixed(),
199
+ expectedInput: expectedInput.toFixed(),
200
+ userBal: userBal.toFixed(),
201
+ remainder: remainder.toFixed(),
202
+ });
203
+ const adequate = await (0, autochoice_1.aggregateAggregators)([
204
+ {
205
+ ...quoteData.req,
206
+ seriousness: iface_1.QuoteSeriousness.SERIOUS,
207
+ inputAmount: (0, data_1.convertDecimalToBigInt)(expectedInput),
208
+ },
209
+ ], aggregators, autochoice_1.AggregateAggregatorsMode.MaximizeOutput);
210
+ if (adequate.length !== 1) {
211
+ throw new autochoice_1.AutoSelectionError("Unexpected response length from aggregateAggregators");
212
+ }
213
+ const aq = adequate[0];
214
+ if (aq.quote == null) {
215
+ throw new autochoice_1.AutoSelectionError("Couldn't get buy quote");
216
+ }
217
+ const oam2D = new decimal_js_1.default(aq.quote.output.amount);
218
+ if (oam2D.gte(remainder)) {
219
+ final.push({
220
+ quote: aq.quote,
221
+ aggregator: aq.aggregator,
222
+ holding: quoteData.originalHolding,
223
+ chainID: Number(quoteData.req.chain.chainID),
224
+ });
225
+ remainder = remainder.minus(oam2D);
226
+ break;
227
+ }
228
+ else if (expectedInput.eq(userBal)) {
229
+ throw new autochoice_1.AutoSelectionError("Holding was supposedly enough to meet the full requirement but ceased to be so subsequently");
230
+ }
231
+ else {
232
+ expectedInput = decimal_js_1.default.min(expectedInput.mul(safetyMultiplier), userBal);
233
+ }
234
+ }
235
+ }
236
+ else {
237
+ final.push({
238
+ quote: resp,
239
+ holding: quoteData.originalHolding,
240
+ aggregator,
241
+ chainID: Number(quoteData.req.chain.chainID),
242
+ });
243
+ remainder = remainder.minus(resp.output.amount);
244
+ console.debug("XCS | SS | full_quote", {
245
+ idx: quoteData.idx,
246
+ remainder: remainder.toFixed(),
247
+ });
248
+ }
249
+ }
250
+ if (remainder.gt(0)) {
251
+ throw new autochoice_1.AutoSelectionError("NOT_ENOUGH_SWAP_FOR_REQUIREMENT");
252
+ }
253
+ console.log("XCS | SS | final_quotes", { quotes: final, cots: usedCOTs });
254
+ return { quoteResponses: final, usedCOTs };
255
+ }
@@ -6,6 +6,11 @@ import { ChaindataMap, convertBigIntToDecimal, convertDecimalToBigInt, CurrencyI
6
6
  export class AutoSelectionError extends Error {
7
7
  }
8
8
  const safetyMultiplier = new Decimal("1.025");
9
+ export var AggregateAggregatorsMode;
10
+ (function (AggregateAggregatorsMode) {
11
+ AggregateAggregatorsMode[AggregateAggregatorsMode["MaximizeOutput"] = 0] = "MaximizeOutput";
12
+ AggregateAggregatorsMode[AggregateAggregatorsMode["MinimizeInput"] = 1] = "MinimizeInput";
13
+ })(AggregateAggregatorsMode || (AggregateAggregatorsMode = {}));
9
14
  export async function aggregateAggregators(requests, aggregators, mode) {
10
15
  const responses = await Promise.all(aggregators.map(async (agg) => {
11
16
  let quotes;
@@ -23,7 +28,7 @@ export async function aggregateAggregators(requests, aggregators, mode) {
23
28
  }));
24
29
  const final = new Array(requests.length);
25
30
  switch (mode) {
26
- case 0 /* AggregateAggregatorsMode.MaximizeOutput */: {
31
+ case AggregateAggregatorsMode.MaximizeOutput: {
27
32
  for (let i = 0; i < requests.length; i++) {
28
33
  const best = maxByBigInt(responses.map((ra) => ({ quote: ra.quotes[i], aggregator: ra.agg })), (r) => r.quote?.output.amountRaw ?? 0n);
29
34
  if (best != null) {
@@ -38,7 +43,7 @@ export async function aggregateAggregators(requests, aggregators, mode) {
38
43
  }
39
44
  break;
40
45
  }
41
- case 1 /* AggregateAggregatorsMode.MinimizeInput */: {
46
+ case AggregateAggregatorsMode.MinimizeInput: {
42
47
  for (let i = 0; i < requests.length; i++) {
43
48
  const best = minByBigInt(responses.map((ra) => ({ quote: ra.quotes[i], aggregator: ra.agg })),
44
49
  // Default null quotes to MAX so they never win as the minimum
@@ -215,7 +220,7 @@ export async function autoSelectSourcesV2ByRecipient(holdings, outputRequired, a
215
220
  }
216
221
  // Sort by original index to maintain priority
217
222
  processingQueue.sort((a, b) => a.idx - b.idx);
218
- const responses = await aggregateAggregators(fullLiquidationQuotes.map((fq) => fq.req), aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
223
+ const responses = await aggregateAggregators(fullLiquidationQuotes.map((fq) => fq.req), aggregators, AggregateAggregatorsMode.MaximizeOutput);
219
224
  console.debug("AutoSelectSources:Quotes", responses);
220
225
  const final = [];
221
226
  const usedCOTs = [];
@@ -277,7 +282,7 @@ export async function autoSelectSourcesV2ByRecipient(holdings, outputRequired, a
277
282
  seriousness: QuoteSeriousness.SERIOUS,
278
283
  inputAmount: convertDecimalToBigInt(expectedInput),
279
284
  },
280
- ], aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
285
+ ], aggregators, AggregateAggregatorsMode.MaximizeOutput);
281
286
  if (adequateQuoteResult.length !== 1) {
282
287
  throw new AutoSelectionError("Unexpected response length from aggregateAggregators");
283
288
  }
@@ -355,7 +360,7 @@ export async function determineDestinationSwaps(userAddress, requirement, aggreg
355
360
  inputAmount: requirement.amountRaw,
356
361
  seriousness: QuoteSeriousness.PRICE_SURVEY,
357
362
  };
358
- const fullLiquidationResult = await aggregateAggregators([fullLiquidationQR], aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
363
+ const fullLiquidationResult = await aggregateAggregators([fullLiquidationQR], aggregators, AggregateAggregatorsMode.MaximizeOutput);
359
364
  if (fullLiquidationResult.length !== 1) {
360
365
  throw new AutoSelectionError("Unexpected response length from aggregateAggregators");
361
366
  }
@@ -380,7 +385,7 @@ export async function determineDestinationSwaps(userAddress, requirement, aggreg
380
385
  inputAmount: convertDecimalToBigInt(curAmount),
381
386
  seriousness: QuoteSeriousness.SERIOUS,
382
387
  },
383
- ], aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
388
+ ], aggregators, AggregateAggregatorsMode.MaximizeOutput);
384
389
  if (buyQuoteResult.length !== 1) {
385
390
  throw new AutoSelectionError("Unexpected response length from aggregateAggregators");
386
391
  }
@@ -457,7 +462,7 @@ export async function liquidateInputHoldingsByRecipient(holdings, aggregators, c
457
462
  });
458
463
  }
459
464
  }
460
- const responses = await aggregateAggregators(fullLiquidationQuotes.map((fq) => fq.req), aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
465
+ const responses = await aggregateAggregators(fullLiquidationQuotes.map((fq) => fq.req), aggregators, AggregateAggregatorsMode.MaximizeOutput);
461
466
  console.debug("XCS | LIH | Responses:", responses);
462
467
  const quotes = [];
463
468
  for (const [i, response] of responses.entries()) {
@@ -496,7 +501,7 @@ export async function destinationSwapWithExactIn(userAddress, omniChainID, input
496
501
  inputAmount: inputAmount,
497
502
  seriousness: QuoteSeriousness.SERIOUS,
498
503
  },
499
- ], aggregators, 0 /* AggregateAggregatorsMode.MaximizeOutput */);
504
+ ], aggregators, AggregateAggregatorsMode.MaximizeOutput);
500
505
  if (fullLiquidationResult.length !== 1) {
501
506
  throw new AutoSelectionError("Unexpected response length from aggregateAggregators");
502
507
  }
@@ -115,7 +115,7 @@ export class BebopAggregator {
115
115
  amountRaw: BigInt(sellT.amount),
116
116
  contractAddress: inputTokenAddr,
117
117
  decimals: sellT.decimals,
118
- value: Decimal.mul(inputAmountInDecimal, sellT.priceUsd).toNumber(),
118
+ value: Decimal.mul(inputAmountInDecimal, sellT.priceUsd ?? 0).toNumber(),
119
119
  symbol: sellT.symbol,
120
120
  },
121
121
  output: {
@@ -123,7 +123,7 @@ export class BebopAggregator {
123
123
  amountRaw: BigInt(buyT.minimumAmount),
124
124
  contractAddress: outputTokenAddr,
125
125
  decimals: buyT.decimals,
126
- value: Decimal.mul(buyT.priceUsd, outputAmountInDecimal).toNumber(),
126
+ value: Decimal.mul(outputAmountInDecimal, buyT.priceUsd ?? 0).toNumber(),
127
127
  symbol: buyT.symbol,
128
128
  },
129
129
  txData: {
@@ -3,3 +3,4 @@ export * from './lifi-agg';
3
3
  export * from './bebop-agg';
4
4
  export * from './fibrous-agg';
5
5
  export * from './autochoice';
6
+ export * from './selectsources';
@@ -19,11 +19,31 @@ const ALLOWED_CHAINS = new Set([
19
19
  ]);
20
20
  export class LiFiAggregator {
21
21
  static BASE_URL_V1 = "https://li.quest/v1";
22
+ // Params that don't depend on chain — spread into every quote request.
23
+ // `denyExchanges` is built per-call via `denyExchangesFor(chainId)`.
22
24
  static COMMON_OPTIONS = {
23
- denyExchanges: "openocean",
24
25
  slippage: "0.01",
25
26
  skipSimulation: true,
26
27
  };
28
+ // Tools denied on every chain.
29
+ static GLOBAL_DENY = ["openocean"];
30
+ // Tools denied only on specific chains. Add an entry here when a tool quotes
31
+ // well on most chains but mis-quotes on a specific one — global-denying it
32
+ // would needlessly disable healthy routes elsewhere.
33
+ //
34
+ // 999 (HyperEVM): fly/hyperflow/liquidswap all share the same on-chain entry
35
+ // (0x0a0758d937d1059c356d4714e57f5df0239bce1a) and systematically over-quote
36
+ // native HYPE -> USDC routes by 5-11% (LI.FI /quote toAmountMin vs on-chain
37
+ // delivery, observed via InsufficientAmountOut(0xe52970aa) failures in
38
+ // HyperEVM Safe-mode swaps). They quote within ±0.2% of other tools on
39
+ // Ethereum/Base/Arbitrum/Polygon/Optimism, so the deny is chain-local.
40
+ static PER_CHAIN_DENY = {
41
+ 999: ["fly", "hyperflow", "liquidswap"],
42
+ };
43
+ denyExchangesFor(chainId) {
44
+ const perChain = LiFiAggregator.PER_CHAIN_DENY[chainId] ?? [];
45
+ return [...LiFiAggregator.GLOBAL_DENY, ...perChain].join(",");
46
+ }
27
47
  axios;
28
48
  constructor(apiKey) {
29
49
  this.axios = axios.create({
@@ -64,6 +84,7 @@ export class LiFiAggregator {
64
84
  toAddress: receiverAddrHex,
65
85
  fromAmount: r.inputAmount.toString(),
66
86
  ...LiFiAggregator.COMMON_OPTIONS,
87
+ denyExchanges: this.denyExchangesFor(Number(r.chain.chainID)),
67
88
  },
68
89
  });
69
90
  break;
@@ -81,6 +102,7 @@ export class LiFiAggregator {
81
102
  toAddress: receiverAddrHex,
82
103
  toAmount: r.outputAmount.toString(),
83
104
  ...LiFiAggregator.COMMON_OPTIONS,
105
+ denyExchanges: this.denyExchangesFor(Number(r.chain.chainID)),
84
106
  },
85
107
  });
86
108
  break;
@@ -0,0 +1,251 @@
1
+ import Decimal from "decimal.js";
2
+ import { QuoteSeriousness, QuoteType, } from "./iface";
3
+ import { AggregateAggregatorsMode, aggregateAggregators, AutoSelectionError, } from "./autochoice";
4
+ import { ChaindataMap, convertDecimalToBigInt, CurrencyID, } from "../data";
5
+ const safetyMultiplier = new Decimal("1.025");
6
+ export async function selectSources(args) {
7
+ const { sources, outputRequired, aggregators, commonCurrencyID = CurrencyID.USDC, prefixHeadroom = new Decimal("1.25"), } = args;
8
+ console.debug("XCS | SS:", {
9
+ sources,
10
+ outputRequired: outputRequired.toFixed(),
11
+ prefixHeadroom: prefixHeadroom.toFixed(),
12
+ });
13
+ const cotList = [];
14
+ const nonCOTQuotes = [];
15
+ for (const [idx, src] of sources.entries()) {
16
+ const chain = ChaindataMap.get(src.chainID);
17
+ if (chain == null) {
18
+ throw new AutoSelectionError("Chain not found");
19
+ }
20
+ const cot = chain.Currencies.find((c) => c.currencyID === commonCurrencyID);
21
+ if (cot == null) {
22
+ console.debug("XCS | SS | Skipping source — no COT on chain", { chain });
23
+ continue;
24
+ }
25
+ const originalHolding = {
26
+ chainID: src.chainID,
27
+ tokenAddress: src.tokenAddress,
28
+ amountRaw: src.amountRaw,
29
+ };
30
+ if (Buffer.compare(src.tokenAddress, cot.tokenAddress) === 0) {
31
+ const normalizedAmount = new Decimal(src.amountRaw.toString()).div(Decimal.pow(10, cot.decimals));
32
+ cotList.push({
33
+ amount: normalizedAmount,
34
+ idx,
35
+ chainID: src.chainID,
36
+ currency: cot,
37
+ originalHolding,
38
+ });
39
+ }
40
+ else {
41
+ nonCOTQuotes.push({
42
+ req: {
43
+ userAddress: src.takerAddress,
44
+ receiverAddress: src.receiverAddress,
45
+ type: QuoteType.EXACT_IN,
46
+ chain: chain.ChainID,
47
+ inputToken: src.tokenAddress,
48
+ inputAmount: src.amountRaw,
49
+ outputToken: cot.tokenAddress,
50
+ seriousness: QuoteSeriousness.PRICE_SURVEY,
51
+ },
52
+ originalHolding,
53
+ cur: cot,
54
+ idx,
55
+ value: src.value,
56
+ });
57
+ }
58
+ }
59
+ // Continuous-COT fast path — covers outputRequired without touching any aggregator.
60
+ if (cotList.length > 0 && cotList[0].idx === 0) {
61
+ let continuousAmount = new Decimal(0);
62
+ let continuousCount = 0;
63
+ for (const cot of cotList) {
64
+ if (cot.idx !== continuousCount)
65
+ break;
66
+ continuousAmount = continuousAmount.add(cot.amount);
67
+ continuousCount++;
68
+ if (continuousAmount.gte(outputRequired)) {
69
+ console.log("XCS | SS | Continuous COTs cover requirement, no quotes needed");
70
+ const usedCOTs = [];
71
+ let r = outputRequired;
72
+ for (let i = 0; i < continuousCount; i++) {
73
+ const c = cotList[i];
74
+ const amountToUse = Decimal.min(r, c.amount);
75
+ usedCOTs.push({
76
+ originalHolding: c.originalHolding,
77
+ amountUsed: amountToUse,
78
+ idx: c.idx,
79
+ cur: c.currency,
80
+ });
81
+ r = r.minus(amountToUse);
82
+ if (r.lte(0))
83
+ break;
84
+ }
85
+ return { quoteResponses: [], usedCOTs };
86
+ }
87
+ }
88
+ }
89
+ const queue = [];
90
+ for (const c of cotList) {
91
+ queue.push({ idx: c.idx, isCOT: true, cotData: c });
92
+ }
93
+ for (const q of nonCOTQuotes) {
94
+ queue.push({ idx: q.idx, isCOT: false, quoteData: q });
95
+ }
96
+ queue.sort((a, b) => a.idx - b.idx);
97
+ // Smallest priority-ordered prefix whose cumulative value covers
98
+ // `outputRequired × prefixHeadroom`. Quote only that prefix initially; the batch
99
+ // is extended below if realised aggregator output under-delivers.
100
+ const target = outputRequired.mul(prefixHeadroom);
101
+ let cumulative = new Decimal(0);
102
+ let prefixCutoffIdx = null;
103
+ for (const item of queue) {
104
+ if (item.isCOT) {
105
+ cumulative = cumulative.add(item.cotData.amount);
106
+ }
107
+ else {
108
+ cumulative = cumulative.add(item.quoteData.value);
109
+ }
110
+ if (cumulative.gte(target)) {
111
+ prefixCutoffIdx = item.idx;
112
+ break;
113
+ }
114
+ }
115
+ console.debug("XCS | SS | Prefix selected", {
116
+ prefixCutoffIdx,
117
+ cumulative: cumulative.toFixed(),
118
+ target: target.toFixed(),
119
+ });
120
+ const responseByIdx = new Map();
121
+ const quoteAndStore = async (batch) => {
122
+ if (batch.length === 0)
123
+ return;
124
+ const r = await aggregateAggregators(batch.map((b) => b.req), aggregators, AggregateAggregatorsMode.MaximizeOutput);
125
+ for (let i = 0; i < batch.length; i++) {
126
+ responseByIdx.set(batch[i].idx, r[i]);
127
+ }
128
+ };
129
+ const initialBatch = prefixCutoffIdx == null
130
+ ? nonCOTQuotes
131
+ : nonCOTQuotes.filter((q) => q.idx <= prefixCutoffIdx);
132
+ console.debug("XCS | SS | Initial quote batch", {
133
+ initialCount: initialBatch.length,
134
+ totalNonCOT: nonCOTQuotes.length,
135
+ });
136
+ await quoteAndStore(initialBatch);
137
+ const final = [];
138
+ const usedCOTs = [];
139
+ let remainder = outputRequired;
140
+ for (const item of queue) {
141
+ if (remainder.lte(0))
142
+ break;
143
+ if (item.isCOT) {
144
+ const { cotData } = item;
145
+ const amountToUse = Decimal.min(remainder, cotData.amount);
146
+ usedCOTs.push({
147
+ originalHolding: cotData.originalHolding,
148
+ amountUsed: amountToUse,
149
+ idx: cotData.idx,
150
+ cur: cotData.currency,
151
+ });
152
+ remainder = remainder.minus(amountToUse);
153
+ console.debug("XCS | SS | cot", {
154
+ idx: cotData.idx,
155
+ amountToUse: amountToUse.toFixed(),
156
+ remainder: remainder.toFixed(),
157
+ });
158
+ continue;
159
+ }
160
+ const { quoteData } = item;
161
+ let lookup = responseByIdx.get(quoteData.idx);
162
+ if (lookup == null) {
163
+ // Prefix under-delivered: quote every non-COT we haven't yet and continue.
164
+ const unquoted = nonCOTQuotes.filter((q) => !responseByIdx.has(q.idx));
165
+ console.log("XCS | SS | Prefix under-delivered, extending batch", {
166
+ remaining: unquoted.length,
167
+ });
168
+ await quoteAndStore(unquoted);
169
+ lookup = responseByIdx.get(quoteData.idx);
170
+ }
171
+ if (lookup == null)
172
+ continue;
173
+ const { quote: resp, aggregator } = lookup;
174
+ if (resp == null)
175
+ continue;
176
+ console.debug("XCS | SS | quote-eval", {
177
+ idx: quoteData.idx,
178
+ remainder: remainder.toFixed(),
179
+ input: resp.input,
180
+ output: resp.output,
181
+ });
182
+ const divisor = Decimal.pow(10, quoteData.cur.decimals);
183
+ const oamD = new Decimal(resp.output.amount);
184
+ if (oamD.gt(remainder)) {
185
+ const indicativePrice = Decimal.div(resp.input.amountRaw.toString(), resp.output.amountRaw.toString());
186
+ const userBal = new Decimal(quoteData.originalHolding.amountRaw.toString());
187
+ let expectedInput = Decimal.min(remainder.mul(divisor).mul(indicativePrice).mul(safetyMultiplier), userBal);
188
+ let attempts = 0;
189
+ while (true) {
190
+ if (++attempts > 10) {
191
+ throw new AutoSelectionError("Partial quote did not converge");
192
+ }
193
+ console.debug("XCS | SS | partial_quote_loop", {
194
+ indicativePrice: indicativePrice.toFixed(),
195
+ expectedInput: expectedInput.toFixed(),
196
+ userBal: userBal.toFixed(),
197
+ remainder: remainder.toFixed(),
198
+ });
199
+ const adequate = await aggregateAggregators([
200
+ {
201
+ ...quoteData.req,
202
+ seriousness: QuoteSeriousness.SERIOUS,
203
+ inputAmount: convertDecimalToBigInt(expectedInput),
204
+ },
205
+ ], aggregators, AggregateAggregatorsMode.MaximizeOutput);
206
+ if (adequate.length !== 1) {
207
+ throw new AutoSelectionError("Unexpected response length from aggregateAggregators");
208
+ }
209
+ const aq = adequate[0];
210
+ if (aq.quote == null) {
211
+ throw new AutoSelectionError("Couldn't get buy quote");
212
+ }
213
+ const oam2D = new Decimal(aq.quote.output.amount);
214
+ if (oam2D.gte(remainder)) {
215
+ final.push({
216
+ quote: aq.quote,
217
+ aggregator: aq.aggregator,
218
+ holding: quoteData.originalHolding,
219
+ chainID: Number(quoteData.req.chain.chainID),
220
+ });
221
+ remainder = remainder.minus(oam2D);
222
+ break;
223
+ }
224
+ else if (expectedInput.eq(userBal)) {
225
+ throw new AutoSelectionError("Holding was supposedly enough to meet the full requirement but ceased to be so subsequently");
226
+ }
227
+ else {
228
+ expectedInput = Decimal.min(expectedInput.mul(safetyMultiplier), userBal);
229
+ }
230
+ }
231
+ }
232
+ else {
233
+ final.push({
234
+ quote: resp,
235
+ holding: quoteData.originalHolding,
236
+ aggregator,
237
+ chainID: Number(quoteData.req.chain.chainID),
238
+ });
239
+ remainder = remainder.minus(resp.output.amount);
240
+ console.debug("XCS | SS | full_quote", {
241
+ idx: quoteData.idx,
242
+ remainder: remainder.toFixed(),
243
+ });
244
+ }
245
+ }
246
+ if (remainder.gt(0)) {
247
+ throw new AutoSelectionError("NOT_ENOUGH_SWAP_FOR_REQUIREMENT");
248
+ }
249
+ console.log("XCS | SS | final_quotes", { quotes: final, cots: usedCOTs });
250
+ return { quoteResponses: final, usedCOTs };
251
+ }
@@ -13,7 +13,7 @@ export type HoldingWithSwapAddresses = Holding & {
13
13
  };
14
14
  export declare class AutoSelectionError extends Error {
15
15
  }
16
- declare const enum AggregateAggregatorsMode {
16
+ export declare enum AggregateAggregatorsMode {
17
17
  MaximizeOutput = 0,
18
18
  MinimizeInput = 1
19
19
  }
@@ -102,4 +102,3 @@ export declare function autoSelectSources(args: {
102
102
  cur: Currency;
103
103
  }[];
104
104
  }>;
105
- export {};
@@ -18,21 +18,21 @@ export type BebopCommonQuote = {
18
18
  buyTokens: Record<Hex, {
19
19
  amount: string;
20
20
  decimals: number;
21
- priceUsd: number;
21
+ priceUsd?: number;
22
22
  symbol: string;
23
23
  minimumAmount: string;
24
- price: number;
25
- priceBeforeFee: number;
24
+ price?: number;
25
+ priceBeforeFee?: number;
26
26
  amountBeforeFee: string;
27
27
  deltaFromExpected: number;
28
28
  }>;
29
29
  sellTokens: Record<Hex, {
30
30
  amount: string;
31
31
  decimals: number;
32
- priceUsd: number;
32
+ priceUsd?: number;
33
33
  symbol: string;
34
- price: number;
35
- priceBeforeFee: number;
34
+ price?: number;
35
+ priceBeforeFee?: number;
36
36
  }>;
37
37
  settlementAddress: string;
38
38
  approvalTarget: Hex;
@@ -3,3 +3,4 @@ export * from './lifi-agg';
3
3
  export * from './bebop-agg';
4
4
  export * from './fibrous-agg';
5
5
  export * from './autochoice';
6
+ export * from './selectsources';
@@ -33,6 +33,9 @@ export type LiFiResponse = {
33
33
  export declare class LiFiAggregator implements Aggregator {
34
34
  private static readonly BASE_URL_V1;
35
35
  private static readonly COMMON_OPTIONS;
36
+ private static readonly GLOBAL_DENY;
37
+ private static readonly PER_CHAIN_DENY;
38
+ private denyExchangesFor;
36
39
  private readonly axios;
37
40
  constructor(apiKey: string);
38
41
  getQuotes(requests: (QuoteRequestExactInput | QuoteRequestExactOutput)[]): Promise<(Quote | null)[]>;
@@ -0,0 +1,27 @@
1
+ import Decimal from "decimal.js";
2
+ import { Aggregator, Holding, QuoteResponse } from "./iface";
3
+ import { Currency, CurrencyID, OmniversalChainID } from "../data";
4
+ import { Bytes } from "../types";
5
+ export type SourceWithValue = {
6
+ chainID: OmniversalChainID;
7
+ tokenAddress: Bytes;
8
+ amountRaw: bigint;
9
+ takerAddress: Bytes;
10
+ receiverAddress: Bytes;
11
+ value: number;
12
+ };
13
+ export declare function selectSources(args: {
14
+ sources: SourceWithValue[];
15
+ outputRequired: Decimal;
16
+ aggregators: Aggregator[];
17
+ commonCurrencyID?: CurrencyID;
18
+ prefixHeadroom?: Decimal;
19
+ }): Promise<{
20
+ quoteResponses: QuoteResponse[];
21
+ usedCOTs: {
22
+ originalHolding: Holding;
23
+ amountUsed: Decimal;
24
+ idx: number;
25
+ cur: Currency;
26
+ }[];
27
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avail-project/ca-common",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "common utilities for CA",
5
5
  "files": [
6
6
  "dist"