@mento-protocol/mento-sdk 3.3.0 → 3.4.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/README.md +44 -31
- package/dist/cache/routes.js +7745 -5125
- package/dist/core/types/route.d.ts +2 -1
- package/dist/esm/cache/routes.js +7745 -5125
- package/dist/esm/services/routes/RouteService.js +41 -7
- package/dist/esm/utils/pathEncoder.js +1 -1
- package/dist/esm/utils/routeUtils.js +189 -58
- package/dist/services/routes/RouteService.d.ts +4 -4
- package/dist/services/routes/RouteService.js +40 -6
- package/dist/utils/pathEncoder.d.ts +1 -1
- package/dist/utils/pathEncoder.js +1 -1
- package/dist/utils/routeUtils.d.ts +38 -14
- package/dist/utils/routeUtils.js +192 -58
- package/package.json +1 -1
package/dist/utils/routeUtils.js
CHANGED
|
@@ -5,9 +5,13 @@ exports.generateAllRoutes = generateAllRoutes;
|
|
|
5
5
|
exports.createTwoHopRoute = createTwoHopRoute;
|
|
6
6
|
exports.selectOptimalRoutes = selectOptimalRoutes;
|
|
7
7
|
exports.selectBestRoute = selectBestRoute;
|
|
8
|
+
exports.compareRoutePath = compareRoutePath;
|
|
9
|
+
exports.deterministicRoutePathKey = deterministicRoutePathKey;
|
|
8
10
|
exports.getIntermediateToken = getIntermediateToken;
|
|
11
|
+
exports.getIntermediateTokens = getIntermediateTokens;
|
|
9
12
|
exports.hasCostData = hasCostData;
|
|
10
13
|
const sortUtils_1 = require("./sortUtils");
|
|
14
|
+
const MAX_DISCOVERED_ROUTE_HOPS = 3;
|
|
11
15
|
/**
|
|
12
16
|
* Builds the connectivity data structures needed for route generation.
|
|
13
17
|
*
|
|
@@ -88,15 +92,15 @@ function buildConnectivityStructures(directRoutes) {
|
|
|
88
92
|
return { addrToSymbol, directRouteMap, tokenGraph, directRoutes };
|
|
89
93
|
}
|
|
90
94
|
/**
|
|
91
|
-
* Generates all possible routes (direct + two-hop) using
|
|
95
|
+
* Generates all possible routes (direct + two-hop + eligible three-hop) using
|
|
96
|
+
* connectivity data.
|
|
92
97
|
*
|
|
93
98
|
* This function implements a route discovery algorithm that:
|
|
94
99
|
*
|
|
95
|
-
* 1. **Adds all direct routes** (single-hop routes)
|
|
96
|
-
* 2. **Discovers
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* - If B connects to token C (C ≠ A), then A->B->C is a valid route
|
|
100
|
+
* 1. **Adds all direct routes** (single-hop routes).
|
|
101
|
+
* 2. **Discovers multi-hop candidates** with one bounded simple-path traversal.
|
|
102
|
+
* 3. **Keeps every two-hop route** and only the shortest routes with three or
|
|
103
|
+
* more hops. The current discovery limit is three hops.
|
|
100
104
|
*
|
|
101
105
|
* **Route Deduplication**: Multiple routes between the same token pair
|
|
102
106
|
* are collected in arrays, allowing the selection algorithm to choose
|
|
@@ -131,39 +135,110 @@ function generateAllRoutes(connectivityData) {
|
|
|
131
135
|
}
|
|
132
136
|
allRoutes.get(route.id).push(route);
|
|
133
137
|
}
|
|
134
|
-
// Step 2:
|
|
135
|
-
|
|
138
|
+
// Step 2: Discover every simple multi-hop candidate in one bounded traversal.
|
|
139
|
+
const discoveredRoutes = [];
|
|
136
140
|
// OUTER LOOP: "For each starting token..." (e.g., USDm, CELO, EURm, etc.)
|
|
137
|
-
for (const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
continue;
|
|
151
|
-
// At this point we have a potential route: start → intermediate → end
|
|
152
|
-
// Example: USDm → CELO → EURm
|
|
153
|
-
// Try to create a valid two-hop trading pair from this route
|
|
154
|
-
const twoHopRoute = createTwoHopRoute(start, intermediate, end, addrToSymbol, directRouteMap);
|
|
155
|
-
// If we successfully created the pair, add it to our collection
|
|
156
|
-
if (twoHopRoute) {
|
|
157
|
-
if (!allRoutes.has(twoHopRoute.id)) {
|
|
158
|
-
allRoutes.set(twoHopRoute.id, []);
|
|
159
|
-
}
|
|
160
|
-
allRoutes.get(twoHopRoute.id).push(twoHopRoute);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
141
|
+
for (const start of tokenGraph.keys()) {
|
|
142
|
+
discoverSimplePaths(start, start, [], new Set([start]), new Set(), MAX_DISCOVERED_ROUTE_HOPS, tokenGraph, addrToSymbol, directRouteMap, (route, pathStart, pathEnd) => discoveredRoutes.push({ route, start: pathStart, end: pathEnd }));
|
|
143
|
+
}
|
|
144
|
+
// Step 3: Determine the minimum structural distance for every endpoint pair.
|
|
145
|
+
// Direct routes seed the map with one hop. The traversal supplies distances
|
|
146
|
+
// from two hops up to MAX_DISCOVERED_ROUTE_HOPS.
|
|
147
|
+
const minimumHopsByRouteId = new Map();
|
|
148
|
+
for (const routeId of allRoutes.keys())
|
|
149
|
+
minimumHopsByRouteId.set(routeId, 1);
|
|
150
|
+
for (const { route } of discoveredRoutes) {
|
|
151
|
+
const minimumHops = minimumHopsByRouteId.get(route.id);
|
|
152
|
+
if (minimumHops === undefined || route.path.length < minimumHops) {
|
|
153
|
+
minimumHopsByRouteId.set(route.id, route.path.length);
|
|
163
154
|
}
|
|
164
155
|
}
|
|
156
|
+
// Preserve existing two-hop alternatives, including alternatives for direct
|
|
157
|
+
// pairs. Routes with three or more hops are eligible only when they are the
|
|
158
|
+
// shortest structural path for their endpoint pair. Insert every two-hop
|
|
159
|
+
// route first to preserve the existing route and map order.
|
|
160
|
+
const seenGeneratedPaths = new Set();
|
|
161
|
+
for (const { route, start, end } of discoveredRoutes) {
|
|
162
|
+
if (route.path.length !== 2)
|
|
163
|
+
continue;
|
|
164
|
+
addGeneratedRoute(allRoutes, route, start, end, addrToSymbol, seenGeneratedPaths);
|
|
165
|
+
}
|
|
166
|
+
for (const { route, start, end } of discoveredRoutes) {
|
|
167
|
+
const minimumHops = minimumHopsByRouteId.get(route.id);
|
|
168
|
+
if (route.path.length < 3 || route.path.length !== minimumHops)
|
|
169
|
+
continue;
|
|
170
|
+
addGeneratedRoute(allRoutes, route, start, end, addrToSymbol, seenGeneratedPaths);
|
|
171
|
+
}
|
|
165
172
|
return allRoutes;
|
|
166
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Walks a token graph up to `maxHops`, emitting only simple paths. The graph
|
|
176
|
+
* stores token connectivity while `directRouteMap` resolves each edge to the
|
|
177
|
+
* executable pool used by the route path.
|
|
178
|
+
*/
|
|
179
|
+
function discoverSimplePaths(start, current, path, visitedTokens, visitedPools, maxHops, tokenGraph, addrToSymbol, directRouteMap, onRoute) {
|
|
180
|
+
if (path.length === maxHops)
|
|
181
|
+
return;
|
|
182
|
+
// RECURSIVE LOOP: "Where can I go from the current token?"
|
|
183
|
+
// Example: USDm → CELO then inspects CELO's neighbors, such as EURm.
|
|
184
|
+
for (const next of tokenGraph.get(current) ?? []) {
|
|
185
|
+
// Skip circular paths such as USDm → CELO → USDm.
|
|
186
|
+
if (visitedTokens.has(next))
|
|
187
|
+
continue;
|
|
188
|
+
const currentSymbol = addrToSymbol.get(current);
|
|
189
|
+
const nextSymbol = addrToSymbol.get(next);
|
|
190
|
+
if (!currentSymbol || !nextSymbol)
|
|
191
|
+
continue;
|
|
192
|
+
const pool = directRouteMap.get((0, sortUtils_1.canonicalSymbolKey)(currentSymbol, nextSymbol));
|
|
193
|
+
if (!pool || visitedPools.has(poolSignature(pool)))
|
|
194
|
+
continue;
|
|
195
|
+
const nextPath = [...path, pool];
|
|
196
|
+
const nextVisitedTokens = new Set(visitedTokens).add(next);
|
|
197
|
+
const nextVisitedPools = new Set(visitedPools).add(poolSignature(pool));
|
|
198
|
+
// Two or more pools define a potential multi-hop route.
|
|
199
|
+
// Example: USDm → CELO → EURm is a two-hop route.
|
|
200
|
+
if (nextPath.length >= 2) {
|
|
201
|
+
const route = createRouteFromPath(start, next, nextPath, addrToSymbol);
|
|
202
|
+
if (route)
|
|
203
|
+
onRoute(route, start, next);
|
|
204
|
+
}
|
|
205
|
+
discoverSimplePaths(start, next, nextPath, nextVisitedTokens, nextVisitedPools, maxHops, tokenGraph, addrToSymbol, directRouteMap, onRoute);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function addGeneratedRoute(allRoutes, route, start, end, addrToSymbol, seenGeneratedPaths) {
|
|
209
|
+
const canonicalRoute = canonicalizeGeneratedRoute(route, start, end, addrToSymbol);
|
|
210
|
+
const signature = `${canonicalRoute.id}:${canonicalRoute.path.map(poolSignature).join('|')}`;
|
|
211
|
+
if (seenGeneratedPaths.has(signature))
|
|
212
|
+
return;
|
|
213
|
+
seenGeneratedPaths.add(signature);
|
|
214
|
+
if (!allRoutes.has(canonicalRoute.id))
|
|
215
|
+
allRoutes.set(canonicalRoute.id, []);
|
|
216
|
+
allRoutes.get(canonicalRoute.id).push(canonicalRoute);
|
|
217
|
+
}
|
|
218
|
+
function createRouteFromPath(startAddr, endAddr, path, addrToSymbol) {
|
|
219
|
+
const startSymbol = addrToSymbol.get(startAddr);
|
|
220
|
+
const endSymbol = addrToSymbol.get(endAddr);
|
|
221
|
+
if (!startSymbol || !endSymbol || startAddr === endAddr)
|
|
222
|
+
return null;
|
|
223
|
+
const routeId = (0, sortUtils_1.canonicalSymbolKey)(startSymbol, endSymbol);
|
|
224
|
+
const startToken = { address: startAddr, symbol: startSymbol };
|
|
225
|
+
const endToken = { address: endAddr, symbol: endSymbol };
|
|
226
|
+
return {
|
|
227
|
+
id: routeId,
|
|
228
|
+
tokens: startSymbol <= endSymbol ? [startToken, endToken] : [endToken, startToken],
|
|
229
|
+
path,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function canonicalizeGeneratedRoute(route, startAddr, endAddr, addrToSymbol) {
|
|
233
|
+
const startSymbol = addrToSymbol.get(startAddr);
|
|
234
|
+
const endSymbol = addrToSymbol.get(endAddr);
|
|
235
|
+
if (!startSymbol || !endSymbol || startSymbol < endSymbol)
|
|
236
|
+
return route;
|
|
237
|
+
return { ...route, path: [...route.path].reverse() };
|
|
238
|
+
}
|
|
239
|
+
function poolSignature(pool) {
|
|
240
|
+
return `${pool.poolAddr}:${pool.factoryAddr}`;
|
|
241
|
+
}
|
|
167
242
|
/**
|
|
168
243
|
* Creates a two-hop tradable pair if valid exchange hops exist.
|
|
169
244
|
*
|
|
@@ -294,8 +369,10 @@ function selectOptimalRoutes(allRoutes, returnAllRoutes, addrToSymbol) {
|
|
|
294
369
|
/**
|
|
295
370
|
* Selects the best route from candidates using cost data or fallback heuristics.
|
|
296
371
|
*
|
|
297
|
-
* This function implements a
|
|
298
|
-
*
|
|
372
|
+
* This function implements a tiered route selection algorithm.
|
|
373
|
+
*
|
|
374
|
+
* **Eligibility guard**:
|
|
375
|
+
* - Exclude three-hop candidates when a direct or two-hop candidate exists
|
|
299
376
|
*
|
|
300
377
|
* **Tier 1 - Cost-Based Optimization** (Preferred):
|
|
301
378
|
* - Use routes with cost data (actual cost information)
|
|
@@ -310,8 +387,8 @@ function selectOptimalRoutes(allRoutes, returnAllRoutes, addrToSymbol) {
|
|
|
310
387
|
* - For two-hop routes, prefer those going through major stablecoins
|
|
311
388
|
* - Major FX currencies like USDm and EURm typically have better liquidity
|
|
312
389
|
*
|
|
313
|
-
* **Tier 4 -
|
|
314
|
-
* - If no other
|
|
390
|
+
* **Tier 4 - Deterministic Path Order** (Last Resort):
|
|
391
|
+
* - If no other heuristic applies, use the route with the lowest stable path key
|
|
315
392
|
*
|
|
316
393
|
* @param candidates - Array of possible routes for the same token pair
|
|
317
394
|
* @param assetMap - Asset map for token symbol lookups
|
|
@@ -330,37 +407,94 @@ function selectOptimalRoutes(allRoutes, returnAllRoutes, addrToSymbol) {
|
|
|
330
407
|
* ```
|
|
331
408
|
*/
|
|
332
409
|
function selectBestRoute(candidates, addrToSymbol) {
|
|
410
|
+
// A three-hop route is eligible only when no direct or two-hop candidate
|
|
411
|
+
// exists. Keep this invariant at selection time as a defense against stale
|
|
412
|
+
// or manually assembled candidate sets.
|
|
413
|
+
const shorterCandidates = candidates.filter((candidate) => candidate.path.length <= 2);
|
|
414
|
+
const eligibleCandidates = shorterCandidates.length > 0 ? shorterCandidates : candidates;
|
|
333
415
|
// Tier 1: Prefer routes with cost data (lowest cost wins)
|
|
334
|
-
const candidatesWithCost =
|
|
416
|
+
const candidatesWithCost = eligibleCandidates.filter(hasCostData);
|
|
335
417
|
if (candidatesWithCost.length > 0) {
|
|
336
|
-
return candidatesWithCost.reduce((best, current) => current
|
|
418
|
+
return candidatesWithCost.reduce((best, current) => (compareCostedRoutes(current, best) < 0 ? current : best));
|
|
337
419
|
}
|
|
338
420
|
// Tier 2: Prefer direct routes (single-hop, lower risk)
|
|
339
|
-
const
|
|
340
|
-
if (
|
|
341
|
-
return
|
|
421
|
+
const directRoutes = eligibleCandidates.filter((candidate) => candidate.path.length === 1);
|
|
422
|
+
if (directRoutes.length > 0)
|
|
423
|
+
return [...directRoutes].sort(compareRoutePath)[0];
|
|
342
424
|
// Tier 3: Prefer routes through major stablecoins (better liquidity)
|
|
343
425
|
const stablecoins = ['USDm', 'EURm', 'USDC', 'USDT'];
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return
|
|
348
|
-
|
|
349
|
-
return symbol && stablecoins.includes(symbol);
|
|
426
|
+
const routesWithStablecoin = eligibleCandidates.filter((candidate) => {
|
|
427
|
+
return getIntermediateTokens(candidate).some((token) => {
|
|
428
|
+
const symbol = addrToSymbol.get(token);
|
|
429
|
+
return symbol !== undefined && stablecoins.includes(symbol);
|
|
430
|
+
});
|
|
350
431
|
});
|
|
351
|
-
|
|
352
|
-
|
|
432
|
+
if (routesWithStablecoin.length > 0)
|
|
433
|
+
return [...routesWithStablecoin].sort(compareRoutePath)[0];
|
|
434
|
+
// Tier 4: Use a stable path key so discovery order cannot change the result.
|
|
435
|
+
return [...eligibleCandidates].sort(compareRoutePath)[0];
|
|
436
|
+
}
|
|
437
|
+
function compareCostedRoutes(candidate, current) {
|
|
438
|
+
const costDifference = candidate.costData.totalCostPercent - current.costData.totalCostPercent;
|
|
439
|
+
return costDifference || compareRoutePath(candidate, current);
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Compares route paths by hop count and a stable pool identity key.
|
|
443
|
+
*
|
|
444
|
+
* Callers must apply their own higher-level ordering before this comparator,
|
|
445
|
+
* such as route ID or cost preference.
|
|
446
|
+
*/
|
|
447
|
+
function compareRoutePath(first, second) {
|
|
448
|
+
const hopDifference = first.path.length - second.path.length;
|
|
449
|
+
if (hopDifference !== 0)
|
|
450
|
+
return hopDifference;
|
|
451
|
+
const firstKey = deterministicRoutePathKey(first);
|
|
452
|
+
const secondKey = deterministicRoutePathKey(second);
|
|
453
|
+
if (firstKey < secondKey)
|
|
454
|
+
return -1;
|
|
455
|
+
if (firstKey > secondKey)
|
|
456
|
+
return 1;
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Returns the canonical, case-insensitive key for a route's pool path.
|
|
461
|
+
*
|
|
462
|
+
* Keep this ordering stable across fresh route selection, cache generation,
|
|
463
|
+
* and cached route lookup.
|
|
464
|
+
*/
|
|
465
|
+
function deterministicRoutePathKey(route) {
|
|
466
|
+
return route.path
|
|
467
|
+
.map((pool) => [pool.poolType, pool.factoryAddr, pool.poolAddr, pool.token0, pool.token1]
|
|
468
|
+
.map((value) => value.toLowerCase())
|
|
469
|
+
.join(':'))
|
|
470
|
+
.join('|');
|
|
353
471
|
}
|
|
354
472
|
/**
|
|
355
|
-
* Extracts the intermediate token address from a
|
|
356
|
-
* In a two-hop route A->B->C, this function finds token B
|
|
473
|
+
* Extracts the first intermediate token address from a multi-hop route.
|
|
474
|
+
* In a two-hop route A->B->C, this function finds token B. For longer routes,
|
|
475
|
+
* it preserves the helper's original single-token return type and returns the
|
|
476
|
+
* first intermediate token.
|
|
357
477
|
*/
|
|
358
478
|
function getIntermediateToken(route) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
479
|
+
return getIntermediateTokens(route)[0];
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Returns every intermediate token in an executable multi-hop path. Adjacent
|
|
483
|
+
* pools must share exactly one token for that token to be considered part of
|
|
484
|
+
* the path.
|
|
485
|
+
*/
|
|
486
|
+
function getIntermediateTokens(route) {
|
|
487
|
+
const intermediateTokens = [];
|
|
488
|
+
for (let index = 0; index < route.path.length - 1; index++) {
|
|
489
|
+
const currentPool = route.path[index];
|
|
490
|
+
const nextPool = route.path[index + 1];
|
|
491
|
+
const nextTokens = new Set([nextPool.token0, nextPool.token1]);
|
|
492
|
+
const sharedTokens = [currentPool.token0, currentPool.token1].filter((token) => nextTokens.has(token));
|
|
493
|
+
if (sharedTokens.length !== 1)
|
|
494
|
+
return [];
|
|
495
|
+
intermediateTokens.push(sharedTokens[0]);
|
|
496
|
+
}
|
|
497
|
+
return intermediateTokens;
|
|
364
498
|
}
|
|
365
499
|
/**
|
|
366
500
|
* Type guard to check if a Route has cost data.
|