@mento-protocol/mento-sdk 3.3.1 → 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 +6741 -4919
- package/dist/core/types/route.d.ts +2 -1
- package/dist/esm/cache/routes.js +6741 -4919
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ERC20_ABI } from '../../core/abis';
|
|
2
2
|
import { RouteNotFoundError } from '../../core/errors';
|
|
3
|
-
import { buildConnectivityStructures, generateAllRoutes, selectOptimalRoutes } from '../../utils/routeUtils';
|
|
3
|
+
import { buildConnectivityStructures, compareRoutePath, generateAllRoutes, selectOptimalRoutes, } from '../../utils/routeUtils';
|
|
4
4
|
import { canonicalSymbolKey } from '../../utils/sortUtils';
|
|
5
5
|
import { multicall } from '../../utils/multicall';
|
|
6
6
|
/**
|
|
@@ -8,8 +8,8 @@ import { multicall } from '../../utils/multicall';
|
|
|
8
8
|
* Handles route discovery for both direct (single-hop) and multi-hop trading paths.
|
|
9
9
|
*
|
|
10
10
|
* Routes are identified by their token pair and include the path of pools
|
|
11
|
-
* needed to execute the trade.
|
|
12
|
-
*
|
|
11
|
+
* needed to execute the trade. The service discovers routes with at most three
|
|
12
|
+
* hops. It adds three-hop routes only when no direct or two-hop path exists.
|
|
13
13
|
*/
|
|
14
14
|
export class RouteService {
|
|
15
15
|
constructor(publicClient, chainId, poolService) {
|
|
@@ -77,7 +77,7 @@ export class RouteService {
|
|
|
77
77
|
return routes;
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
|
-
* Discovers all tradable routes
|
|
80
|
+
* Discovers all tradable routes with at most three hops
|
|
81
81
|
* Uses cached data by default for instant results, or generates fresh from blockchain
|
|
82
82
|
*
|
|
83
83
|
* @param options - Configuration options
|
|
@@ -144,7 +144,7 @@ export class RouteService {
|
|
|
144
144
|
* if (route.path.length === 1) {
|
|
145
145
|
* console.log('Direct route available')
|
|
146
146
|
* } else {
|
|
147
|
-
* console.log('
|
|
147
|
+
* console.log('Multi-hop route:', route.path)
|
|
148
148
|
* }
|
|
149
149
|
* ```
|
|
150
150
|
*/
|
|
@@ -173,7 +173,7 @@ export class RouteService {
|
|
|
173
173
|
}
|
|
174
174
|
// Build connectivity structures for route finding
|
|
175
175
|
const connectivity = buildConnectivityStructures(directRoutes);
|
|
176
|
-
// Generate
|
|
176
|
+
// Generate direct, two-hop, and eligible three-hop routes
|
|
177
177
|
const allRoutes = generateAllRoutes(connectivity);
|
|
178
178
|
// Select routes based on returnAllRoutes flag
|
|
179
179
|
const selectedRoutes = selectOptimalRoutes(allRoutes, returnAllRoutes, connectivity.addrToSymbol);
|
|
@@ -208,7 +208,11 @@ export class RouteService {
|
|
|
208
208
|
buildLookup(routes) {
|
|
209
209
|
const lookup = new Map();
|
|
210
210
|
for (const route of routes) {
|
|
211
|
-
|
|
211
|
+
const key = makeTokenPairKey(route.tokens[0].address, route.tokens[1].address);
|
|
212
|
+
const current = lookup.get(key);
|
|
213
|
+
if (!current || isPreferredCachedRoute(route, current)) {
|
|
214
|
+
lookup.set(key, route);
|
|
215
|
+
}
|
|
212
216
|
}
|
|
213
217
|
return lookup;
|
|
214
218
|
}
|
|
@@ -266,3 +270,33 @@ function makeTokenPairKey(tokenA, tokenB) {
|
|
|
266
270
|
const [first, second] = [tokenA.toLowerCase(), tokenB.toLowerCase()].sort();
|
|
267
271
|
return `${first}:${second}`;
|
|
268
272
|
}
|
|
273
|
+
function isPreferredCachedRoute(candidate, current) {
|
|
274
|
+
const candidateIsThreeHop = candidate.path.length === 3;
|
|
275
|
+
const currentIsThreeHop = current.path.length === 3;
|
|
276
|
+
// Three-hop routes must not compete with direct or two-hop routes. This also
|
|
277
|
+
// protects lookups from a stale or manually produced cache that violates the
|
|
278
|
+
// route-generation eligibility rule.
|
|
279
|
+
if (candidateIsThreeHop !== currentIsThreeHop) {
|
|
280
|
+
return currentIsThreeHop;
|
|
281
|
+
}
|
|
282
|
+
const candidateCost = getRouteCost(candidate);
|
|
283
|
+
const currentCost = getRouteCost(current);
|
|
284
|
+
// A measured cost is preferred over a route without cost data. When both
|
|
285
|
+
// costs are available, lower cost is preferred.
|
|
286
|
+
if (candidateCost !== undefined || currentCost !== undefined) {
|
|
287
|
+
if (candidateCost === undefined)
|
|
288
|
+
return false;
|
|
289
|
+
if (currentCost === undefined)
|
|
290
|
+
return true;
|
|
291
|
+
if (candidateCost !== currentCost)
|
|
292
|
+
return candidateCost < currentCost;
|
|
293
|
+
}
|
|
294
|
+
// Hop count and exact path ties use the shared deterministic route order.
|
|
295
|
+
return compareRoutePath(candidate, current) < 0;
|
|
296
|
+
}
|
|
297
|
+
function getRouteCost(route) {
|
|
298
|
+
if (!('costData' in route))
|
|
299
|
+
return undefined;
|
|
300
|
+
const cost = route.costData?.totalCostPercent;
|
|
301
|
+
return typeof cost === 'number' && Number.isFinite(cost) ? cost : undefined;
|
|
302
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @param tokenIn - The input token address (determines swap direction)
|
|
7
7
|
* @param _tokenOut - The output token address (unused but kept for API clarity)
|
|
8
8
|
* @returns Array of RouterRoute objects for the contract call
|
|
9
|
-
* @throws {Error} If path is empty
|
|
9
|
+
* @throws {Error} If path is empty or contains invalid pools
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```typescript
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canonicalSymbolKey } from './sortUtils';
|
|
2
|
+
const MAX_DISCOVERED_ROUTE_HOPS = 3;
|
|
2
3
|
/**
|
|
3
4
|
* Builds the connectivity data structures needed for route generation.
|
|
4
5
|
*
|
|
@@ -79,15 +80,15 @@ export function buildConnectivityStructures(directRoutes) {
|
|
|
79
80
|
return { addrToSymbol, directRouteMap, tokenGraph, directRoutes };
|
|
80
81
|
}
|
|
81
82
|
/**
|
|
82
|
-
* Generates all possible routes (direct + two-hop) using
|
|
83
|
+
* Generates all possible routes (direct + two-hop + eligible three-hop) using
|
|
84
|
+
* connectivity data.
|
|
83
85
|
*
|
|
84
86
|
* This function implements a route discovery algorithm that:
|
|
85
87
|
*
|
|
86
|
-
* 1. **Adds all direct routes** (single-hop routes)
|
|
87
|
-
* 2. **Discovers
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* - If B connects to token C (C ≠ A), then A->B->C is a valid route
|
|
88
|
+
* 1. **Adds all direct routes** (single-hop routes).
|
|
89
|
+
* 2. **Discovers multi-hop candidates** with one bounded simple-path traversal.
|
|
90
|
+
* 3. **Keeps every two-hop route** and only the shortest routes with three or
|
|
91
|
+
* more hops. The current discovery limit is three hops.
|
|
91
92
|
*
|
|
92
93
|
* **Route Deduplication**: Multiple routes between the same token pair
|
|
93
94
|
* are collected in arrays, allowing the selection algorithm to choose
|
|
@@ -122,39 +123,110 @@ export function generateAllRoutes(connectivityData) {
|
|
|
122
123
|
}
|
|
123
124
|
allRoutes.get(route.id).push(route);
|
|
124
125
|
}
|
|
125
|
-
// Step 2:
|
|
126
|
-
|
|
126
|
+
// Step 2: Discover every simple multi-hop candidate in one bounded traversal.
|
|
127
|
+
const discoveredRoutes = [];
|
|
127
128
|
// OUTER LOOP: "For each starting token..." (e.g., USDm, CELO, EURm, etc.)
|
|
128
|
-
for (const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
continue;
|
|
142
|
-
// At this point we have a potential route: start → intermediate → end
|
|
143
|
-
// Example: USDm → CELO → EURm
|
|
144
|
-
// Try to create a valid two-hop trading pair from this route
|
|
145
|
-
const twoHopRoute = createTwoHopRoute(start, intermediate, end, addrToSymbol, directRouteMap);
|
|
146
|
-
// If we successfully created the pair, add it to our collection
|
|
147
|
-
if (twoHopRoute) {
|
|
148
|
-
if (!allRoutes.has(twoHopRoute.id)) {
|
|
149
|
-
allRoutes.set(twoHopRoute.id, []);
|
|
150
|
-
}
|
|
151
|
-
allRoutes.get(twoHopRoute.id).push(twoHopRoute);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
129
|
+
for (const start of tokenGraph.keys()) {
|
|
130
|
+
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 }));
|
|
131
|
+
}
|
|
132
|
+
// Step 3: Determine the minimum structural distance for every endpoint pair.
|
|
133
|
+
// Direct routes seed the map with one hop. The traversal supplies distances
|
|
134
|
+
// from two hops up to MAX_DISCOVERED_ROUTE_HOPS.
|
|
135
|
+
const minimumHopsByRouteId = new Map();
|
|
136
|
+
for (const routeId of allRoutes.keys())
|
|
137
|
+
minimumHopsByRouteId.set(routeId, 1);
|
|
138
|
+
for (const { route } of discoveredRoutes) {
|
|
139
|
+
const minimumHops = minimumHopsByRouteId.get(route.id);
|
|
140
|
+
if (minimumHops === undefined || route.path.length < minimumHops) {
|
|
141
|
+
minimumHopsByRouteId.set(route.id, route.path.length);
|
|
154
142
|
}
|
|
155
143
|
}
|
|
144
|
+
// Preserve existing two-hop alternatives, including alternatives for direct
|
|
145
|
+
// pairs. Routes with three or more hops are eligible only when they are the
|
|
146
|
+
// shortest structural path for their endpoint pair. Insert every two-hop
|
|
147
|
+
// route first to preserve the existing route and map order.
|
|
148
|
+
const seenGeneratedPaths = new Set();
|
|
149
|
+
for (const { route, start, end } of discoveredRoutes) {
|
|
150
|
+
if (route.path.length !== 2)
|
|
151
|
+
continue;
|
|
152
|
+
addGeneratedRoute(allRoutes, route, start, end, addrToSymbol, seenGeneratedPaths);
|
|
153
|
+
}
|
|
154
|
+
for (const { route, start, end } of discoveredRoutes) {
|
|
155
|
+
const minimumHops = minimumHopsByRouteId.get(route.id);
|
|
156
|
+
if (route.path.length < 3 || route.path.length !== minimumHops)
|
|
157
|
+
continue;
|
|
158
|
+
addGeneratedRoute(allRoutes, route, start, end, addrToSymbol, seenGeneratedPaths);
|
|
159
|
+
}
|
|
156
160
|
return allRoutes;
|
|
157
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Walks a token graph up to `maxHops`, emitting only simple paths. The graph
|
|
164
|
+
* stores token connectivity while `directRouteMap` resolves each edge to the
|
|
165
|
+
* executable pool used by the route path.
|
|
166
|
+
*/
|
|
167
|
+
function discoverSimplePaths(start, current, path, visitedTokens, visitedPools, maxHops, tokenGraph, addrToSymbol, directRouteMap, onRoute) {
|
|
168
|
+
if (path.length === maxHops)
|
|
169
|
+
return;
|
|
170
|
+
// RECURSIVE LOOP: "Where can I go from the current token?"
|
|
171
|
+
// Example: USDm → CELO then inspects CELO's neighbors, such as EURm.
|
|
172
|
+
for (const next of tokenGraph.get(current) ?? []) {
|
|
173
|
+
// Skip circular paths such as USDm → CELO → USDm.
|
|
174
|
+
if (visitedTokens.has(next))
|
|
175
|
+
continue;
|
|
176
|
+
const currentSymbol = addrToSymbol.get(current);
|
|
177
|
+
const nextSymbol = addrToSymbol.get(next);
|
|
178
|
+
if (!currentSymbol || !nextSymbol)
|
|
179
|
+
continue;
|
|
180
|
+
const pool = directRouteMap.get(canonicalSymbolKey(currentSymbol, nextSymbol));
|
|
181
|
+
if (!pool || visitedPools.has(poolSignature(pool)))
|
|
182
|
+
continue;
|
|
183
|
+
const nextPath = [...path, pool];
|
|
184
|
+
const nextVisitedTokens = new Set(visitedTokens).add(next);
|
|
185
|
+
const nextVisitedPools = new Set(visitedPools).add(poolSignature(pool));
|
|
186
|
+
// Two or more pools define a potential multi-hop route.
|
|
187
|
+
// Example: USDm → CELO → EURm is a two-hop route.
|
|
188
|
+
if (nextPath.length >= 2) {
|
|
189
|
+
const route = createRouteFromPath(start, next, nextPath, addrToSymbol);
|
|
190
|
+
if (route)
|
|
191
|
+
onRoute(route, start, next);
|
|
192
|
+
}
|
|
193
|
+
discoverSimplePaths(start, next, nextPath, nextVisitedTokens, nextVisitedPools, maxHops, tokenGraph, addrToSymbol, directRouteMap, onRoute);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function addGeneratedRoute(allRoutes, route, start, end, addrToSymbol, seenGeneratedPaths) {
|
|
197
|
+
const canonicalRoute = canonicalizeGeneratedRoute(route, start, end, addrToSymbol);
|
|
198
|
+
const signature = `${canonicalRoute.id}:${canonicalRoute.path.map(poolSignature).join('|')}`;
|
|
199
|
+
if (seenGeneratedPaths.has(signature))
|
|
200
|
+
return;
|
|
201
|
+
seenGeneratedPaths.add(signature);
|
|
202
|
+
if (!allRoutes.has(canonicalRoute.id))
|
|
203
|
+
allRoutes.set(canonicalRoute.id, []);
|
|
204
|
+
allRoutes.get(canonicalRoute.id).push(canonicalRoute);
|
|
205
|
+
}
|
|
206
|
+
function createRouteFromPath(startAddr, endAddr, path, addrToSymbol) {
|
|
207
|
+
const startSymbol = addrToSymbol.get(startAddr);
|
|
208
|
+
const endSymbol = addrToSymbol.get(endAddr);
|
|
209
|
+
if (!startSymbol || !endSymbol || startAddr === endAddr)
|
|
210
|
+
return null;
|
|
211
|
+
const routeId = canonicalSymbolKey(startSymbol, endSymbol);
|
|
212
|
+
const startToken = { address: startAddr, symbol: startSymbol };
|
|
213
|
+
const endToken = { address: endAddr, symbol: endSymbol };
|
|
214
|
+
return {
|
|
215
|
+
id: routeId,
|
|
216
|
+
tokens: startSymbol <= endSymbol ? [startToken, endToken] : [endToken, startToken],
|
|
217
|
+
path,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function canonicalizeGeneratedRoute(route, startAddr, endAddr, addrToSymbol) {
|
|
221
|
+
const startSymbol = addrToSymbol.get(startAddr);
|
|
222
|
+
const endSymbol = addrToSymbol.get(endAddr);
|
|
223
|
+
if (!startSymbol || !endSymbol || startSymbol < endSymbol)
|
|
224
|
+
return route;
|
|
225
|
+
return { ...route, path: [...route.path].reverse() };
|
|
226
|
+
}
|
|
227
|
+
function poolSignature(pool) {
|
|
228
|
+
return `${pool.poolAddr}:${pool.factoryAddr}`;
|
|
229
|
+
}
|
|
158
230
|
/**
|
|
159
231
|
* Creates a two-hop tradable pair if valid exchange hops exist.
|
|
160
232
|
*
|
|
@@ -285,8 +357,10 @@ export function selectOptimalRoutes(allRoutes, returnAllRoutes, addrToSymbol) {
|
|
|
285
357
|
/**
|
|
286
358
|
* Selects the best route from candidates using cost data or fallback heuristics.
|
|
287
359
|
*
|
|
288
|
-
* This function implements a
|
|
289
|
-
*
|
|
360
|
+
* This function implements a tiered route selection algorithm.
|
|
361
|
+
*
|
|
362
|
+
* **Eligibility guard**:
|
|
363
|
+
* - Exclude three-hop candidates when a direct or two-hop candidate exists
|
|
290
364
|
*
|
|
291
365
|
* **Tier 1 - Cost-Based Optimization** (Preferred):
|
|
292
366
|
* - Use routes with cost data (actual cost information)
|
|
@@ -301,8 +375,8 @@ export function selectOptimalRoutes(allRoutes, returnAllRoutes, addrToSymbol) {
|
|
|
301
375
|
* - For two-hop routes, prefer those going through major stablecoins
|
|
302
376
|
* - Major FX currencies like USDm and EURm typically have better liquidity
|
|
303
377
|
*
|
|
304
|
-
* **Tier 4 -
|
|
305
|
-
* - If no other
|
|
378
|
+
* **Tier 4 - Deterministic Path Order** (Last Resort):
|
|
379
|
+
* - If no other heuristic applies, use the route with the lowest stable path key
|
|
306
380
|
*
|
|
307
381
|
* @param candidates - Array of possible routes for the same token pair
|
|
308
382
|
* @param assetMap - Asset map for token symbol lookups
|
|
@@ -321,37 +395,94 @@ export function selectOptimalRoutes(allRoutes, returnAllRoutes, addrToSymbol) {
|
|
|
321
395
|
* ```
|
|
322
396
|
*/
|
|
323
397
|
export function selectBestRoute(candidates, addrToSymbol) {
|
|
398
|
+
// A three-hop route is eligible only when no direct or two-hop candidate
|
|
399
|
+
// exists. Keep this invariant at selection time as a defense against stale
|
|
400
|
+
// or manually assembled candidate sets.
|
|
401
|
+
const shorterCandidates = candidates.filter((candidate) => candidate.path.length <= 2);
|
|
402
|
+
const eligibleCandidates = shorterCandidates.length > 0 ? shorterCandidates : candidates;
|
|
324
403
|
// Tier 1: Prefer routes with cost data (lowest cost wins)
|
|
325
|
-
const candidatesWithCost =
|
|
404
|
+
const candidatesWithCost = eligibleCandidates.filter(hasCostData);
|
|
326
405
|
if (candidatesWithCost.length > 0) {
|
|
327
|
-
return candidatesWithCost.reduce((best, current) => current
|
|
406
|
+
return candidatesWithCost.reduce((best, current) => (compareCostedRoutes(current, best) < 0 ? current : best));
|
|
328
407
|
}
|
|
329
408
|
// Tier 2: Prefer direct routes (single-hop, lower risk)
|
|
330
|
-
const
|
|
331
|
-
if (
|
|
332
|
-
return
|
|
409
|
+
const directRoutes = eligibleCandidates.filter((candidate) => candidate.path.length === 1);
|
|
410
|
+
if (directRoutes.length > 0)
|
|
411
|
+
return [...directRoutes].sort(compareRoutePath)[0];
|
|
333
412
|
// Tier 3: Prefer routes through major stablecoins (better liquidity)
|
|
334
413
|
const stablecoins = ['USDm', 'EURm', 'USDC', 'USDT'];
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return
|
|
339
|
-
|
|
340
|
-
return symbol && stablecoins.includes(symbol);
|
|
414
|
+
const routesWithStablecoin = eligibleCandidates.filter((candidate) => {
|
|
415
|
+
return getIntermediateTokens(candidate).some((token) => {
|
|
416
|
+
const symbol = addrToSymbol.get(token);
|
|
417
|
+
return symbol !== undefined && stablecoins.includes(symbol);
|
|
418
|
+
});
|
|
341
419
|
});
|
|
342
|
-
|
|
343
|
-
|
|
420
|
+
if (routesWithStablecoin.length > 0)
|
|
421
|
+
return [...routesWithStablecoin].sort(compareRoutePath)[0];
|
|
422
|
+
// Tier 4: Use a stable path key so discovery order cannot change the result.
|
|
423
|
+
return [...eligibleCandidates].sort(compareRoutePath)[0];
|
|
424
|
+
}
|
|
425
|
+
function compareCostedRoutes(candidate, current) {
|
|
426
|
+
const costDifference = candidate.costData.totalCostPercent - current.costData.totalCostPercent;
|
|
427
|
+
return costDifference || compareRoutePath(candidate, current);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Compares route paths by hop count and a stable pool identity key.
|
|
431
|
+
*
|
|
432
|
+
* Callers must apply their own higher-level ordering before this comparator,
|
|
433
|
+
* such as route ID or cost preference.
|
|
434
|
+
*/
|
|
435
|
+
export function compareRoutePath(first, second) {
|
|
436
|
+
const hopDifference = first.path.length - second.path.length;
|
|
437
|
+
if (hopDifference !== 0)
|
|
438
|
+
return hopDifference;
|
|
439
|
+
const firstKey = deterministicRoutePathKey(first);
|
|
440
|
+
const secondKey = deterministicRoutePathKey(second);
|
|
441
|
+
if (firstKey < secondKey)
|
|
442
|
+
return -1;
|
|
443
|
+
if (firstKey > secondKey)
|
|
444
|
+
return 1;
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Returns the canonical, case-insensitive key for a route's pool path.
|
|
449
|
+
*
|
|
450
|
+
* Keep this ordering stable across fresh route selection, cache generation,
|
|
451
|
+
* and cached route lookup.
|
|
452
|
+
*/
|
|
453
|
+
export function deterministicRoutePathKey(route) {
|
|
454
|
+
return route.path
|
|
455
|
+
.map((pool) => [pool.poolType, pool.factoryAddr, pool.poolAddr, pool.token0, pool.token1]
|
|
456
|
+
.map((value) => value.toLowerCase())
|
|
457
|
+
.join(':'))
|
|
458
|
+
.join('|');
|
|
344
459
|
}
|
|
345
460
|
/**
|
|
346
|
-
* Extracts the intermediate token address from a
|
|
347
|
-
* In a two-hop route A->B->C, this function finds token B
|
|
461
|
+
* Extracts the first intermediate token address from a multi-hop route.
|
|
462
|
+
* In a two-hop route A->B->C, this function finds token B. For longer routes,
|
|
463
|
+
* it preserves the helper's original single-token return type and returns the
|
|
464
|
+
* first intermediate token.
|
|
348
465
|
*/
|
|
349
466
|
export function getIntermediateToken(route) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
467
|
+
return getIntermediateTokens(route)[0];
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Returns every intermediate token in an executable multi-hop path. Adjacent
|
|
471
|
+
* pools must share exactly one token for that token to be considered part of
|
|
472
|
+
* the path.
|
|
473
|
+
*/
|
|
474
|
+
export function getIntermediateTokens(route) {
|
|
475
|
+
const intermediateTokens = [];
|
|
476
|
+
for (let index = 0; index < route.path.length - 1; index++) {
|
|
477
|
+
const currentPool = route.path[index];
|
|
478
|
+
const nextPool = route.path[index + 1];
|
|
479
|
+
const nextTokens = new Set([nextPool.token0, nextPool.token1]);
|
|
480
|
+
const sharedTokens = [currentPool.token0, currentPool.token1].filter((token) => nextTokens.has(token));
|
|
481
|
+
if (sharedTokens.length !== 1)
|
|
482
|
+
return [];
|
|
483
|
+
intermediateTokens.push(sharedTokens[0]);
|
|
484
|
+
}
|
|
485
|
+
return intermediateTokens;
|
|
355
486
|
}
|
|
356
487
|
/**
|
|
357
488
|
* Type guard to check if a Route has cost data.
|
|
@@ -16,8 +16,8 @@ export interface RouteOptions {
|
|
|
16
16
|
* Handles route discovery for both direct (single-hop) and multi-hop trading paths.
|
|
17
17
|
*
|
|
18
18
|
* Routes are identified by their token pair and include the path of pools
|
|
19
|
-
* needed to execute the trade.
|
|
20
|
-
*
|
|
19
|
+
* needed to execute the trade. The service discovers routes with at most three
|
|
20
|
+
* hops. It adds three-hop routes only when no direct or two-hop path exists.
|
|
21
21
|
*/
|
|
22
22
|
export declare class RouteService {
|
|
23
23
|
private publicClient;
|
|
@@ -43,7 +43,7 @@ export declare class RouteService {
|
|
|
43
43
|
*/
|
|
44
44
|
getDirectRoutes(): Promise<Route[]>;
|
|
45
45
|
/**
|
|
46
|
-
* Discovers all tradable routes
|
|
46
|
+
* Discovers all tradable routes with at most three hops
|
|
47
47
|
* Uses cached data by default for instant results, or generates fresh from blockchain
|
|
48
48
|
*
|
|
49
49
|
* @param options - Configuration options
|
|
@@ -83,7 +83,7 @@ export declare class RouteService {
|
|
|
83
83
|
* if (route.path.length === 1) {
|
|
84
84
|
* console.log('Direct route available')
|
|
85
85
|
* } else {
|
|
86
|
-
* console.log('
|
|
86
|
+
* console.log('Multi-hop route:', route.path)
|
|
87
87
|
* }
|
|
88
88
|
* ```
|
|
89
89
|
*/
|
|
@@ -44,8 +44,8 @@ const multicall_1 = require("../../utils/multicall");
|
|
|
44
44
|
* Handles route discovery for both direct (single-hop) and multi-hop trading paths.
|
|
45
45
|
*
|
|
46
46
|
* Routes are identified by their token pair and include the path of pools
|
|
47
|
-
* needed to execute the trade.
|
|
48
|
-
*
|
|
47
|
+
* needed to execute the trade. The service discovers routes with at most three
|
|
48
|
+
* hops. It adds three-hop routes only when no direct or two-hop path exists.
|
|
49
49
|
*/
|
|
50
50
|
class RouteService {
|
|
51
51
|
constructor(publicClient, chainId, poolService) {
|
|
@@ -113,7 +113,7 @@ class RouteService {
|
|
|
113
113
|
return routes;
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
|
-
* Discovers all tradable routes
|
|
116
|
+
* Discovers all tradable routes with at most three hops
|
|
117
117
|
* Uses cached data by default for instant results, or generates fresh from blockchain
|
|
118
118
|
*
|
|
119
119
|
* @param options - Configuration options
|
|
@@ -180,7 +180,7 @@ class RouteService {
|
|
|
180
180
|
* if (route.path.length === 1) {
|
|
181
181
|
* console.log('Direct route available')
|
|
182
182
|
* } else {
|
|
183
|
-
* console.log('
|
|
183
|
+
* console.log('Multi-hop route:', route.path)
|
|
184
184
|
* }
|
|
185
185
|
* ```
|
|
186
186
|
*/
|
|
@@ -209,7 +209,7 @@ class RouteService {
|
|
|
209
209
|
}
|
|
210
210
|
// Build connectivity structures for route finding
|
|
211
211
|
const connectivity = (0, routeUtils_1.buildConnectivityStructures)(directRoutes);
|
|
212
|
-
// Generate
|
|
212
|
+
// Generate direct, two-hop, and eligible three-hop routes
|
|
213
213
|
const allRoutes = (0, routeUtils_1.generateAllRoutes)(connectivity);
|
|
214
214
|
// Select routes based on returnAllRoutes flag
|
|
215
215
|
const selectedRoutes = (0, routeUtils_1.selectOptimalRoutes)(allRoutes, returnAllRoutes, connectivity.addrToSymbol);
|
|
@@ -244,7 +244,11 @@ class RouteService {
|
|
|
244
244
|
buildLookup(routes) {
|
|
245
245
|
const lookup = new Map();
|
|
246
246
|
for (const route of routes) {
|
|
247
|
-
|
|
247
|
+
const key = makeTokenPairKey(route.tokens[0].address, route.tokens[1].address);
|
|
248
|
+
const current = lookup.get(key);
|
|
249
|
+
if (!current || isPreferredCachedRoute(route, current)) {
|
|
250
|
+
lookup.set(key, route);
|
|
251
|
+
}
|
|
248
252
|
}
|
|
249
253
|
return lookup;
|
|
250
254
|
}
|
|
@@ -303,4 +307,34 @@ function makeTokenPairKey(tokenA, tokenB) {
|
|
|
303
307
|
const [first, second] = [tokenA.toLowerCase(), tokenB.toLowerCase()].sort();
|
|
304
308
|
return `${first}:${second}`;
|
|
305
309
|
}
|
|
310
|
+
function isPreferredCachedRoute(candidate, current) {
|
|
311
|
+
const candidateIsThreeHop = candidate.path.length === 3;
|
|
312
|
+
const currentIsThreeHop = current.path.length === 3;
|
|
313
|
+
// Three-hop routes must not compete with direct or two-hop routes. This also
|
|
314
|
+
// protects lookups from a stale or manually produced cache that violates the
|
|
315
|
+
// route-generation eligibility rule.
|
|
316
|
+
if (candidateIsThreeHop !== currentIsThreeHop) {
|
|
317
|
+
return currentIsThreeHop;
|
|
318
|
+
}
|
|
319
|
+
const candidateCost = getRouteCost(candidate);
|
|
320
|
+
const currentCost = getRouteCost(current);
|
|
321
|
+
// A measured cost is preferred over a route without cost data. When both
|
|
322
|
+
// costs are available, lower cost is preferred.
|
|
323
|
+
if (candidateCost !== undefined || currentCost !== undefined) {
|
|
324
|
+
if (candidateCost === undefined)
|
|
325
|
+
return false;
|
|
326
|
+
if (currentCost === undefined)
|
|
327
|
+
return true;
|
|
328
|
+
if (candidateCost !== currentCost)
|
|
329
|
+
return candidateCost < currentCost;
|
|
330
|
+
}
|
|
331
|
+
// Hop count and exact path ties use the shared deterministic route order.
|
|
332
|
+
return (0, routeUtils_1.compareRoutePath)(candidate, current) < 0;
|
|
333
|
+
}
|
|
334
|
+
function getRouteCost(route) {
|
|
335
|
+
if (!('costData' in route))
|
|
336
|
+
return undefined;
|
|
337
|
+
const cost = route.costData?.totalCostPercent;
|
|
338
|
+
return typeof cost === 'number' && Number.isFinite(cost) ? cost : undefined;
|
|
339
|
+
}
|
|
306
340
|
//# sourceMappingURL=RouteService.js.map
|
|
@@ -21,7 +21,7 @@ export type ReadonlyRouterRoutes = readonly {
|
|
|
21
21
|
* @param tokenIn - The input token address (determines swap direction)
|
|
22
22
|
* @param _tokenOut - The output token address (unused but kept for API clarity)
|
|
23
23
|
* @returns Array of RouterRoute objects for the contract call
|
|
24
|
-
* @throws {Error} If path is empty
|
|
24
|
+
* @throws {Error} If path is empty or contains invalid pools
|
|
25
25
|
*
|
|
26
26
|
* @example
|
|
27
27
|
* ```typescript
|
|
@@ -9,7 +9,7 @@ exports.encodeRoutePath = encodeRoutePath;
|
|
|
9
9
|
* @param tokenIn - The input token address (determines swap direction)
|
|
10
10
|
* @param _tokenOut - The output token address (unused but kept for API clarity)
|
|
11
11
|
* @returns Array of RouterRoute objects for the contract call
|
|
12
|
-
* @throws {Error} If path is empty
|
|
12
|
+
* @throws {Error} If path is empty or contains invalid pools
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
15
|
* ```typescript
|
|
@@ -11,12 +11,12 @@ type Address = string;
|
|
|
11
11
|
* The main workflow is:
|
|
12
12
|
*
|
|
13
13
|
* 1. Build connectivity structures from direct trading pairs
|
|
14
|
-
* 2. Generate all possible routes (direct + two-hop)
|
|
14
|
+
* 2. Generate all possible routes (direct + two-hop + eligible three-hop)
|
|
15
15
|
* 3. Select optimal routes using cost data or heuristics
|
|
16
16
|
*
|
|
17
17
|
* ALGORITHM OVERVIEW:
|
|
18
18
|
* - Creates a graph where tokens are nodes and direct exchanges are edges
|
|
19
|
-
* - Uses graph traversal to find
|
|
19
|
+
* - Uses bounded graph traversal to find simple routes through intermediate tokens
|
|
20
20
|
* - Optimizes route selection based on cost data when available
|
|
21
21
|
* - Falls back to heuristics (prefer direct routes, major stablecoins)
|
|
22
22
|
* =============================================================================
|
|
@@ -130,15 +130,15 @@ export interface ConnectivityData {
|
|
|
130
130
|
*/
|
|
131
131
|
export declare function buildConnectivityStructures(directRoutes: Route[]): ConnectivityData;
|
|
132
132
|
/**
|
|
133
|
-
* Generates all possible routes (direct + two-hop) using
|
|
133
|
+
* Generates all possible routes (direct + two-hop + eligible three-hop) using
|
|
134
|
+
* connectivity data.
|
|
134
135
|
*
|
|
135
136
|
* This function implements a route discovery algorithm that:
|
|
136
137
|
*
|
|
137
|
-
* 1. **Adds all direct routes** (single-hop routes)
|
|
138
|
-
* 2. **Discovers
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* - If B connects to token C (C ≠ A), then A->B->C is a valid route
|
|
138
|
+
* 1. **Adds all direct routes** (single-hop routes).
|
|
139
|
+
* 2. **Discovers multi-hop candidates** with one bounded simple-path traversal.
|
|
140
|
+
* 3. **Keeps every two-hop route** and only the shortest routes with three or
|
|
141
|
+
* more hops. The current discovery limit is three hops.
|
|
142
142
|
*
|
|
143
143
|
* **Route Deduplication**: Multiple routes between the same token pair
|
|
144
144
|
* are collected in arrays, allowing the selection algorithm to choose
|
|
@@ -246,8 +246,10 @@ export declare function selectOptimalRoutes(allRoutes: Map<RouteID, Route[]>, re
|
|
|
246
246
|
/**
|
|
247
247
|
* Selects the best route from candidates using cost data or fallback heuristics.
|
|
248
248
|
*
|
|
249
|
-
* This function implements a
|
|
250
|
-
*
|
|
249
|
+
* This function implements a tiered route selection algorithm.
|
|
250
|
+
*
|
|
251
|
+
* **Eligibility guard**:
|
|
252
|
+
* - Exclude three-hop candidates when a direct or two-hop candidate exists
|
|
251
253
|
*
|
|
252
254
|
* **Tier 1 - Cost-Based Optimization** (Preferred):
|
|
253
255
|
* - Use routes with cost data (actual cost information)
|
|
@@ -262,8 +264,8 @@ export declare function selectOptimalRoutes(allRoutes: Map<RouteID, Route[]>, re
|
|
|
262
264
|
* - For two-hop routes, prefer those going through major stablecoins
|
|
263
265
|
* - Major FX currencies like USDm and EURm typically have better liquidity
|
|
264
266
|
*
|
|
265
|
-
* **Tier 4 -
|
|
266
|
-
* - If no other
|
|
267
|
+
* **Tier 4 - Deterministic Path Order** (Last Resort):
|
|
268
|
+
* - If no other heuristic applies, use the route with the lowest stable path key
|
|
267
269
|
*
|
|
268
270
|
* @param candidates - Array of possible routes for the same token pair
|
|
269
271
|
* @param assetMap - Asset map for token symbol lookups
|
|
@@ -283,10 +285,32 @@ export declare function selectOptimalRoutes(allRoutes: Map<RouteID, Route[]>, re
|
|
|
283
285
|
*/
|
|
284
286
|
export declare function selectBestRoute(candidates: Route[], addrToSymbol: Map<Address, TokenSymbol>): Route | RouteWithCost;
|
|
285
287
|
/**
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
+
* Compares route paths by hop count and a stable pool identity key.
|
|
289
|
+
*
|
|
290
|
+
* Callers must apply their own higher-level ordering before this comparator,
|
|
291
|
+
* such as route ID or cost preference.
|
|
292
|
+
*/
|
|
293
|
+
export declare function compareRoutePath(first: Route, second: Route): number;
|
|
294
|
+
/**
|
|
295
|
+
* Returns the canonical, case-insensitive key for a route's pool path.
|
|
296
|
+
*
|
|
297
|
+
* Keep this ordering stable across fresh route selection, cache generation,
|
|
298
|
+
* and cached route lookup.
|
|
299
|
+
*/
|
|
300
|
+
export declare function deterministicRoutePathKey(route: Route): string;
|
|
301
|
+
/**
|
|
302
|
+
* Extracts the first intermediate token address from a multi-hop route.
|
|
303
|
+
* In a two-hop route A->B->C, this function finds token B. For longer routes,
|
|
304
|
+
* it preserves the helper's original single-token return type and returns the
|
|
305
|
+
* first intermediate token.
|
|
288
306
|
*/
|
|
289
307
|
export declare function getIntermediateToken(route: Route): Address | undefined;
|
|
308
|
+
/**
|
|
309
|
+
* Returns every intermediate token in an executable multi-hop path. Adjacent
|
|
310
|
+
* pools must share exactly one token for that token to be considered part of
|
|
311
|
+
* the path.
|
|
312
|
+
*/
|
|
313
|
+
export declare function getIntermediateTokens(route: Route): Address[];
|
|
290
314
|
/**
|
|
291
315
|
* Type guard to check if a Route has cost data.
|
|
292
316
|
*/
|