Coinfair Swap Protocol Integration Guide
This document provides guidance on integrating with the Coinfair swap protocol, focusing on how to select the optimal swap path in the frontend.
How to Select the Optimal Swap Path
Pool Types and Fee Combinations
Coinfair supports multiple pool types and fee combinations:
Pool Types (5 types): 1, 2, 3, 4, 5
Fee Types (3 rates): 3, 5, 10
Optimal Path Selection Process
Finding the optimal trading path requires a comprehensive evaluation of all possible combinations:
1. Initialization and Setup
Establish connection to blockchain provider
Initialize Router and Factory contract interfaces
Setup multicall infrastructure for batch queries
Define available pool types, fee rates, and intermediary tokens
2. Direct Path Evaluation
For all 15 combinations of pool types and fee rates:
Create a direct path array:
[TokenA, TokenB]
Create corresponding poolTypePath and feePath arrays
Prepare requests to
Router.getAmountsOut()
orRouter.getAmountsIn()
3. Multi-Hop Path Evaluation
For each intermediary token:
Skip if intermediary is the same as input or output token
For all combinations of first-hop and second-hop parameters:
Create a multi-hop path array:
[TokenA, Intermediary, TokenB]
Create corresponding poolTypePath and feePath arrays with both hops' parameters
Prepare requests to
Router.getAmountsOut()
orRouter.getAmountsIn()
4. Comparison and Selection
For exact input swaps: select the path with maximum output amount
For exact output swaps: select the path with minimum input amount
Consider gas costs when comparing direct vs. multi-hop paths
Return the optimal path information
(path, poolTypePath, feePath, expected amounts)
TypeScript Implementation
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
}
Last updated