Skip to content

Commit

Permalink
feat: add graph path finding
Browse files Browse the repository at this point in the history
  • Loading branch information
SirTLB committed Feb 14, 2023
1 parent 8130313 commit 613462c
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 123 deletions.
189 changes: 189 additions & 0 deletions src/core/arbitrage/graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { AssetInfo, isNativeAsset } from "../types/base/asset";
import { Path } from "../types/base/path";
import { Pool } from "../types/base/pool";

export interface Graph {
vertices: Map<string, Vertex>;
}

type Vertex = {
name: string;
edges: Array<Edge>;
};

type Edge = {
from: Vertex;
to: Vertex;
pools: Array<Pool>;
};

// **************************************** GRAPH **************************************** \\

/**
*
*/
export function newGraph(pools: Array<Pool>): Graph {
const vertices = new Map();
const graph: Graph = { vertices: vertices };
for (const pool of pools) {
const vertexA: Vertex = getVertex(
graph,
isNativeAsset(pool.assets[0].info)
? pool.assets[0].info.native_token.denom
: pool.assets[0].info.token.contract_addr,
);
const vertexB: Vertex = getVertex(
graph,
isNativeAsset(pool.assets[1].info)
? pool.assets[1].info.native_token.denom
: pool.assets[1].info.token.contract_addr,
);
connectTo(vertexA, vertexB, pool);
connectTo(vertexB, vertexA, pool);
}
return graph;
}
/**
*
*/
function getVertex(graph: Graph, name: string): Vertex {
const vertex = graph.vertices.get(name);
if (vertex === undefined) {
const addedVertex = newVertex(name);
graph.vertices.set(name, addedVertex);
return addedVertex;
} else {
return vertex;
}
}
/**
*
*/
export function getPaths(graph: Graph, startingAsset: AssetInfo, depth: number): Array<Path> | undefined {
const startingAssetName = isNativeAsset(startingAsset)
? startingAsset.native_token.denom
: startingAsset.token.contract_addr;
const root = graph.vertices.get(startingAssetName);
if (!root) {
console.log("graph does not contain starting asset");
return undefined;
}

const edgeLists = depthFirstSearch(root, root.name, depth);

const poolLists: Array<Array<Pool>> = [];
for (const edgeList of edgeLists) {
let newPoolLists: Array<Array<Pool>> = [];
for (const edge of edgeList) {
newPoolLists = expandEdge(edge, newPoolLists);
if (newPoolLists.length === 0) {
break;
}
}
poolLists.push(...newPoolLists);
}
const paths: Array<Path> = [];
// create paths
for (const poolList of poolLists) {
if (poolList.length < 2) {
continue;
}
paths.push({
pools: poolList,
});
}
return paths;
}

// *************************************** VERTEX **************************************** \\

/**
*
*/
function newVertex(name: string): Vertex {
const vertex: Vertex = { name: name, edges: [] };
return vertex;
}

/**
*
*/
function connectTo(vertex: Vertex, otherVertex: Vertex, pool: Pool) {
for (const edge of vertex.edges) {
if (edge.to.name == otherVertex.name) {
addEdge(edge, pool);
return;
}
}
vertex.edges.push(newEdge(vertex, otherVertex, pool));
}
/**
*
*/
function depthFirstSearch(vertex: Vertex, start: string, depth: number): Array<Array<Edge>> {
const edgeLists: Array<Array<Edge>> = [];
if (depth < 1) {
return edgeLists;
}
for (const edge of vertex.edges) {
// Base Case
if (edge.to.name === start) {
edgeLists.push([edge]);
}
// Recursive case
if (depth > 1) {
for (const edgeList of depthFirstSearch(edge.to, start, depth - 1)) {
edgeList.push(edge);
edgeLists.push(edgeList);
}
}
}
return edgeLists;
}

// **************************************** EDGES **************************************** \\
/**
*
*/
function newEdge(from: Vertex, to: Vertex, pool: Pool): Edge {
const edge: Edge = { from: from, to: to, pools: [pool] };
return edge;
}
/**
*
*/
function addEdge(edge: Edge, pool: Pool) {
edge.pools.push(pool);
}

/**
*
*/
function expandEdge(edge: Edge, oldLists: Array<Array<Pool>>): Array<Array<Pool>> {
const newHopLists: Array<Array<Pool>> = [];
if (oldLists.length === 0) {
for (const pool of edge.pools) {
newHopLists.push([pool]);
}
} else {
for (const pool of edge.pools) {
for (const oldList of oldLists) {
if (!contained(oldList, pool)) {
const newHopList: Array<Pool> = [pool];
for (const oldPool of oldList) {
newHopList.push(oldPool);
}
newHopLists.push(newHopList);
}
}
}
}
return newHopLists;
}

