@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.
@@ -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 connectivity data.
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 two-hop routes** using graph traversal:
97
- * - For each token A, find its neighbors (tokens directly connected)
98
- * - For each neighbor B, find B's neighbors
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: Generate two-hop routes using graph traversal
135
- // Algorithm: For each token, explore all paths of length 2
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 [start, neighbors] of tokenGraph.entries()) {
138
- // MIDDLE LOOP: "Where can I go from the starting token?" (first hop)
139
- // Example: If start = USDm, neighbors might be [CELO, USDC, KESm]
140
- for (const intermediate of neighbors) {
141
- // Get all tokens reachable from this intermediate token (second hop destinations)
142
- const secondHopNeighbors = tokenGraph.get(intermediate);
143
- if (!secondHopNeighbors)
144
- continue;
145
- // INNER LOOP: "From the intermediate token, where can I go?" (second hop)
146
- // Example: If intermediate = CELO, secondHopNeighbors might be [USDm, EURm, BRLm]
147
- for (const end of secondHopNeighbors) {
148
- // Skip circular routes like USDm CELO → USDm (pointless)
149
- if (end === start)
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 sophisticated route selection algorithm with
298
- * multiple optimization tiers:
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 - First Available** (Last Resort):
314
- * - If no other heuristics apply, use the first route found
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 = candidates.filter(hasCostData);
416
+ const candidatesWithCost = eligibleCandidates.filter(hasCostData);
335
417
  if (candidatesWithCost.length > 0) {
336
- return candidatesWithCost.reduce((best, current) => current.costData.totalCostPercent < best.costData.totalCostPercent ? current : best);
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 directRoute = candidates.find((c) => c.path.length === 1);
340
- if (directRoute)
341
- return directRoute;
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 routeWithStablecoin = candidates.find((candidate) => {
345
- const intermediateToken = getIntermediateToken(candidate);
346
- if (!intermediateToken)
347
- return false;
348
- const symbol = addrToSymbol.get(intermediateToken);
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
- // Tier 4: Use first available route as last resort
352
- return routeWithStablecoin || candidates[0];
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 two-hop route.
356
- * In a two-hop route A->B->C, this function finds token B (the intermediate).
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
- // Find the common token between the two hops
360
- const [hop1, hop2] = route.path;
361
- const hop1Tokens = [hop1.token0, hop1.token1];
362
- const hop2Tokens = [hop2.token0, hop2.token1];
363
- return hop1Tokens.find((addr) => hop2Tokens.includes(addr));
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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mento-protocol/mento-sdk",
3
3
  "description": "Official SDK for interacting with the Mento Protocol",
4
- "version": "3.3.1",
4
+ "version": "3.4.0",
5
5
  "license": "MIT",
6
6
  "author": "Mento Labs",
7
7
  "keywords": [