UNPKG

four-flap-meme-sdk

Version:

SDK for Flap bonding curve and four.meme TokenManager

791 lines (790 loc) 34 kB
import { Contract, JsonRpcProvider, Interface, formatUnits } from 'ethers'; import { ADDRESSES, ZERO_ADDRESS } from './constants.js'; import { MULTICALL3_ABI, V2_FACTORY_ABI, V2_PAIR_ABI, V3_FACTORY_ABI, ERC20_ABI } from '../abis/common.js'; import { Helper3 } from '../contracts/helper3.js'; import { FlapPortal } from '../flap/portal.js'; // ============================================================================ // 链配置 // ============================================================================ /** 默认 V3 费率档位(合并 PancakeSwap 和 Uniswap 的所有档位) */ const DEFAULT_V3_FEE_TIERS = [100, 500, 2500, 3000, 10000]; /** 链 DEX 配置 */ const CHAIN_DEX_CONFIGS = { BSC: { wrappedNative: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', wrappedNativeSymbol: 'WBNB', stableCoins: [ { address: '0x55d398326f99059fF775485246999027B3197955', symbol: 'USDT', decimals: 18 }, { address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', symbol: 'USDC', decimals: 18 }, ], dexes: { PANCAKESWAP: { name: 'PancakeSwap', v2Factory: '0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73', v3Factory: '0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865', enabled: true, } } }, MONAD: { wrappedNative: '0x3bd359c1119da7da1d913d1c4d2b7c461115433a', wrappedNativeSymbol: 'WMON', stableCoins: [ { address: '0x754704bc059f8c67012fed69bc8a327a5aafb603', symbol: 'USDC', decimals: 6 }, ], dexes: { PANCAKESWAP: { name: 'PancakeSwap', v2Factory: '0x44c90b66eb4c4f814cea6aaf2ac71ca88fa77308', v3Factory: '0x40ce898c43df68c8c4229cd5ce21d3ce1f3ddcf1', enabled: true, }, UNISWAP: { name: 'Uniswap', v2Factory: '0x182a927119d56008d921126764bf884221b10f59', v3Factory: '0x204faca1764b154221e35c0d20abb3c525710498', v2Router: '0x4b2ab38dbf28d31d467aa8993f6c2585981d6804', v3Router: '0xd6145b2d3f379919e8cdeda7b97e37c4b2ca9c40', quoterV2: '0x661e93cca42afacb172121ef892830ca3b70f08d', enabled: true, } } }, // ✅ 新增 XLayer 配置 XLAYER: { wrappedNative: '0xe538905cf8410324e03a5a23c1c177a474d59b2b', wrappedNativeSymbol: 'WOKB', stableCoins: [ { address: '0x1e4a5963abfd975d8c9021ce480b42188849d41d', symbol: 'USDT', decimals: 6 }, { address: '0x74b7f16337b8972027f6196a17a631ac6de26d22', symbol: 'USDC', decimals: 6 }, ], dexes: { POTATOSWAP: { name: 'PotatoSwap', v2Factory: '0x630DB8E822805c82Ca40a54daE02dd5aC31f7fcF', // V2 Factory v2Router: '0x881fb2f98c13d521009464e7d1cbf16e1b394e8e', // ✅ V2 Router (标准 Uniswap V2 风格) v3Router: '0xB45D0149249488333E3F3f9F359807F4b810C1FC', // SwapRouter02 (V3 风格) v3Factory: '0xa1415fAe79c4B196d087F02b8aD5a622B8A827E5', // V3 Factory quoterV2: '0x5A6f3723346aF54a4D0693bfC1718D64d4915C3e', // QuoterV2 enabled: true, }, DYORSWAP: { name: 'DYORSwap', v2Factory: '0x2CcaDb1e437AA9cDc741574bDa154686B1F04C09', // ✅ V2 Factory v2Router: '0xfb001fbbace32f09cb6d3c449b935183de53ee96', // DYORSwap Router (SwapRouter02 风格) enabled: true, // ✅ 已启用 } } } }; // ============================================================================ // 动态配置 API // ============================================================================ /** 运行时配置存储(内存) */ const runtimeConfigs = {}; /** * 从 Router 合约获取 Factory 地址 * 支持 Uniswap V2 Router 和 V3 SwapRouter02 */ export async function getFactoryFromRouter(routerAddress, rpcUrl, routerType = 'v2') { const provider = new JsonRpcProvider(rpcUrl); const result = {}; // V2 Router ABI: factory() const V2_ROUTER_ABI = ['function factory() view returns (address)']; // V3 SwapRouter02 ABI: factory() 和 factoryV2() const V3_ROUTER_ABI = [ 'function factory() view returns (address)', 'function factoryV2() view returns (address)', ]; try { if (routerType === 'v2') { const router = new Contract(routerAddress, V2_ROUTER_ABI, provider); result.v2Factory = await router.factory(); } else { // V3 SwapRouter02 可能同时有 V2 和 V3 Factory const router = new Contract(routerAddress, V3_ROUTER_ABI, provider); // ✅ 并行获取 V3 和 V2 Factory const [v3Factory, v2Factory] = await Promise.all([ router.factory().catch(() => undefined), router.factoryV2().catch(() => undefined) ]); if (v3Factory) result.v3Factory = v3Factory; if (v2Factory) result.v2Factory = v2Factory; } } catch (error) { console.error(`❌ 获取 Factory 地址失败:`, error); } return result; } /** * 动态注册 DYORSwap(从 Router 获取 Factory) */ export async function registerDYORSwap(rpcUrl) { const DYORSWAP_ROUTER = '0xfb001fbbace32f09cb6d3c449b935183de53ee96'; try { // 从 Router 获取 Factory 地址 const factories = await getFactoryFromRouter(DYORSWAP_ROUTER, rpcUrl, 'v3'); if (factories.v2Factory || factories.v3Factory) { const chainConfig = getChainConfig('XLAYER'); if (chainConfig && chainConfig.dexes.DYORSWAP) { if (factories.v2Factory) { chainConfig.dexes.DYORSWAP.v2Factory = factories.v2Factory; } if (factories.v3Factory) { chainConfig.dexes.DYORSWAP.v3Factory = factories.v3Factory; } chainConfig.dexes.DYORSWAP.enabled = true; return true; } } return false; } catch (error) { console.error('❌ 注册 DYORSwap 失败:', error); return false; } } /** 注册新的 DEX */ export function registerDex(chain, dexKey, config) { const chainConfig = getChainConfig(chain); if (chainConfig) { chainConfig.dexes[dexKey] = config; } } /** 注册新的稳定币 */ export function registerStableCoin(chain, coin) { const chainConfig = getChainConfig(chain); if (chainConfig) { // 检查是否已存在 const exists = chainConfig.stableCoins.some(c => c.address.toLowerCase() === coin.address.toLowerCase()); if (!exists) { chainConfig.stableCoins.push(coin); } } } /** 获取链配置(优先运行时配置) */ export function getChainConfig(chain) { if (runtimeConfigs[chain]) { return runtimeConfigs[chain]; } if (CHAIN_DEX_CONFIGS[chain]) { // 深拷贝到运行时配置 runtimeConfigs[chain] = JSON.parse(JSON.stringify(CHAIN_DEX_CONFIGS[chain])); return runtimeConfigs[chain]; } return undefined; } /** 获取支持的 DEX 列表 */ export function getSupportedDexes(chain) { const config = getChainConfig(chain); if (!config) return []; return Object.entries(config.dexes).map(([key, dex]) => ({ key, name: dex.name, enabled: dex.enabled, })); } /** 获取支持的稳定币列表 */ export function getSupportedStableCoins(chain) { const config = getChainConfig(chain); if (!config) return []; return config.stableCoins; } /** 获取所有报价代币(包装原生 + 稳定币) */ export function getQuoteTokens(chain) { const config = getChainConfig(chain); if (!config) return []; return [ { address: config.wrappedNative, symbol: config.wrappedNativeSymbol, decimals: 18, isNative: true }, ...config.stableCoins.map(c => ({ ...c, isNative: false })), ]; } // ============================================================================ // ABI 别名(从公共模块导入) // ============================================================================ const I_UNIV2_FACTORY_ABI = V2_FACTORY_ABI; const I_UNIV2_PAIR_ABI = V2_PAIR_ABI; const I_ERC20_ABI = ERC20_ABI; const I_UNIV3_FACTORY_ABI = V3_FACTORY_ABI; // ============================================================================ // 常量(从公共模块导入) // ============================================================================ const MULTICALL3_ADDRESS = ADDRESSES.BSC.Multicall3; /** Provider 缓存 */ const providerCache = new Map(); function getProvider(rpcUrl) { if (!providerCache.has(rpcUrl)) { providerCache.set(rpcUrl, new JsonRpcProvider(rpcUrl)); } return providerCache.get(rpcUrl); } /** 精度缓存 */ const decimalsCache = new Map(); async function getTokenDecimals(tokenAddress, provider) { const key = tokenAddress.toLowerCase(); if (decimalsCache.has(key)) { return decimalsCache.get(key); } try { const contract = new Contract(tokenAddress, I_ERC20_ABI, provider); const decimals = await contract.decimals(); const result = Number(decimals); decimalsCache.set(key, result); return result; } catch { return 18; } } /** 格式化余额 */ function formatBalance(balance, format, decimals = 18) { return format ? formatUnits(balance, decimals) : balance.toString(); } /** 标准化流动性值(转换为统一精度用于比较) */ function normalizeLiquidity(amount, decimals) { // 将所有值标准化到 18 位精度进行比较 if (decimals < 18) { return amount * BigInt(10 ** (18 - decimals)); } else if (decimals > 18) { return amount / BigInt(10 ** (decimals - 18)); } return amount; } // ============================================================================ // 主查询函数 // ============================================================================ export async function inspectTokenLP(token, opts) { const provider = getProvider(opts.rpcUrl); const shouldFormat = opts.formatBalances !== false; const multicall3 = opts.multicall3 || MULTICALL3_ADDRESS; const v3FeeTiers = opts.v3FeeTiers || DEFAULT_V3_FEE_TIERS; // 初始化返回结构 const result = { platform: 'UNKNOWN', allPools: [], bestPools: [], }; // 获取链配置 let chainConfig = getChainConfig(opts.chain); if (!chainConfig) { console.warn(`[LP Inspect] Unknown chain: ${opts.chain}`); return result; } // 应用自定义配置 if (opts.customDexConfig) { chainConfig = { ...chainConfig, ...opts.customDexConfig }; } // 查询代币精度和总供应量 let tokenDecimals; let totalSupplyRaw; try { const tokenContract = new Contract(token, [ 'function decimals() view returns (uint8)', 'function totalSupply() view returns (uint256)' ], provider); const [decimals, supply] = await Promise.all([ tokenContract.decimals().catch(() => 18), tokenContract.totalSupply().catch(() => undefined) ]); tokenDecimals = Number(decimals); totalSupplyRaw = supply ? BigInt(supply) : undefined; result.decimals = tokenDecimals; if (totalSupplyRaw !== undefined) { result.totalSupplyRaw = totalSupplyRaw; result.totalSupply = formatBalance(totalSupplyRaw, shouldFormat, tokenDecimals); } } catch { tokenDecimals = 18; } // ======================================== // 1. 内盘检测:Flap // ======================================== if (opts.flapChain) { try { const flap = new FlapPortal({ chain: opts.flapChain, rpcUrl: opts.rpcUrl }); const st = await flap.getTokenV5(token); if (st && st.status !== undefined && Number(st.status) !== 4) { result.platform = 'FLAP'; let quoteDecimals = 18; let quoteSymbol = chainConfig.wrappedNativeSymbol; if (st.quoteTokenAddress && st.quoteTokenAddress.toLowerCase() !== ZERO_ADDRESS) { quoteDecimals = await getTokenDecimals(st.quoteTokenAddress, provider); // 查找符号 const stableCoin = chainConfig.stableCoins.find(c => c.address.toLowerCase() === st.quoteTokenAddress.toLowerCase()); quoteSymbol = stableCoin?.symbol || 'TOKEN'; } result.flap = { quoteToken: st.quoteTokenAddress, quoteSymbol, quoteDecimals, reserveNative: formatBalance(st.reserve, shouldFormat, quoteDecimals), circulatingSupply: formatBalance(st.circulatingSupply, shouldFormat, tokenDecimals ?? 18), price: formatBalance(st.price, shouldFormat), }; return result; } } catch (err) { if (opts.debug) console.log('[LP Inspect] Flap check failed:', err); } } // ======================================== // 2. 内盘检测:Four.meme(仅 BSC) // ======================================== if (opts.chain === 'BSC') { try { const helper = Helper3.connectByChain('BSC', opts.rpcUrl); const info = await helper.getTokenInfo(token); if (info) { const tokenManager = info.tokenManager; const versionNum = Number(info.version ?? 0); const liquidityAdded = Boolean(info.liquidityAdded); const isValid = tokenManager && tokenManager !== ZERO_ADDRESS && (versionNum === 1 || versionNum === 2); if (isValid && !liquidityAdded) { result.platform = 'FOUR'; const reserveNative = info.funds !== undefined ? formatBalance(info.funds, shouldFormat) : undefined; const offerTokens = info.offers !== undefined ? formatBalance(info.offers, shouldFormat) : undefined; const lastPrice = info.lastPrice !== undefined ? formatBalance(info.lastPrice, shouldFormat) : undefined; result.four = { helper: ADDRESSES.BSC.TokenManagerHelper3, reserveNative, offerTokens, lastPrice }; return result; } } } catch (err) { if (opts.debug) console.log('[LP Inspect] Four.meme check failed:', err); } } // ======================================== // 3. 外盘检测:多 DEX 批量查询 // ======================================== const enabledDexes = Object.entries(chainConfig.dexes).filter(([_, dex]) => dex.enabled); // 构建报价代币列表 const quoteTokens = [ { address: chainConfig.wrappedNative, symbol: chainConfig.wrappedNativeSymbol, decimals: 18 }, ...chainConfig.stableCoins, ]; // 所有最佳池子候选 const allBestPoolCandidates = []; // 并行查询所有 DEX const dexPromises = enabledDexes.map(async ([dexKey, dexConfig]) => { const dexPoolInfo = { dexName: dexConfig.name, dexKey, v2Pairs: [], v3Pools: [], }; // V2 查询 if (dexConfig.v2Factory) { try { const v2Pairs = await queryV2Pairs(token, dexConfig.v2Factory, quoteTokens, provider, multicall3, tokenDecimals ?? 18, shouldFormat, opts.debug); dexPoolInfo.v2Pairs = v2Pairs; // 添加到最佳池子候选 for (const pair of v2Pairs) { if (pair.reserveQuoteRaw && pair.reserveQuoteRaw > 0n) { allBestPoolCandidates.push({ dex: dexConfig.name, dexKey, version: 'v2', quoteToken: pair.quoteToken, quoteSymbol: pair.quoteSymbol, quoteDecimals: pair.quoteDecimals, pairAddress: pair.pairAddress, liquidity: pair.reserveQuote, liquidityRaw: normalizeLiquidity(pair.reserveQuoteRaw, pair.quoteDecimals), reserveToken: pair.reserveToken, // ✅ 新增 reserveTokenRaw: pair.reserveTokenRaw, // ✅ 新增 }); } } } catch (err) { if (opts.debug) console.log(`[LP Inspect] ${dexConfig.name} V2 query failed:`, err); } } // V3 查询 if (dexConfig.v3Factory) { try { const v3Pools = await queryV3Pools(token, dexConfig.v3Factory, quoteTokens, v3FeeTiers, provider, multicall3, tokenDecimals ?? 18, shouldFormat, opts.debug); dexPoolInfo.v3Pools = v3Pools; // 添加到最佳池子候选 for (const pool of v3Pools) { if (pool.reserveQuoteRaw && pool.reserveQuoteRaw > 0n) { allBestPoolCandidates.push({ dex: dexConfig.name, dexKey, version: 'v3', fee: pool.fee, quoteToken: pool.quoteToken, quoteSymbol: pool.quoteSymbol, quoteDecimals: pool.quoteDecimals, pairAddress: pool.pairAddress, liquidity: pool.reserveQuote, liquidityRaw: normalizeLiquidity(pool.reserveQuoteRaw, pool.quoteDecimals), reserveToken: pool.reserveToken, // ✅ 新增 reserveTokenRaw: pool.reserveTokenRaw, // ✅ 新增 }); } } } catch (err) { if (opts.debug) console.log(`[LP Inspect] ${dexConfig.name} V3 query failed:`, err); } } return dexPoolInfo; }); // 等待所有 DEX 查询完成 const dexResults = await Promise.all(dexPromises); // 过滤出有流动性的 DEX result.allPools = dexResults.filter(dex => dex.v2Pairs.length > 0 || dex.v3Pools.length > 0); // 按流动性排序最佳池子,并计算 poolRatio result.bestPools = allBestPoolCandidates .sort((a, b) => { // 降序排列 if (b.liquidityRaw > a.liquidityRaw) return 1; if (b.liquidityRaw < a.liquidityRaw) return -1; return 0; }) .slice(0, 10) // 最多返回 10 个 .map(pool => { // ✅ 计算池子代币占比 if (pool.reserveTokenRaw && totalSupplyRaw && totalSupplyRaw > 0n) { // 计算百分比:(reserveTokenRaw / totalSupplyRaw) * 100 // 为了保持精度,先乘以 10000,再除以 totalSupply,得到万分比 const ratioBps = (pool.reserveTokenRaw * 10000n) / totalSupplyRaw; const ratioPercent = Number(ratioBps) / 100; // 转为百分比 pool.poolRatio = `${ratioPercent.toFixed(2)}%`; } return pool; }); // ✅ 同时为 allPools 中的每个池子计算 poolRatio if (totalSupplyRaw && totalSupplyRaw > 0n) { for (const dexPool of result.allPools) { for (const pair of dexPool.v2Pairs) { if (pair.reserveTokenRaw) { const ratioBps = (pair.reserveTokenRaw * 10000n) / totalSupplyRaw; const ratioPercent = Number(ratioBps) / 100; pair.poolRatio = `${ratioPercent.toFixed(2)}%`; } } for (const pool of dexPool.v3Pools) { if (pool.reserveTokenRaw) { const ratioBps = (pool.reserveTokenRaw * 10000n) / totalSupplyRaw; const ratioPercent = Number(ratioBps) / 100; pool.poolRatio = `${ratioPercent.toFixed(2)}%`; } } } } // 确定平台类型 result.platform = determinePlatform(result.allPools); return result; } // ============================================================================ // V2 批量查询 // ============================================================================ async function queryV2Pairs(token, factoryAddress, quoteTokens, provider, multicall3, tokenDecimals, shouldFormat, debug) { const results = []; const factoryIface = new Interface(I_UNIV2_FACTORY_ABI); const pairIface = new Interface(I_UNIV2_PAIR_ABI); const mcIface = new Interface(MULTICALL3_ABI); // 第一步:批量查询所有 getPair const pairCalls = quoteTokens.map(qt => ({ target: factoryAddress, callData: factoryIface.encodeFunctionData('getPair', [token, qt.address]), quoteToken: qt, })); try { const aggData = mcIface.encodeFunctionData('aggregate', [ pairCalls.map(c => ({ target: c.target, callData: c.callData })) ]); const res = await provider.call({ to: multicall3, data: aggData }); const [, returnData] = mcIface.decodeFunctionResult('aggregate', res); // 解析 pair 地址 const validPairs = []; for (let i = 0; i < pairCalls.length; i++) { try { const decoded = factoryIface.decodeFunctionResult('getPair', returnData[i]); const pairAddress = decoded[0]; if (pairAddress && pairAddress.toLowerCase() !== ZERO_ADDRESS) { validPairs.push({ pairAddress, quoteToken: pairCalls[i].quoteToken }); } } catch { // 解码失败,跳过 } } if (validPairs.length === 0) return results; // 第二步:批量查询 getReserves const reserveCalls = validPairs.flatMap(p => [ { target: p.pairAddress, callData: pairIface.encodeFunctionData('token0', []) }, { target: p.pairAddress, callData: pairIface.encodeFunctionData('token1', []) }, { target: p.pairAddress, callData: pairIface.encodeFunctionData('getReserves', []) }, ]); const aggData2 = mcIface.encodeFunctionData('aggregate', [reserveCalls]); const res2 = await provider.call({ to: multicall3, data: aggData2 }); const [, returnData2] = mcIface.decodeFunctionResult('aggregate', res2); // 解析储备量 for (let i = 0; i < validPairs.length; i++) { try { const idx = i * 3; const token0 = pairIface.decodeFunctionResult('token0', returnData2[idx])[0]; const token1 = pairIface.decodeFunctionResult('token1', returnData2[idx + 1])[0]; const [r0, r1] = pairIface.decodeFunctionResult('getReserves', returnData2[idx + 2]); const p = validPairs[i]; const isToken0 = token0.toLowerCase() === token.toLowerCase(); const reserveToken = isToken0 ? r0 : r1; const reserveQuote = isToken0 ? r1 : r0; results.push({ quoteToken: p.quoteToken.address, quoteSymbol: p.quoteToken.symbol, quoteDecimals: p.quoteToken.decimals, pairAddress: p.pairAddress, reserveToken: formatBalance(reserveToken, shouldFormat, tokenDecimals), reserveQuote: formatBalance(reserveQuote, shouldFormat, p.quoteToken.decimals), reserveQuoteRaw: reserveQuote, reserveTokenRaw: reserveToken, // ✅ 新增 }); } catch (err) { if (debug) console.log('[V2] Reserve decode error:', err); } } } catch (err) { if (debug) console.log('[V2] Multicall failed:', err); // Fallback: 逐个查询 return queryV2PairsFallback(token, factoryAddress, quoteTokens, provider, tokenDecimals, shouldFormat); } return results; } async function queryV2PairsFallback(token, factoryAddress, quoteTokens, provider, tokenDecimals, shouldFormat) { const factory = new Contract(factoryAddress, I_UNIV2_FACTORY_ABI, provider); // ✅ 并行查询所有 quoteToken 的交易对 const pairResults = await Promise.all(quoteTokens.map(async (qt) => { try { const pairAddress = await factory.getPair(token, qt.address); if (!pairAddress || pairAddress.toLowerCase() === ZERO_ADDRESS) { return null; } const pair = new Contract(pairAddress, I_UNIV2_PAIR_ABI, provider); // ✅ 并行获取 token0、token1 和 reserves const [token0, token1, reserves] = await Promise.all([ pair.token0(), pair.token1(), pair.getReserves() ]); const [r0, r1] = reserves; const isToken0 = token0.toLowerCase() === token.toLowerCase(); const reserveToken = isToken0 ? r0 : r1; const reserveQuote = isToken0 ? r1 : r0; return { quoteToken: qt.address, quoteSymbol: qt.symbol, quoteDecimals: qt.decimals, pairAddress, reserveToken: formatBalance(reserveToken, shouldFormat, tokenDecimals), reserveQuote: formatBalance(reserveQuote, shouldFormat, qt.decimals), reserveQuoteRaw: reserveQuote, reserveTokenRaw: reserveToken, }; } catch { return null; } })); // 过滤掉 null 结果 return pairResults.filter((r) => r !== null); } // ============================================================================ // V3 批量查询 // ============================================================================ async function queryV3Pools(token, factoryAddress, quoteTokens, feeTiers, provider, multicall3, tokenDecimals, shouldFormat, debug) { const results = []; const factoryIface = new Interface(I_UNIV3_FACTORY_ABI); const erc20Iface = new Interface(I_ERC20_ABI); const mcIface = new Interface(MULTICALL3_ABI); // 构建所有查询组合 const poolCalls = []; for (const qt of quoteTokens) { for (const fee of feeTiers) { // V3 要求 token0 < token1 const tokenLower = token.toLowerCase(); const quoteLower = qt.address.toLowerCase(); const [token0, token1] = tokenLower < quoteLower ? [tokenLower, quoteLower] : [quoteLower, tokenLower]; poolCalls.push({ target: factoryAddress, callData: factoryIface.encodeFunctionData('getPool', [token0, token1, fee]), quoteToken: qt, fee, }); } } try { // 第一步:批量查询所有 getPool const aggData = mcIface.encodeFunctionData('aggregate', [ poolCalls.map(c => ({ target: c.target, callData: c.callData })) ]); const res = await provider.call({ to: multicall3, data: aggData }); const [, returnData] = mcIface.decodeFunctionResult('aggregate', res); // 解析有效的池子 const validPools = []; for (let i = 0; i < poolCalls.length; i++) { try { const decoded = factoryIface.decodeFunctionResult('getPool', returnData[i]); const poolAddress = decoded[0]; if (poolAddress && poolAddress.toLowerCase() !== ZERO_ADDRESS) { validPools.push({ poolAddress, quoteToken: poolCalls[i].quoteToken, fee: poolCalls[i].fee, }); } } catch { // 解码失败,跳过 } } if (validPools.length === 0) return results; // 第二步:批量查询余额 const balanceCalls = validPools.flatMap(p => [ { target: token, callData: erc20Iface.encodeFunctionData('balanceOf', [p.poolAddress]) }, { target: p.quoteToken.address, callData: erc20Iface.encodeFunctionData('balanceOf', [p.poolAddress]) }, ]); const aggData2 = mcIface.encodeFunctionData('aggregate', [balanceCalls]); const res2 = await provider.call({ to: multicall3, data: aggData2 }); const [, returnData2] = mcIface.decodeFunctionResult('aggregate', res2); // 解析余额 for (let i = 0; i < validPools.length; i++) { try { const idx = i * 2; const tokenBalance = erc20Iface.decodeFunctionResult('balanceOf', returnData2[idx])[0]; const quoteBalance = erc20Iface.decodeFunctionResult('balanceOf', returnData2[idx + 1])[0]; const p = validPools[i]; results.push({ quoteToken: p.quoteToken.address, quoteSymbol: p.quoteToken.symbol, quoteDecimals: p.quoteToken.decimals, pairAddress: p.poolAddress, reserveToken: formatBalance(tokenBalance, shouldFormat, tokenDecimals), reserveQuote: formatBalance(quoteBalance, shouldFormat, p.quoteToken.decimals), reserveQuoteRaw: quoteBalance, reserveTokenRaw: tokenBalance, // ✅ 新增 fee: p.fee, }); } catch (err) { if (debug) console.log('[V3] Balance decode error:', err); } } } catch (err) { if (debug) console.log('[V3] Multicall failed:', err); // Fallback: 逐个查询 return queryV3PoolsFallback(token, factoryAddress, quoteTokens, feeTiers, provider, tokenDecimals, shouldFormat); } return results; } async function queryV3PoolsFallback(token, factoryAddress, quoteTokens, feeTiers, provider, tokenDecimals, shouldFormat) { const factory = new Contract(factoryAddress, I_UNIV3_FACTORY_ABI, provider); const tokenContract = new Contract(token, I_ERC20_ABI, provider); // ✅ 构建所有查询组合 const queries = []; for (const qt of quoteTokens) { const tokenLower = token.toLowerCase(); const quoteLower = qt.address.toLowerCase(); const [token0, token1] = tokenLower < quoteLower ? [tokenLower, quoteLower] : [quoteLower, tokenLower]; for (const fee of feeTiers) { queries.push({ qt, fee, token0, token1 }); } } // ✅ 并行查询所有池子 const poolResults = await Promise.all(queries.map(async ({ qt, fee, token0, token1 }) => { try { const poolAddress = await factory.getPool(token0, token1, fee); if (!poolAddress || poolAddress.toLowerCase() === ZERO_ADDRESS) { return null; } const quoteContract = new Contract(qt.address, I_ERC20_ABI, provider); // ✅ 并行获取余额 const [tokenBalance, quoteBalance] = await Promise.all([ tokenContract.balanceOf(poolAddress), quoteContract.balanceOf(poolAddress), ]); return { quoteToken: qt.address, quoteSymbol: qt.symbol, quoteDecimals: qt.decimals, pairAddress: poolAddress, reserveToken: formatBalance(tokenBalance, shouldFormat, tokenDecimals), reserveQuote: formatBalance(quoteBalance, shouldFormat, qt.decimals), reserveQuoteRaw: quoteBalance, reserveTokenRaw: tokenBalance, fee, }; } catch { return null; } })); // 过滤掉 null 结果 return poolResults.filter((r) => r !== null); } // ============================================================================ // 平台类型判断 // ============================================================================ function determinePlatform(allPools) { if (allPools.length === 0) return 'UNKNOWN'; // 统计有流动性的 DEX const dexesWithLiquidity = allPools.filter(dex => dex.v2Pairs.length > 0 || dex.v3Pools.length > 0); if (dexesWithLiquidity.length > 1) { return 'MULTI_DEX'; } if (dexesWithLiquidity.length === 1) { const dex = dexesWithLiquidity[0]; const hasV2 = dex.v2Pairs.length > 0; const hasV3 = dex.v3Pools.length > 0; if (dex.dexKey === 'PANCAKESWAP') { if (hasV2 && hasV3) return 'MULTI_DEX'; if (hasV3) return 'PANCAKESWAP_V3'; return 'PANCAKESWAP_V2'; } if (dex.dexKey === 'UNISWAP') { if (hasV2 && hasV3) return 'MULTI_DEX'; if (hasV3) return 'UNISWAP_V3'; return 'UNISWAP_V2'; } // 其他 DEX return 'MULTI_DEX'; } return 'UNKNOWN'; }