four-flap-meme-sdk
Version:
SDK for Flap bonding curve and four.meme TokenManager
791 lines (790 loc) • 34 kB
JavaScript
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';
}