import { ethers } from 'ethers';
import { Contract } from '@ethersproject/contracts';
import { Provider } from '@ethersproject/providers';
import { Multicall } from 'ethereum-multicall';
// Configuration constants
const POOL_TYPES = [1, 2, 3, 4, 5];
const FEE_TYPES = [3, 5, 10];
// Intermediate tokens for multi-hop paths
const INTERMEDIARY_TOKENS = [
'0x55d398326f99059fF775485246999027B3197955',
'0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d',
'0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c'
];
// Contract addresses (to be filled with actual addresses)
const ROUTER_ADDRESS = '0x2b5970d94908664aC5023c08FCd48Bf4DBE3E034';
const FACTORY_ADDRESS = '0xBBC73Eaaa789c0Eb860749855B3928f6b851685F';
/**
* PathFinder class for finding optimal swap paths
*/
export class PathFinder {
private router: Contract;
private factory: Contract;
private provider: Provider;
private multicall: Multicall;
constructor(provider: Provider, signer?: ethers.Signer) {
this.provider = provider;
this.router = new Contract(
ROUTER_ADDRESS,
[
'function getAmountsOut(uint amountIn, address[] path, uint8[] poolTypes, uint[] fees) view returns (uint[] amounts, address[] pairs)',
'function getAmountsIn(uint amountOut, address[] path, uint8[] poolTypes, uint[] fees) view returns (uint[] amounts, address[] pairs)'
],
signer || provider
);
this.factory = new Contract(
FACTORY_ADDRESS,
['function getPair(address tokenA, address tokenB, uint8 poolType, uint fee) view returns (address pair)'],
provider
);
this.multicall = new Multicall({ ethersProvider: provider });
}
/**
* Find the best path for a token swap
* @param tokenIn Input token address
* @param tokenOut Output token address
* @param amountIn Input amount (in wei)
* @param exactOutput Whether this is an exact output swap
* @param amountOut Output amount (for exact output swaps)
* @returns Best path information
*/
async findBestPath(
tokenIn: string,
tokenOut: string,
amountIn: ethers.BigNumber,
exactOutput: boolean = false,
amountOut?: ethers.BigNumber
) {
let bestPath = null;
let bestAmountOut = ethers.BigNumber.from(0);
let bestAmountIn = ethers.constants.MaxUint256;
// 1. Check direct paths
const directPathResults = await this.checkDirectPaths(tokenIn, tokenOut, amountIn, exactOutput, amountOut);
if (exactOutput) {
if (directPathResults.bestAmountIn.lt(bestAmountIn)) {
bestAmountIn = directPathResults.bestAmountIn;
bestPath = directPathResults.bestPath;
}
} else {
if (directPathResults.bestAmountOut.gt(bestAmountOut)) {
bestAmountOut = directPathResults.bestAmountOut;
bestPath = directPathResults.bestPath;
}
}
// 2. Check single-hop paths through intermediary tokens
for (const intermediary of INTERMEDIARY_TOKENS) {
// Skip if intermediary is the same as input or output token
if (intermediary === tokenIn || intermediary === tokenOut) continue;
const multiHopResults = await this.checkMultiHopPath(tokenIn, intermediary, tokenOut, amountIn, exactOutput, amountOut);
if (exactOutput) {
if (multiHopResults.bestAmountIn.lt(bestAmountIn)) {
bestAmountIn = multiHopResults.bestAmountIn;
bestPath = multiHopResults.bestPath;
}
} else {
if (multiHopResults.bestAmountOut.gt(bestAmountOut)) {
bestAmountOut = multiHopResults.bestAmountOut;
bestPath = multiHopResults.bestPath;
}
}
}
// If no path found, throw error
if (!bestPath) {
throw new Error("No valid swap path found");
}
return {
...bestPath,
expectedOut: exactOutput ? amountOut : bestAmountOut,
expectedIn: exactOutput ? bestAmountIn : amountIn
};
}
/**
* Check all direct path combinations using multicall
*/
private async checkDirectPaths(
tokenIn: string,
tokenOut: string,
amountIn: ethers.BigNumber,
exactOutput: boolean,
amountOut?: ethers.BigNumber
) {
const multicallContext = [];
// Prepare multicall requests
for (const poolType of POOL_TYPES) {
for (const fee of FEE_TYPES) {
const path = [tokenIn, tokenOut];
const poolTypePath = [poolType];
const feePath = [fee];
multicallContext.push({
reference: `${poolType}-${fee}`,
contractAddress: ROUTER_ADDRESS,
abi: [
exactOutput
? 'function getAmountsIn(uint amountOut, address[] path, uint8[] poolTypes, uint[] fees) view returns (uint[] amounts, address[] pairs)'
: 'function getAmountsOut(uint amountIn, address[] path, uint8[] poolTypes, uint[] fees) view returns (uint[] amounts, address[] pairs)'
],
calls: [
{
reference: 'amounts',
methodName: exactOutput ? 'getAmountsIn' : 'getAmountsOut',
methodParameters: exactOutput
? [amountOut, path, poolTypePath, feePath]
: [amountIn, path, poolTypePath, feePath]
}
]
});
}
}
// Execute multicall
const { results } = await this.multicall.call(multicallContext);
// Process results
let bestPath = null;
let bestAmountOut = ethers.BigNumber.from(0);
let bestAmountIn = ethers.constants.MaxUint256;
for (const poolType of POOL_TYPES) {
for (const fee of FEE_TYPES) {
const key = `${poolType}-${fee}`;
if (results[key] && results[key].success) {
const [amounts, pairs] = results[key].callsReturnContext[0].returnValues;
if (exactOutput) {
// For exact output, we want to minimize input amount
const inputAmount = ethers.BigNumber.from(amounts[0].hex);
if (inputAmount.lt(bestAmountIn)) {
bestAmountIn = inputAmount;
bestPath = {
path: [tokenIn, tokenOut],
poolTypePath: [poolType],
feePath: [fee]
};
}
} else {
// For exact input, we want to maximize output amount
const outputAmount = ethers.BigNumber.from(amounts[1].hex);
if (outputAmount.gt(bestAmountOut)) {
bestAmountOut = outputAmount;
bestPath = {
path: [tokenIn, tokenOut],
poolTypePath: [poolType],
feePath: [fee]
};
}
}
}
}
}
return { bestPath, bestAmountOut, bestAmountIn };
}
/**
* Check all multi-hop path combinations for a specific intermediary token
*/
private async checkMultiHopPath(
tokenIn: string,
intermediary: string,
tokenOut: string,
amountIn: ethers.BigNumber,
exactOutput: boolean,
amountOut?: ethers.BigNumber
) {
const multicallContext = [];
// Prepare multicall requests for all combinations
for (const firstPoolType of POOL_TYPES) {
for (const firstFee of FEE_TYPES) {
for (const secondPoolType of POOL_TYPES) {
for (const secondFee of FEE_TYPES) {
const path = [tokenIn, intermediary, tokenOut];
const poolTypePath = [firstPoolType, secondPoolType];
const feePath = [firstFee, secondFee];
multicallContext.push({
reference: `${firstPoolType}-${firstFee}-${secondPoolType}-${secondFee}`,
contractAddress: ROUTER_ADDRESS,
abi: [
exactOutput
? 'function getAmountsIn(uint amountOut, address[] path, uint8[] poolTypes, uint[] fees) view returns (uint[] amounts, address[] pairs)'
: 'function getAmountsOut(uint amountIn, address[] path, uint8[] poolTypes, uint[] fees) view returns (uint[] amounts, address[] pairs)'
],
calls: [
{
reference: 'amounts',
methodName: exactOutput ? 'getAmountsIn' : 'getAmountsOut',
methodParameters: exactOutput
? [amountOut, path, poolTypePath, feePath]
: [amountIn, path, poolTypePath, feePath]
}
]
});
}
}
}
}
// Execute multicall
const { results } = await this.multicall.call(multicallContext);
// Process results
let bestPath = null;
let bestAmountOut = ethers.BigNumber.from(0);
let bestAmountIn = ethers.constants.MaxUint256;
for (const firstPoolType of POOL_TYPES) {
for (const firstFee of FEE_TYPES) {
for (const secondPoolType of POOL_TYPES) {
for (const secondFee of FEE_TYPES) {
const key = `${firstPoolType}-${firstFee}-${secondPoolType}-${secondFee}`;
if (results[key] && results[key].success) {
const [amounts, pairs] = results[key].callsReturnContext[0].returnValues;
if (exactOutput) {
// For exact output, we want to minimize input amount
const inputAmount = ethers.BigNumber.from(amounts[0].hex);
if (inputAmount.lt(bestAmountIn)) {
bestAmountIn = inputAmount;
bestPath = {
path: [tokenIn, intermediary, tokenOut],
poolTypePath: [firstPoolType, secondPoolType],
feePath: [firstFee, secondFee]
};
}
} else {
// For exact input, we want to maximize output amount
const outputAmount = ethers.BigNumber.from(amounts[2].hex);
if (outputAmount.gt(bestAmountOut)) {
bestAmountOut = outputAmount;
bestPath = {
path: [tokenIn, intermediary, tokenOut],
poolTypePath: [firstPoolType, secondPoolType],
feePath: [firstFee, secondFee]
};
}
}
}
}
}
}
}
return { bestPath, bestAmountOut, bestAmountIn };
}
}
/**
* Swap executor for performing token swaps
*/
export class SwapExecutor {
private router: Contract;
constructor(signer: ethers.Signer) {
this.router = new Contract(
ROUTER_ADDRESS,
[
// Exact input swaps
'function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, uint8[] calldata poolTypePath, uint[] calldata feePath, address to, uint deadline) external returns (uint[] memory amounts)',
'function swapExactETHForTokens(uint amountOutMin, address[] calldata path, uint8[] calldata poolTypePath, uint[] calldata feePath, address to, uint deadline) external payable returns (uint[] memory amounts)',
'function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, uint8[] calldata poolTypePath, uint[] calldata feePath, address to, uint deadline) external returns (uint[] memory amounts)',
// Exact output swaps
'function swapTokensForExactTokens(uint amountOut, uint amountInMax, address[] calldata path, uint8[] calldata poolTypePath, uint[] calldata feePath, address to, uint deadline) external returns (uint[] memory amounts)',
'function swapETHForExactTokens(uint amountOut, address[] calldata path, uint8[] calldata poolTypePath, uint[] calldata feePath, address to, uint deadline) external payable returns (uint[] memory amounts)',
'function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, uint8[] calldata poolTypePath, uint[] calldata feePath, address to, uint deadline) external returns (uint[] memory amounts)',
// Swaps supporting fee on transfer
'function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, uint8[] calldata poolTypePath, uint[] calldata feePath, address to, uint deadline) external',
'function swapExactETHForTokensSupportingFeeOnTransferTokens(uint amountOutMin, address[] calldata path, uint8[] calldata poolTypePath, uint[] calldata feePath, address to, uint deadline) external payable',
'function swapExactTokensForETHSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, uint8[] calldata poolTypePath, uint[] calldata feePath, address to, uint deadline) external'
],
signer
);
}
// Swap execution methods would be implemented here
}