/**
*
*/
function contained(poollist: Array<Pool>, pool: Pool): boolean {
return poollist.find((pooloflist) => pooloflist.address === pool.address) !== undefined;
}
3 changes: 1 addition & 2 deletions src/core/logging/slacklogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ export function getSlackClient(token: string) {
export async function sendSlackMessage(message: string, client: WebClient | undefined, channel: string | undefined) {
if (client && channel) {
// send log to slack channel
const result = await client.chat.postMessage({
await client.chat.postMessage({
text: message,
channel: channel,
});
console.log(result);
} else {
// log to stdout
console.log(message);
Expand Down
2 changes: 1 addition & 1 deletion src/core/types/arbitrageloops/mempoolLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export class MempoolLoop {

const TX_FEE =
this.botConfig.txFees.get(arbTrade.path.pools.length) ??
Array.from(this.botConfig.txFees.values())[this.botConfig.gasFees.size];
Array.from(this.botConfig.txFees.values())[this.botConfig.txFees.size];

// sign, encode and broadcast the transaction
const txRaw = await this.botClients.SigningCWClient.sign(
Expand Down
2 changes: 1 addition & 1 deletion src/core/types/arbitrageloops/skipLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class SkipLoop extends MempoolLoop {
//if gas fee cannot be found in the botconfig based on pathlengths, pick highest available
const TX_FEE =
this.botConfig.txFees.get(arbTrade.path.pools.length) ??
Array.from(this.botConfig.txFees.values())[this.botConfig.gasFees.size];
Array.from(this.botConfig.txFees.values())[this.botConfig.txFees.size];
const txRaw: TxRaw = await this.botClients.SigningCWClient.sign(
this.account.address,
msgs,
Expand Down
11 changes: 6 additions & 5 deletions src/core/types/base/botConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface BotConfig {
chainPrefix: string;
rpcUrl: string;
poolEnvs: Array<{ pool: string; inputfee: number; outputfee: number }>;
maxPathPools: number;
mappingFactoryRouter: Array<{ factory: string; router: string }>;
flashloanRouterAddress: string;
offerAssetInfo: NativeAssetInfo;
Expand All @@ -21,7 +22,6 @@ export interface BotConfig {
baseDenom: string;

gasPrice: string;
gasFees: Map<number, Coin>;
txFees: Map<number, StdFee>;
profitThresholds: Map<number, number>;

Expand Down Expand Up @@ -75,15 +75,17 @@ export function setBotConfig(envs: NodeJS.ProcessEnv): BotConfig {
TX_FEES.set(hops, { amount: [gasFee], gas: String(GAS_USAGE_PER_HOP * hops) });
const profitThreshold: number =
skipConfig === undefined
? PROFIT_THRESHOLD / (1 - FLASHLOAN_FEE) + +gasFee.amount //dont use skip bid on top of the threshold, include flashloan fee and gas fee
: PROFIT_THRESHOLD / (1 - FLASHLOAN_FEE) + +gasFee.amount + skipConfig.skipBidRate * PROFIT_THRESHOLD; //need extra profit to provide the skip bid
? PROFIT_THRESHOLD / (1 - FLASHLOAN_FEE / 100) + +gasFee.amount //dont use skip bid on top of the threshold, include flashloan fee and gas fee
: PROFIT_THRESHOLD / (1 - FLASHLOAN_FEE / 100) +
+gasFee.amount +
skipConfig.skipBidRate * PROFIT_THRESHOLD; //need extra profit to provide the skip bid
PROFIT_THRESHOLDS.set(hops, profitThreshold);
}

const botConfig: BotConfig = {
chainPrefix: envs.CHAIN_PREFIX,
rpcUrl: envs.RPC_URL,
poolEnvs: POOLS_ENVS,
maxPathPools: MAX_PATH_HOPS,
mappingFactoryRouter: FACTORIES_TO_ROUTERS_MAPPING,
flashloanRouterAddress: envs.FLASHLOAN_ROUTER_ADDRESS,
offerAssetInfo: OFFER_ASSET_INFO,
Expand All @@ -92,7 +94,6 @@ export function setBotConfig(envs: NodeJS.ProcessEnv): BotConfig {
baseDenom: envs.BASE_DENOM,
gasPrice: envs.GAS_UNIT_PRICE,
profitThresholds: PROFIT_THRESHOLDS,
gasFees: GAS_FEES,
txFees: TX_FEES,
slackToken: envs.SLACK_TOKEN,
slackChannel: envs.SLACK_CHANNEL,
Expand Down
91 changes: 1 addition & 90 deletions src/core/types/base/path.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,5 @@
import { identity } from "../identity";
import { isMatchingAssetInfos, isNativeAsset, NativeAssetInfo } from "./asset";
import { Pool, routeBetweenPools } from "./pool";
import { Pool } from "./pool";

export interface Path {
pools: Array<Pool>;
}

/**
* Creates a list of possible 2-hop paths given a list of pools.
* @param pools The pools to create paths from.
* @returns All the paths that exist.
*/
export function getPathsFromPool(pools: Array<Pool>, offerAsset: NativeAssetInfo): Array<Path> {
return pools.flatMap((a) => {
return (
pools
// filter out same pools
.filter((b) => a !== b)
// filter pools with same asset infos
.filter((b) => {
const matchingAssets = a.assets.filter(
(assetA) =>
b.assets.find((assetB) => isMatchingAssetInfos(assetA.info, assetB.info)) !== undefined,
);
return matchingAssets.length === a.assets.length;
})
.filter(
(a) =>
isMatchingAssetInfos(a.assets[0].info, offerAsset) ||
isMatchingAssetInfos(a.assets[1].info, offerAsset),
)
.map((b) => {
return identity<Path>({
pools: [a, b],
});
})
);
});
}

/**
*
*/
export function getPathsFromPools3Hop(pools: Array<Pool>, offerAsset: NativeAssetInfo): Array<Path> {
const viablePaths: Array<Path> = [];
const all3HopPaths: Array<Array<Pool>> = [];
pools.map((a) => {
pools.map((b) => {
if (a.address != b.address) {
pools.map((c) => {
if (a.address != c.address && b.address != c.address) {
all3HopPaths.push([a, b, c]);
}
});
}
});
});
for (const potentialPath of all3HopPaths) {
if (
routeBetweenPools(potentialPath[0], potentialPath[1]) &&
routeBetweenPools(potentialPath[1], potentialPath[2]) &&
routeBetweenPools(potentialPath[0], potentialPath[2])
) {
const path: Path = { pools: potentialPath };
if (viable3HopPath(path, offerAsset)) {
viablePaths.push(path);
}
}
}
return viablePaths;
}

/**
*
*/
function viable3HopPath(path: Path, offerAsset: NativeAssetInfo): boolean {
if (
(isMatchingAssetInfos(path.pools[0].assets[0].info, offerAsset) ||
isMatchingAssetInfos(path.pools[0].assets[1].info, offerAsset)) &&
!isMatchingAssetInfos(path.pools[1].assets[0].info, offerAsset) &&
!isMatchingAssetInfos(path.pools[1].assets[1].info, offerAsset) &&
(isMatchingAssetInfos(path.pools[2].assets[0].info, offerAsset) ||
isMatchingAssetInfos(path.pools[2].assets[1].info, offerAsset))
) {
const assetInfos = path.pools.flatMap((pool) => {
return pool.assets.map((asset) => {
return isNativeAsset(asset.info) ? asset.info.native_token.denom : asset.info.token.contract_addr;
});
});
const uniqueSet = [...new Set(assetInfos)];
return uniqueSet.length == 3;
} else return false;
}
15 changes: 0 additions & 15 deletions src/core/types/base/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,21 +217,6 @@ function findPoolByInfos(pools: Array<Pool>, infoA: AssetInfo, infoB: AssetInfo)
return matchedPools[0];
}

/**
*
*/
export function routeBetweenPools(poolA: Pool, poolB: Pool): boolean {
const match0 = isMatchingAssetInfos(poolA.assets[0].info, poolB.assets[0].info) ? 1 : 0;
const match1 = isMatchingAssetInfos(poolA.assets[1].info, poolB.assets[0].info) ? 1 : 0;
const match2 = isMatchingAssetInfos(poolA.assets[1].info, poolB.assets[1].info) ? 1 : 0;
const match3 = isMatchingAssetInfos(poolA.assets[0].info, poolB.assets[1].info) ? 1 : 0;

const matched = match0 + match1 + match2 + match3;
return matched == 1;
// there is exactly 1 match, meaning we can travel from A to B through 1 asset. We want to exclude pools with exactly the same 2 assets
// as they will be included in a 2 hop path.
}

/**
*
*/
Expand Down
Loading

0 comments on commit 613462c

Please sign in to comment.