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