import axios from 'axios'; import { OpenMulticall__factory, IUniswapV2Pair__factory, OpenMulticall } from './typechain'; import { Deployment } from './config'; import { AbiCoder, Contract, formatEther, getAddress, id, Log, Provider } from 'ethers'; import { allRpcs, getArrayEnd, log, printProgressBar, retry, sleep } from './libs'; import { _getV3AmountOut, getPriceFromSqrtPriceX96, getSqrtRatioAtTick } from './libs/tick'; const coder = new AbiCoder(); const OpenMulticallInterface = OpenMulticall__factory.createInterface(); function addressToBuffer(address: string) { return Buffer.from(address.slice(2), 'hex'); } function bufferToAddress(address: Buffer) { return getAddress('0x' + address.toString('hex')); } function getLogsJson(fromBlock: number, toBlock: number, topic: string) { return { jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [ { fromBlock: `0x${fromBlock.toString(16)}`, toBlock: `0x${toBlock.toString(16)}`, topics: [topic], }, ], }; } function getLogData(poolLog: Log) { return ( (poolLog.topics[1] ?? '0x') + (poolLog.topics[2] ?? '').replace('0x', '') + (poolLog.topics[3] ?? '').replace('0x', '') + poolLog.data.replace('0x', '') ); } interface PoolLog { token0: Buffer; token1: Buffer; address: Buffer; type: number; // 0:v2 // 1:v3-100 // 2:v3-500 // 3:v3-3000 // 4:v3-10000 } export class OpenSwapCilent { rpcUrls: string[] = []; provider: Provider; chainId?: number; gasMax = 5000000000n; multicallGasLimit = 1000000n; eventBlockStart = 625; eventBlockEnd = 10000; openMulticall?: OpenMulticall; constructor(provider: Provider) { this.provider = provider; } async pre() { this.chainId = Number(await this.provider.getNetwork().then((network) => network.chainId)); this.openMulticall = OpenMulticall__factory.connect( Deployment[this.chainId!].OpenMulticall.target, this.provider ); allRpcs[this.chainId].rpcs.forEach((url) => { let rpcUrl; if (typeof url === 'string') { rpcUrl = url; } else { rpcUrl = url.url; } if (rpcUrl.indexOf('http') !== -1) { this.rpcUrls.push(rpcUrl); } }); log('rpcUrls', this.rpcUrls.length); } async getMulticallDataList(multicallList: { target: string; callData: string }[]) { const multicallRpcs = await this.getMulticallRpcs(); return await this.getRpcsData( multicallRpcs, multicallList, (url: string, call: { target: string; callData: string }[]) => this.tryAggregate(url, false, 0n, call) ); } async getV2DataList(poolAddressList: string[]) { const IUniswapV2PairInterface = IUniswapV2Pair__factory.createInterface(); const getReservesData = IUniswapV2PairInterface.encodeFunctionData('getReserves'); const multicallList = poolAddressList.map((poolAddress) => ({ target: poolAddress, callData: getReservesData, })); const multicallDataList = await this.getMulticallDataList(multicallList); return multicallDataList.map((multicallData) => { const reservesData = IUniswapV2PairInterface.decodeFunctionResult( 'getReserves', multicallData.returnData ); return { reserve0: reservesData[0], reserve1: reservesData[1], }; }); } async getV3DataList(poolAddressList: string[]) { const bitMapLength = 1000; const multicallList = poolAddressList.map((poolAddress) => ({ target: Deployment[this.chainId!].OpenMulticall.target, callData: OpenMulticallInterface.encodeFunctionData('getV3PoolData', [ poolAddress, -bitMapLength, bitMapLength, ]), })); const multicallDataList = await this.getMulticallDataList(multicallList); return multicallDataList.map((multicallData) => { const reservesData = OpenMulticallInterface.decodeFunctionResult( 'getV3PoolData', multicallData.returnData ); return { slot0: { sqrtPriceX96: reservesData[0][0] as bigint, tick: Number(reservesData[0][1]), }, tickDatas: reservesData[1].map((tickData: any) => ({ tick: Number(tickData[0]), liquidityNet: tickData[1], })) as { tick: number; liquidityNet: bigint; }[], liquidity: reservesData[2] as bigint, fee: reservesData[3] as bigint, }; }); } async getPoolLogsData(fromBlock: number, toBlock: number) { const eventRpcs = await this.getEventRpcs(); return await this.getRpcsData( eventRpcs, Array.from({ length: toBlock - fromBlock + 1 }, (_, i) => fromBlock + i), (url: string, call: number[]) => this.getPoolLogs(url, call[0], getArrayEnd(call)) ); } getV3AmountOut( amountIn: bigint, v3Data: { tickDatas: { tick: number; liquidityNet: bigint; }[]; slot0: { sqrtPriceX96: bigint; tick: number; }; liquidity: bigint; fee: bigint; }, zeroForOne: boolean ) { return _getV3AmountOut(amountIn, v3Data, zeroForOne); } async quoteV3AmountOut(amountIn: bigint, poolAddress: string, zeroForOne: boolean) { const quoterAbi = [ `function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96)) external returns (uint256 amountOut,uint160 sqrtPriceX96After,uint32 initializedTicksCrossed,uint256 gasEstimate)`, ]; const poolAbi = [ 'function token0() external view returns (address)', 'function token1() external view returns (address)', 'function fee() external view returns (uint24)', ]; const poolContract = new Contract(poolAddress, poolAbi, this.provider); const token0 = await poolContract.token0.staticCall(); const token1 = await poolContract.token1.staticCall(); await sleep(1000); const fee = await poolContract.fee.staticCall(); const quoterContract = new Contract( '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', quoterAbi, this.provider ); return ( await quoterContract.quoteExactInputSingle.staticCall([ zeroForOne ? token0 : token1, zeroForOne ? token1 : token0, amountIn, fee, 0, ]) )[0]; } getV2AmountOut( amountIn: bigint, v2Data: { reserve0: bigint; reserve1: bigint }, zeroForOne: boolean ) { const amountInWithFee = amountIn * 997n; const numerator = amountInWithFee * (zeroForOne ? v2Data.reserve0 : v2Data.reserve1); const denominator = (zeroForOne ? v2Data.reserve1 : v2Data.reserve0) * 1000n + amountInWithFee; return numerator / denominator; } getV3PriceList(v3Data: { tickDatas: { tick: number; liquidityNet: bigint; }[]; slot0: { sqrtPriceX96: bigint; tick: number; }; liquidity: bigint; fee: bigint; }) { const priceDatas = v3Data.tickDatas.map((tickData) => ({ sqrtPriceX96: getPriceFromSqrtPriceX96(getSqrtRatioAtTick(tickData.tick), 18, 18), liquidityNet: formatEther(tickData.liquidityNet), })); return priceDatas; } async getMulticallRpcs() { const multicallRpcs = ( await Promise.all( this.rpcUrls.map(async (rpcUrl) => { try { let gasLeft = BigInt( await this.openMulticallPost( rpcUrl, OpenMulticallInterface.encodeFunctionData('getGasLeft') ) ); if (gasLeft > this.gasMax) { gasLeft = this.gasMax; } let max = Number((gasLeft * 4n) / this.multicallGasLimit / 5n); return { url: rpcUrl, max }; } catch (error) { return null; } }) ) ).filter((rpc) => rpc !== null); log('getMulticallRpcs', multicallRpcs.length); return multicallRpcs; } async tryAggregate( rpcUrl: string, requireSuccess: boolean, gasLimit: bigint, multicallList: OpenMulticall.CallStruct[] ) { return OpenMulticallInterface.decodeFunctionResult( 'tryAggregate', await this.openMulticallPost( rpcUrl, OpenMulticallInterface.encodeFunctionData('tryAggregate', [ requireSuccess, gasLimit, multicallList, ]) ) )[0] as OpenMulticall.ResultStructOutput[]; } async openMulticallPost(rpcUrl: string, data: string) { return ( await axios.post(rpcUrl, { jsonrpc: '2.0', id: 1, method: 'eth_call', params: [ { to: Deployment[this.chainId!].OpenMulticall.target, data, }, 'latest', ], }) ).data.result; } async getPoolLogs(rpcUrl: string, fromBlock: number, toBlock: number) { const pairCreatedTopic = id('PairCreated(address,address,address,uint256)'); const poolCreatedTopic = id('PoolCreated(address,address,uint24,int24,address)'); return ( await axios .post( rpcUrl, [pairCreatedTopic, poolCreatedTopic].map((topic) => getLogsJson(fromBlock, toBlock, topic) ) ) .then((res) => { return (res.data as { result: Log[] }[]).flatMap((e) => e.result); }) ).map((poolLog) => { if (poolLog.topics[0] === pairCreatedTopic) { const decodedData = coder.decode( [ 'address', // token0 'address', // token1 'address', // pair 'uint256', // length ], getLogData(poolLog) ) as string[]; return { token0: addressToBuffer(decodedData[0]), token1: addressToBuffer(decodedData[1]), address: addressToBuffer(decodedData[2]), type: 0, } as PoolLog; } else if (poolLog.topics[0] === poolCreatedTopic) { const decodedData = coder.decode( [ 'address', // token0 'address', // token1 'uint24', // fee 'int24', // tickSpacing 'address', // pool ], getLogData(poolLog) ) as string[]; let type; const fee = BigInt(decodedData[2]); if (fee === 100n) { type = 1; } else if (fee === 500n) { type = 2; } else if (fee === 3000n) { type = 3; } else if (fee === 10000n) { type = 4; } return { token0: addressToBuffer(decodedData[0]), token1: addressToBuffer(decodedData[1]), address: addressToBuffer(decodedData[4]), type, } as PoolLog; } throw 'error poolLog'; }); } async getAllPoolLogs(){ const fromBlock = await this.provider.getBlockNumber(); const toBlock = fromBlock - 1000000; return await this.getPoolLogsData(toBlock, fromBlock); } async getEventRpcs() { const blockNumber = Number(await this.openMulticall!.createBlock()); const rpcs = ( await Promise.all( this.rpcUrls.map(async (rpcUrl) => { const rpc = { url: rpcUrl, max: 0, }; let block = this.eventBlockStart; for (; block <= this.eventBlockEnd; block *= 2) { try { await Promise.race([ axios .post(rpc.url, getLogsJson(blockNumber - block, blockNumber, id('Create()'))) .then((res) => { if (res.data.result.length > 0) { rpc.max = block; } }), sleep(10 * 1000).then(() => null), ]); } catch (error) { break; } } return rpc; }) ) ).filter((rpc) => rpc.max !== 0); log('getEventRpcs', rpcs.length); return rpcs; } async getRpcsData( rpcs: { max: number; url: string }[], callList: C[], func: (url: string, call: C[]) => Promise ) { const statusList: { start: number; end: number; index: number; data: T[] | undefined | null; }[] = []; await Promise.all( rpcs.map(async (rpc) => { while (true) { let start: number | undefined; let end: number; let index: number; const waitList = statusList.filter((status) => status.data === null); for (const wait of waitList) { if (rpc.max >= wait.end - wait.start) { start = wait.start; end = wait.end; index = wait.index; break; } } if (!start) { if (statusList.length === 0) { start = 0; } else { start = getArrayEnd(statusList).end; } if (start >= callList.length) { break; } end = Math.min(start + rpc.max, callList.length); index = statusList.length; statusList.push({ start, end, index, data: undefined }); } try { const data = await retry( () => func(rpc.url, callList.slice(start, end!)), () => {}, 3 ); statusList[index!].data = data; let okLength = 0; statusList.filter((status) => status.data).forEach((status) => { okLength+= status.end - status.start; }); printProgressBar(okLength, callList.length); } catch (error) { statusList[index!].data = null; break; } } }) ); return statusList.reduce((result, item) => [...result, ...item.data!], []); } }