UNPKG

four-flap-meme-sdk

Version:

SDK for Flap bonding curve and four.meme TokenManager

226 lines (225 loc) 9.41 kB
import { ethers, Wallet } from 'ethers'; import { ADDRESSES } from '../../utils/constants.js'; import { MULTICALL3_ABI, ERC20_BALANCE_ABI } from '../../abis/common.js'; // 内部缓存:ERC20 小数位 const decimalsCache = new Map(); /** * 获取 ERC20 代币精度(带缓存) * ✅ 优化:避免每次调用都获取 network,直接使用传入的 chainId */ export async function getErc20DecimalsMerkle(provider, token, chainId) { // ✅ 修复:不访问 provider._network(ethers v6 可能未准备好导致 NETWORK_ERROR) // 直接使用传入的 chainId,如果没传则默认 56 (BSC) const resolvedChainId = chainId ?? 56; const key = `${resolvedChainId}_${token.toLowerCase()}`; if (decimalsCache.has(key)) return decimalsCache.get(key); try { const erc20 = new ethers.Contract(token, ['function decimals() view returns (uint8)'], provider); const d = await erc20.decimals(); if (!Number.isFinite(d) || d < 0 || d > 36) return 18; decimalsCache.set(key, d); return d; } catch { return 18; } } export function generateHopWallets(recipientCount, hopCount) { const hopCounts = Array.isArray(hopCount) ? hopCount : new Array(recipientCount).fill(hopCount); if (hopCounts.every(h => h <= 0)) return null; const result = []; for (let i = 0; i < recipientCount; i++) { const chain = []; for (let j = 0; j < hopCounts[i]; j++) { chain.push(Wallet.createRandom().privateKey); } result.push(chain); } return result; } export function normalizeAmounts(recipients, amount, amounts) { if (amounts && amounts.length > 0) { if (amounts.length !== recipients.length) { throw new Error(`amounts length (${amounts.length}) must match recipients length (${recipients.length})`); } return amounts.map(a => (typeof a === 'bigint' ? a.toString() : String(a))); } if (amount !== undefined && String(amount).trim().length > 0) { return new Array(recipients.length).fill(typeof amount === 'bigint' ? amount.toString() : String(amount)); } throw new Error('Either amount or amounts must be provided'); } /** * 批量获取余额(原生代币或 ERC20) * ✅ 优化:原生代币也使用 Multicall3 批量获取,减少 RPC 调用 */ export async function batchGetBalances(provider, addresses, tokenAddress) { if (addresses.length === 0) return []; // ✅ 使用公共模块 const MULTICALL3_ADDRESS = ADDRESSES.BSC.Multicall3; const multicall = new ethers.Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider); if (!tokenAddress) { // ✅ 优化:原生代币使用 Multicall3.getEthBalance 批量获取(单次 RPC 调用) const multicallIface = new ethers.Interface(MULTICALL3_ABI); const calls = addresses.map(addr => ({ target: MULTICALL3_ADDRESS, allowFailure: true, callData: multicallIface.encodeFunctionData('getEthBalance', [addr]) })); try { const results = await multicall.aggregate3(calls); return results.map((result) => { if (result.success && result.returnData && result.returnData !== '0x') { try { const decoded = multicallIface.decodeFunctionResult('getEthBalance', result.returnData); return decoded[0]; } catch { return 0n; } } return 0n; }); } catch { // ✅ 回退:并行调用(如果 Multicall3 失败) return Promise.all(addresses.map(addr => provider.getBalance(addr).catch(() => 0n))); } } else { // ✅ ERC20 余额:使用 Multicall3 批量获取 const iface = new ethers.Interface(ERC20_BALANCE_ABI); const calls = addresses.map(addr => ({ target: tokenAddress, allowFailure: true, callData: iface.encodeFunctionData('balanceOf', [addr]) })); try { const results = await multicall.aggregate3(calls); return results.map((result) => { if (result.success && result.returnData && result.returnData !== '0x') { try { return iface.decodeFunctionResult('balanceOf', result.returnData)[0]; } catch { return 0n; } } return 0n; }); } catch { // ✅ 回退:并行调用(如果 Multicall3 失败) const fallbackCalls = addresses.map(addr => provider .call({ to: tokenAddress, data: iface.encodeFunctionData('balanceOf', [addr]) }) .then(raw => iface.decodeFunctionResult('balanceOf', raw)[0]) .catch(() => 0n)); return Promise.all(fallbackCalls); } } } /** * 通过模拟交易获取 ERC20 转账的最小 Gas Limit * ✅ 使用 eth_estimateGas 预估实际需要的 gas,然后加一个小的缓冲量 * * @param provider - Provider 实例 * @param tokenAddress - ERC20 代币地址 * @param from - 发送方地址 * @param to - 接收方地址 * @param amount - 转账金额(wei) * @param bufferPercent - 缓冲百分比(默认 5%) * @returns 预估的 gas limit */ export async function estimateErc20TransferGas(provider, tokenAddress, from, to, amount, bufferPercent = 5) { try { const iface = new ethers.Interface(['function transfer(address,uint256) returns (bool)']); const data = iface.encodeFunctionData('transfer', [to, amount]); const estimatedGas = await provider.estimateGas({ from, to: tokenAddress, data, value: 0n }); // 添加缓冲量(默认 5%) const buffer = (estimatedGas * BigInt(bufferPercent)) / 100n; const finalGas = estimatedGas + buffer; console.log(`[estimateErc20TransferGas] 预估 gas: ${estimatedGas}, 最终 gas limit: ${finalGas} (+${bufferPercent}%)`); return finalGas; } catch (error) { console.warn(`[estimateErc20TransferGas] 预估失败,使用默认值:`, error); // 回退到默认值 return 52000n; } } /** * 批量预估多个 ERC20 转账的 Gas Limit * ✅ 选取最大值作为统一的 gas limit(确保所有转账都能成功) */ export async function estimateMaxErc20TransferGas(provider, tokenAddress, from, recipients, amounts, bufferPercent = 5) { if (recipients.length === 0) return 52000n; // 为了减少 RPC 调用,最多预估前 3 个和最后 1 个 const sampleIndices = []; sampleIndices.push(0); // 第一个 if (recipients.length > 1) sampleIndices.push(Math.min(1, recipients.length - 1)); if (recipients.length > 2) sampleIndices.push(Math.min(2, recipients.length - 1)); if (recipients.length > 3) sampleIndices.push(recipients.length - 1); // 最后一个 // 去重 const uniqueIndices = [...new Set(sampleIndices)]; try { const estimates = await Promise.all(uniqueIndices.map(i => estimateErc20TransferGas(provider, tokenAddress, from, recipients[i], amounts[i], bufferPercent))); // 取最大值 const maxGas = estimates.reduce((max, gas) => gas > max ? gas : max, 0n); console.log(`[estimateMaxErc20TransferGas] 样本数: ${uniqueIndices.length}, 最大 gas limit: ${maxGas}`); return maxGas; } catch (error) { console.warn(`[estimateMaxErc20TransferGas] 批量预估失败,使用默认值:`, error); return 52000n; } } /** * 计算 Gas Limit * - 原生代币转账:21000 gas(固定) * - ERC20 标准 transfer:约 45000-55000 gas,使用 55000 作为安全值 * * ✅ 优化:降低 ERC20 gas limit,减少中转钱包 BNB 残留 */ export function calculateGasLimit(config, isNative, hasHops, hopCount = 0) { if (config.gasLimit !== undefined) { return BigInt(config.gasLimit); } // ✅ 原生代币: 21000, ERC20 标准 transfer: 48000(USDT 最低约 46815) const baseGas = isNative ? 21000 : 46815; // ✅ 多跳时只需要累加单次转账的 gas,不需要额外乘数 // 每个中转钱包只执行一笔 transfer if (hasHops && hopCount > 0) { // 多跳场景:返回单次转账的 gas limit(中转钱包只执行一笔交易) // 这里不累加,因为每个中转钱包独立执行 const multiplier = config.gasLimitMultiplier ?? 1.1; // ✅ 降低安全系数 return BigInt(Math.ceil(baseGas * multiplier)); } // 无多跳或单次调用:使用较小的安全系数 const multiplier = config.gasLimitMultiplier ?? 1.1; return BigInt(Math.ceil(baseGas * multiplier)); } export function isNativeTokenAddress(tokenAddress) { if (!tokenAddress) return true; const v = tokenAddress.trim().toLowerCase(); if (!v) return true; if (v === 'native') return true; if (v === '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee') return true; return false; }