@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.
- package/dist/cjs/xcs/autochoice.js +14 -9
- package/dist/cjs/xcs/bebop-agg.js +2 -2
- package/dist/cjs/xcs/index.js +1 -0
- package/dist/cjs/xcs/lifi-agg.js +23 -1
- package/dist/cjs/xcs/selectsources.js +255 -0
- package/dist/esm/xcs/autochoice.js +13 -8
- package/dist/esm/xcs/bebop-agg.js +2 -2
- package/dist/esm/xcs/index.js +1 -0
- package/dist/esm/xcs/lifi-agg.js +23 -1
- package/dist/esm/xcs/selectsources.js +251 -0
- package/dist/types/xcs/autochoice.d.ts +1 -2
- package/dist/types/xcs/bebop-agg.d.ts +6 -6
- package/dist/types/xcs/index.d.ts +1 -0
- package/dist/types/xcs/lifi-agg.d.ts +3 -0
- package/dist/types/xcs/selectsources.d.ts +27 -0
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
130
|
+
value: decimal_js_1.default.mul(outputAmountInDecimal, buyT.priceUsd ?? 0).toNumber(),
|
|
131
131
|
symbol: buyT.symbol,
|
|
132
132
|
},
|
|
133
133
|
txData: {
|
package/dist/cjs/xcs/index.js
CHANGED
|
@@ -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);
|
package/dist/cjs/xcs/lifi-agg.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
126
|
+
value: Decimal.mul(outputAmountInDecimal, buyT.priceUsd ?? 0).toNumber(),
|
|
127
127
|
symbol: buyT.symbol,
|
|
128
128
|
},
|
|
129
129
|
txData: {
|
package/dist/esm/xcs/index.js
CHANGED
package/dist/esm/xcs/lifi-agg.js
CHANGED
|
@@ -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
|
|
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
|
|
21
|
+
priceUsd?: number;
|
|
22
22
|
symbol: string;
|
|
23
23
|
minimumAmount: string;
|
|
24
|
-
price
|
|
25
|
-
priceBeforeFee
|
|
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
|
|
32
|
+
priceUsd?: number;
|
|
33
33
|
symbol: string;
|
|
34
|
-
price
|
|
35
|
-
priceBeforeFee
|
|
34
|
+
price?: number;
|
|
35
|
+
priceBeforeFee?: number;
|
|
36
36
|
}>;
|
|
37
37
|
settlementAddress: string;
|
|
38
38
|
approvalTarget: Hex;
|
|
@@ -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
|
+
}>;
|