Skip to content

Commit

Permalink
fix: correctly handle number of hops in the routing algorithm (#15)
Browse files Browse the repository at this point in the history
* fix: enforce max hops in routing algorithm

* fix algorithm

* prettier

* fix types

* remove inner loop
  • Loading branch information
wjw12 authored Nov 3, 2023
1 parent 1ba134c commit 7063446
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 123 deletions.
Binary file modified bun.lockb
Binary file not shown.
55 changes: 29 additions & 26 deletions src/ThalaswapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class ThalaswapRouter {
const graph: Graph = {};

for (const pool of pools) {
// Convert pool data to LiquidityPool type
const assets = ["asset0", "asset1", "asset2", "asset3"]
.filter((a) => pool[a as AssetIndex])
.map((a) => pool[a as AssetIndex]!);
Expand All @@ -121,38 +122,40 @@ class ThalaswapRouter {
.filter((b, i) => assets[i])
.map((b) => pool[b as BalanceIndex] as number);

const poolType = pool.name[0] === "S" ? "stable_pool" : "weighted_pool";
const swapFee =
poolType === "stable_pool"
? DEFAULT_SWAP_FEE_STABLE
: DEFAULT_SWAP_FEE_WEIGHTED;

const weights =
poolType === "weighted_pool"
? this.parseWeightsFromWeightedPoolName(pool.name)
: undefined;

const amp =
poolType === "stable_pool"
? this.parseAmpFactorFromStablePoolName(pool.name)
: undefined;

const convertedPool: LiquidityPool = {
coinAddresses: assets.map((a) => a.address),
balances,
poolType,
swapFee,
weights,
amp,
};

for (let i = 0; i < assets.length; i++) {
const token = assets[i].address;
tokens.add(token);
for (let j = 0; j < assets.length; j++) {
if (i !== j) {
if (!graph[token]) graph[token] = [];
const poolType =
pool.name[0] === "S" ? "stable_pool" : "weighted_pool";
const swapFee =
poolType === "stable_pool"
? DEFAULT_SWAP_FEE_STABLE
: DEFAULT_SWAP_FEE_WEIGHTED;

const weights =
poolType === "weighted_pool"
? this.parseWeightsFromWeightedPoolName(pool.name)
: undefined;

const amp =
poolType === "stable_pool"
? this.parseAmpFactorFromStablePoolName(pool.name)
: undefined;

graph[token].push({
pool: {
coinAddresses: assets.map((a) => a.address),
balances,
poolType,
swapFee,
weights,
amp,
},
pool: convertedPool,
fromIndex: i,
toIndex: j,
});
Expand All @@ -168,7 +171,7 @@ class ThalaswapRouter {
startToken: string,
endToken: string,
amountIn: number,
maxHops = 3,
maxHops: number = 3,
): Promise<Route | null> {
await this.refreshData();

Expand All @@ -190,7 +193,7 @@ class ThalaswapRouter {
startToken: string,
endToken: string,
amountOut: number,
maxHops = 3,
maxHops: number = 3,
): Promise<Route | null> {
await this.refreshData();

Expand Down
170 changes: 87 additions & 83 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
LiquidityPool,
Distances,
Predecessors,
Hops,
} from "./types";

function calcOutGivenIn(
Expand Down Expand Up @@ -119,69 +118,72 @@ export function findRouteGivenExactInput(
startToken: string,
endToken: string,
amountIn: number,
maxHops = 3,
maxHops: number,
): Route | null {
const tokens = Object.keys(graph);
// distances[token][hop] is the maximum amount of token that can be received given hop number
let distances: Distances = {};
// predecessors[token][hop] is the previous hop of the optimal path
let predecessors: Predecessors = {};
let hops: Hops = {};

const defaultDistance = -Infinity;
for (const token of tokens) {
distances[token] = -Infinity;
predecessors[token] = null;
hops[token] = 0;
distances[token] = {};
predecessors[token] = {};
}
distances[startToken] = amountIn;
distances[startToken][0] = amountIn;

for (let i = 0; i < maxHops; i++) {
const newDistances = { ...distances };
const newPredecessors = { ...predecessors };
const newHops = { ...hops };

for (const [token, edges] of Object.entries(graph)) {
for (const edge of edges) {
const fromToken = edge.pool.coinAddresses[edge.fromIndex];
// Skip if fromToken is the endToken. This prevents cycles.
if (fromToken === endToken) continue;

const toToken = edge.pool.coinAddresses[edge.toIndex];
if (distances[fromToken] !== -Infinity) {
const newDistance = calcOutGivenIn(
distances[fromToken],
edge.pool,
edge.fromIndex,
edge.toIndex,
);
if (fromToken === endToken || toToken === startToken) continue; // This prevents cycles

if (
newDistance > newDistances[toToken] &&
newHops[fromToken] + 1 <= maxHops
) {
newDistances[toToken] = newDistance;
newPredecessors[toToken] = { token: fromToken, pool: edge.pool };
newHops[toToken] = newHops[fromToken] + 1;
}
if (distances[fromToken][i] === undefined) continue; // Skip unvisited nodes

const newDistance = calcOutGivenIn(
distances[fromToken][i]!,
edge.pool,
edge.fromIndex,
edge.toIndex,
);

const nextHop = i + 1;
if (newDistance > (distances[toToken][nextHop] || defaultDistance)) {
distances[toToken][nextHop] = newDistance;
predecessors[toToken][nextHop] = {
token: fromToken,
pool: edge.pool,
};
}
}
}
}

distances = newDistances;
predecessors = newPredecessors;
hops = newHops;
// Find the best number of hops
let maxDistance = -Infinity;
let hops = 0;
for (let i = 1; i <= maxHops; i++) {
const distance = distances[endToken][i];
if (distance && distance > maxDistance) {
maxDistance = distance;
hops = i;
}
}
if (maxDistance === -Infinity) {
console.error("No path found");
return null;
}

// Reconstruct the path
const path: SwapPath[] = [];
let currentToken = endToken;

while (currentToken !== startToken) {
if (predecessors[currentToken] === null) {
console.error("No path found");
break;
}

const { token, pool } = predecessors[currentToken]!;
while (hops > 0) {
const { token, pool } = predecessors[currentToken]![hops]!;
path.push({ from: token, to: currentToken, pool });
currentToken = token;
hops--;
}

path.reverse();
Expand Down Expand Up @@ -214,7 +216,7 @@ export function findRouteGivenExactInput(
return {
path,
amountIn,
amountOut: distances[endToken],
amountOut: maxDistance,
priceImpactPercentage,
type: "exact_input",
};
Expand All @@ -225,74 +227,76 @@ export function findRouteGivenExactOutput(
startToken: string,
endToken: string,
amountOut: number,
maxHops = 3,
maxHops: number,
): Route | null {
const tokens = Object.keys(graph);
let distances: Distances = {};
let predecessors: Predecessors = {};
let hops: Hops = {};

const defaultDistance = Infinity;
for (const token of tokens) {
distances[token] = Infinity;
predecessors[token] = null;
hops[token] = 0;
distances[token] = {};
predecessors[token] = {};
}
distances[endToken] = amountOut;
distances[endToken][0] = amountOut;

for (let i = 0; i < maxHops; i++) {
const newDistances = { ...distances };
const newPredecessors = { ...predecessors };
const newHops = { ...hops };

for (const [token, edges] of Object.entries(graph)) {
for (const edge of edges) {
const fromToken = edge.pool.coinAddresses[edge.fromIndex];
const toToken = edge.pool.coinAddresses[edge.toIndex];
if (fromToken === endToken || toToken === startToken) continue; // This prevents cycles

// Skip if toToken is the startToken. This prevents cycles.
if (toToken === startToken) continue;
if (distances[toToken][i] === undefined) continue; // Skip unvisited nodes

if (distances[toToken] !== Infinity) {
try {
const newDistance = calcInGivenOut(
distances[toToken],
edge.pool,
edge.fromIndex,
edge.toIndex,
);
try {
const newDistance = calcInGivenOut(
distances[toToken][i]!,
edge.pool,
edge.fromIndex,
edge.toIndex,
);

if (
newDistance < newDistances[fromToken] &&
newHops[toToken] + 1 <= maxHops
) {
newDistances[fromToken] = newDistance;
newPredecessors[fromToken] = { token: toToken, pool: edge.pool };
newHops[fromToken] = newHops[toToken] + 1;
}
} catch (error) {
// If expected output amount is greater than pool balance, do not update distance
const nextHop = i + 1;
if (
newDistance < (distances[fromToken][nextHop] || defaultDistance)
) {
distances[fromToken][nextHop] = newDistance;
predecessors[fromToken][nextHop] = {
token: toToken,
pool: edge.pool,
};
}
} catch (error) {
// If expected output amount is greater than pool balance, do not update distance
}
}
}
}

distances = newDistances;
predecessors = newPredecessors;
hops = newHops;
// Find the best number of hops
let minDistance = Infinity;
let hops = 0;
for (let i = 1; i <= maxHops; i++) {
const distance = distances[startToken][i];
if (distance && distance < minDistance) {
minDistance = distance;
hops = i;
}
}
if (minDistance === Infinity) {
console.error("No path found");
return null;
}

// Reconstruct the path
const path: SwapPath[] = [];
let currentToken = startToken;

while (currentToken !== endToken) {
if (predecessors[currentToken] === null) {
console.error("No path found");
return null;
}

const { token, pool } = predecessors[currentToken]!;
while (hops > 0) {
const { token, pool } = predecessors[currentToken]![hops]!;
path.push({ from: currentToken, to: token, pool });
currentToken = token;
hops--;
}

// We use the maximum price impact of all path segments as the price impact of the entire route
Expand Down Expand Up @@ -325,7 +329,7 @@ export function findRouteGivenExactOutput(

return {
path: path.reverse(),
amountIn: distances[startToken],
amountIn: minDistance,
amountOut,
priceImpactPercentage,
type: "exact_output",
Expand Down
6 changes: 2 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,11 @@ type PoolData = {
type RouteType = "exact_input" | "exact_output";
type PoolType = "stable_pool" | "weighted_pool";
type Graph = Record<string, Edge[]>;
type Distances = Record<string, number>;
type Distances = Record<string, Record<number, number>>;
type Predecessors = Record<
string,
{ token: string; pool: LiquidityPool } | null
Record<number, { token: string; pool: LiquidityPool } | null>
>;
type Hops = Record<string, number>;

type AssetIndex = "asset0" | "asset1" | "asset2" | "asset3";
type BalanceIndex = "balance0" | "balance1" | "balance2" | "balance3";
Expand All @@ -85,7 +84,6 @@ export type {
Graph,
Distances,
Predecessors,
Hops,
PoolType,
AssetIndex,
BalanceIndex,
Expand Down
Loading

0 comments on commit 7063446

Please sign in to comment.