four-flap-meme-sdk
Version:
SDK for Flap bonding curve and four.meme TokenManager
615 lines (614 loc) • 28 kB
JavaScript
import { Contract, Wallet, JsonRpcProvider, Interface, parseUnits } from 'ethers';
import { ADDRESSES, ZERO_ADDRESS } from './constants.js';
import { NonceManager } from './bundle-helpers.js';
import { ERC20_ABI, MULTICALL3_ABI } from '../abis/common.js';
/**
* 验证合约地址是否有效(是否部署了代码)
*/
async function validateContractAddress(provider, address, label) {
try {
// ✅ 先规范化地址(转为 checksum 格式),避免 ethers v6 的严格校验报错
const normalizedAddress = address.toLowerCase().startsWith('0x')
? address.toLowerCase()
: `0x${address.toLowerCase()}`;
const code = await provider.getCode(normalizedAddress);
if (code === '0x' || code.length <= 2) {
throw new Error(`❌ ${label} 地址无效或未部署合约: ${address}`);
}
}
catch (error) {
if (error.message.includes('无效或未部署')) {
throw error;
}
// ✅ 如果是 checksum 错误,提供更友好的提示
if (error.message.includes('bad address checksum') || error.code === 'INVALID_ARGUMENT') {
throw new Error(`❌ ${label} 地址格式错误,请使用正确的 checksum 格式: ${address}`);
}
throw new Error(`❌ 无法验证 ${label} 地址 ${address}: ${error.message}`);
}
}
/**
* 通用 ERC20 授权方法(内部辅助函数)
* 自动检查当前授权额度,只在不足时才发送交易
*/
async function ensureAllowance(rpcUrl, privateKey, token, owner, spender, required) {
const provider = new JsonRpcProvider(rpcUrl);
const signer = new Wallet(privateKey, provider);
// ✅ 验证 token 和 spender 地址
await validateContractAddress(provider, token, 'Token');
await validateContractAddress(provider, spender, 'Spender');
const erc20 = new Contract(token, ERC20_ABI, signer);
// ✅ 自动检查当前授权额度,只在不足时才发送交易
let current;
try {
current = await erc20.allowance(owner, spender);
}
catch (error) {
throw new Error(`❌ 调用 allowance 失败(Token 可能不是 ERC20): ${error.message}`);
}
if (current >= required) {
return {
alreadyApproved: true,
currentAllowance: current,
requiredAllowance: required,
};
}
const tx = await erc20.approve(spender, required);
const receipt = await tx.wait();
return {
alreadyApproved: false,
currentAllowance: current,
requiredAllowance: required,
txReceipt: receipt,
};
}
/**
* 检查代币授权状态 - Four.meme V1(只读,不发送交易)
* @returns 是否已授权足够的额度
*/
export async function checkSellApprovalV1(chain, rpcUrl, token, owner, amount) {
const proxyAddresses = {
BSC: ADDRESSES.BSC.TokenManagerV1, // FourMeme 代理合约 V1 (BSC)
BASE: ADDRESSES.BASE.TokenManagerHelper3, // FourMeme 代理合约 (BASE)
ARBITRUM_ONE: ADDRESSES.ARBITRUM_ONE.TokenManagerHelper3, // FourMeme 代理合约 (ARBITRUM)
};
const provider = new JsonRpcProvider(rpcUrl);
// ✅ 验证 token 和代理合约地址
await validateContractAddress(provider, token, 'Token');
await validateContractAddress(provider, proxyAddresses[chain], `Four.meme V1 Proxy (${chain})`);
const erc20 = new Contract(token, ERC20_ABI, provider);
let current;
try {
current = await erc20.allowance(owner, proxyAddresses[chain]);
}
catch (error) {
throw new Error(`❌ 调用 allowance 失败(Token 可能不是 ERC20): ${error.message}`);
}
return {
isApproved: current >= amount,
currentAllowance: current,
requiredAllowance: amount,
};
}
/**
* 确保代币已授权给 Four.meme V1 代理合约
* 用于 Four.meme V1 代币的卖出操作
* @returns 授权结果,包含是否已授权、当前额度、交易回执等信息
*/
export async function ensureSellApprovalV1(chain, rpcUrl, privateKey, token, owner, amount) {
// ✅ 授权给 FourMeme 代理合约 V1(收费版)
const proxyAddresses = {
BSC: ADDRESSES.BSC.TokenManagerV1, // FourMeme 代理合约 V1 (BSC)
BASE: ADDRESSES.BASE.TokenManagerHelper3, // FourMeme 代理合约 (BASE)
ARBITRUM_ONE: ADDRESSES.ARBITRUM_ONE.TokenManagerHelper3, // FourMeme 代理合约 (ARBITRUM)
};
return await ensureAllowance(rpcUrl, privateKey, token, owner, proxyAddresses[chain], amount);
}
/**
* 检查代币授权状态 - Four.meme V2(只读,不发送交易)
* @returns 是否已授权足够的额度
*/
export async function checkSellApprovalV2(chain, rpcUrl, token, owner, amount) {
const proxyAddresses = {
BSC: ADDRESSES.BSC.TokenManagerV2, // FourMeme 代理合约 V2 (BSC)
BASE: ADDRESSES.BASE.TokenManagerHelper3, // FourMeme 代理合约 (BASE)
ARBITRUM_ONE: ADDRESSES.ARBITRUM_ONE.TokenManagerHelper3, // FourMeme 代理合约 (ARBITRUM)
};
const provider = new JsonRpcProvider(rpcUrl);
// ✅ 验证 token 和代理合约地址
await validateContractAddress(provider, token, 'Token');
await validateContractAddress(provider, proxyAddresses[chain], `Four.meme V2 Proxy (${chain})`);
const erc20 = new Contract(token, ERC20_ABI, provider);
let current;
try {
current = await erc20.allowance(owner, proxyAddresses[chain]);
}
catch (error) {
throw new Error(`❌ 调用 allowance 失败(Token 可能不是 ERC20): ${error.message}`);
}
return {
isApproved: current >= amount,
currentAllowance: current,
requiredAllowance: amount,
};
}
/**
* 确保代币已授权给 Four.meme V2 代理合约
* 用于 Four.meme V2 代币的卖出操作
* @returns 授权结果,包含是否已授权、当前额度、交易回执等信息
*/
export async function ensureSellApprovalV2(chain, rpcUrl, privateKey, token, owner, amount) {
// ✅ 授权给 FourMeme 代理合约 V2(收费版)
const proxyAddresses = {
BSC: ADDRESSES.BSC.TokenManagerV2, // FourMeme 代理合约 V2 (BSC)
BASE: ADDRESSES.BASE.TokenManagerHelper3, // FourMeme 代理合约 (BASE)
ARBITRUM_ONE: ADDRESSES.ARBITRUM_ONE.TokenManagerHelper3, // FourMeme 代理合约 (ARBITRUM)
};
return await ensureAllowance(rpcUrl, privateKey, token, owner, proxyAddresses[chain], amount);
}
/**
* 检查代币授权状态 - Four.meme(通用,默认使用 V2)
* @deprecated 建议使用明确版本的方法:checkSellApprovalV1 或 checkSellApprovalV2
*/
export async function checkSellApproval(chain, rpcUrl, token, owner, amount) {
return checkSellApprovalV2(chain, rpcUrl, token, owner, amount);
}
/**
* 确保代币已授权给 Four.meme 代理合约(通用,默认使用 V2)
* @deprecated 建议使用明确版本的方法:ensureSellApprovalV1 或 ensureSellApprovalV2
*/
export async function ensureSellApproval(chain, rpcUrl, privateKey, token, owner, amount) {
return ensureSellApprovalV2(chain, rpcUrl, privateKey, token, owner, amount);
}
/**
* 检查代币授权状态 - Flap Protocol(只读,不发送交易)
* @returns 是否已授权足够的额度
*/
export async function checkFlapSellApproval(chain, rpcUrl, token, owner, amount) {
const proxyAddresses = {
BSC: ADDRESSES.BSC.FlapPortal, // Flap Portal 代理合约 (BSC)
BASE: ADDRESSES.BASE.FlapPortal, // Flap Portal 代理合约 (BASE)
XLAYER: ADDRESSES.XLAYER.FlapPortal, // Flap Portal 代理合约 (XLAYER)
MORPH: ADDRESSES.MORPH.FlapPortal, // Flap Portal 代理合约 (MORPH)
MONAD: ADDRESSES.MONAD.FlapPortal, // Flap Portal 代理合约 (MONAD)
};
const provider = new JsonRpcProvider(rpcUrl);
// ✅ 验证 token 和代理合约地址
await validateContractAddress(provider, token, 'Token');
await validateContractAddress(provider, proxyAddresses[chain], `Flap Portal (${chain})`);
const erc20 = new Contract(token, ERC20_ABI, provider);
let current;
try {
current = await erc20.allowance(owner, proxyAddresses[chain]);
}
catch (error) {
throw new Error(`❌ 调用 allowance 失败(Token 可能不是 ERC20): ${error.message}`);
}
return {
isApproved: current >= amount,
currentAllowance: current,
requiredAllowance: amount,
};
}
/**
* 确保代币已授权给 Flap Protocol 代理合约
* 用于 Flap Protocol 代币的卖出操作
* @returns 授权结果,包含是否已授权、当前额度、交易回执等信息
*/
export async function ensureFlapSellApproval(chain, rpcUrl, privateKey, token, owner, amount) {
// ✅ 授权给 Flap Portal 代理合约(收费版)
const proxyAddresses = {
BSC: ADDRESSES.BSC.FlapPortal, // Flap Portal 代理合约 (BSC)
BASE: ADDRESSES.BASE.FlapPortal, // Flap Portal 代理合约 (BASE)
XLAYER: ADDRESSES.XLAYER.FlapPortal, // Flap Portal 代理合约 (XLAYER)
MORPH: ADDRESSES.MORPH.FlapPortal, // Flap Portal 代理合约 (MORPH)
MONAD: ADDRESSES.MONAD.FlapPortal, // Flap Portal 代理合约 (MONAD)
};
return await ensureAllowance(rpcUrl, privateKey, token, owner, proxyAddresses[chain], amount);
}
/**
* 批量确保代币已授权给 Flap Protocol 代理合约(默认授权上限 2^256-1)
* @param privateKeys 拥有者私钥数组(每个地址需自行签名)
* @returns 每个地址的授权结果(包含 owner 与交易回执等信息)
*/
export async function ensureFlapSellApprovalBatch(chain, rpcUrl, privateKeys, token, required = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')) {
if (!privateKeys || privateKeys.length === 0)
return [];
// ✅ 并行处理所有授权
const results = await Promise.all(privateKeys.map(async (pk) => {
const owner = new Wallet(pk).address;
const r = await ensureFlapSellApproval(chain, rpcUrl, pk, token, owner, required);
return { owner, ...r };
}));
return results;
}
/**
* 批量检查 Flap Protocol 授权状态(默认按上限 2^256-1 判断)
* ✅ 使用 Multicall3 批量查询,减少 RPC 调用次数
*/
export async function checkFlapSellApprovalBatch(chain, rpcUrl, token, owners, required = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')) {
if (!owners || owners.length === 0)
return [];
const proxyAddresses = {
BSC: ADDRESSES.BSC.FlapPortal,
BASE: ADDRESSES.BASE.FlapPortal,
XLAYER: ADDRESSES.XLAYER.FlapPortal,
MORPH: ADDRESSES.MORPH.FlapPortal,
MONAD: ADDRESSES.MONAD.FlapPortal,
};
const provider = new JsonRpcProvider(rpcUrl);
// ✅ 并行验证 token 和代理合约地址
await Promise.all([
validateContractAddress(provider, token, 'Token'),
validateContractAddress(provider, proxyAddresses[chain], `Flap Portal (${chain})`)
]);
// ✅ 使用 batchCheckAllowances 批量查询(Multicall3)
const allowances = await batchCheckAllowances(provider, token, owners, proxyAddresses[chain]);
return owners.map((owner, i) => ({
owner,
isApproved: allowances[i] >= required,
currentAllowance: allowances[i],
requiredAllowance: required
}));
}
/**
* 使用 Multicall3 批量查询 ERC20 授权额度
* @param provider - Provider 实例
* @param tokenAddress - ERC20 代币地址
* @param owners - 所有者地址数组
* @param spender - 被授权的 spender 地址
* @returns 每个地址的授权额度数组
*/
export async function batchCheckAllowances(provider, tokenAddress, owners, spender) {
// ✅ 使用公共常量和 ABI
const multicall3Address = ADDRESSES.BSC.Multicall3;
const multicall3 = new Contract(multicall3Address, MULTICALL3_ABI, provider);
// 编码 allowance(owner, spender) 调用数据
const erc20Interface = new Contract(tokenAddress, ERC20_ABI, provider).interface;
const calls = owners.map(owner => ({
target: tokenAddress,
allowFailure: true,
callData: erc20Interface.encodeFunctionData('allowance', [owner, spender])
}));
// 批量调用
const results = await multicall3.aggregate3(calls);
// 解析结果
return results.map((result, index) => {
if (!result.success) {
return 0n;
}
try {
const decoded = erc20Interface.decodeFunctionResult('allowance', result.returnData);
return decoded[0];
}
catch (error) {
return 0n;
}
});
}
/**
* 🔧 内部辅助函数:根据链和平台自动解析 spender 地址
*/
function resolveSpenderAddress(chain, platform) {
const spenderMap = {
flap: {
BSC: ADDRESSES.BSC.FlapPortal,
BASE: ADDRESSES.BASE.FlapPortal,
XLAYER: ADDRESSES.XLAYER.FlapPortal,
MORPH: ADDRESSES.MORPH.FlapPortal,
MONAD: ADDRESSES.MONAD.FlapPortal, // ✅ Monad Flap Portal
},
four: {
// Four.meme 使用 TokenManagerV2Proxy(默认)
BSC: ADDRESSES.BSC.TokenManagerV2Proxy,
BASE: ADDRESSES.BASE.TokenManagerHelper3,
XLAYER: ZERO_ADDRESS, // XLAYER 暂不支持 Four.meme
MORPH: ZERO_ADDRESS, // MORPH 暂不支持 Four.meme
MONAD: ZERO_ADDRESS, // MONAD 不支持 Four.meme
},
'pancake-v2': {
// PancakeSwap V2 Router 地址
BSC: ADDRESSES.BSC.PancakeV2Router,
BASE: '0x8cFe327CEc66d1C090Dd72bd0FF11d690C33a2Eb', // BASE PancakeSwap V2 Router
XLAYER: ZERO_ADDRESS, // XLAYER 暂不支持
MORPH: ZERO_ADDRESS, // MORPH 暂不支持
MONAD: '0xb1bc24c34e88f7d43d5923034e3a14b24daacff9', // Monad PancakeSwap V2 Router
},
'pancake-v3': {
// PancakeSwap V3 SmartRouter 地址
BSC: ADDRESSES.BSC.PancakeV3Router,
BASE: '0x678Aa4bF4E210cf2166753e054d5b7c31cc7fa86', // BASE PancakeSwap V3 SmartRouter
XLAYER: ZERO_ADDRESS, // XLAYER 暂不支持
MORPH: ZERO_ADDRESS, // MORPH 暂不支持
MONAD: '0x1b81d678ffb9c0263b24a97847620c99d213eb14', // Monad PancakeSwap V3 Router
},
};
const spender = spenderMap[platform]?.[chain];
if (!spender || spender === ZERO_ADDRESS) {
throw new Error(`❌ 不支持的链或平台: ${chain} / ${platform}`);
}
return spender;
}
/**
* ✅ 智能路由:检查单个 ERC20 授权额度(自动选择 spender)
*
* @param chain - 链名称 ('BSC' | 'BASE' | 'XLAYER' | 'MORPH' | 'MONAD')
* @param platform - 平台名称 ('flap' | 'four' | 'pancake-v2' | 'pancake-v3')
* @param rpcUrl - RPC 节点地址
* @param tokenAddress - 代币合约地址
* @param ownerAddress - 代币持有者地址
* @returns 当前授权额度(bigint)
*/
export async function checkAllowance(chain, platform, rpcUrl, tokenAddress, ownerAddress) {
const spenderAddress = resolveSpenderAddress(chain, platform);
return checkAllowanceRaw(rpcUrl, tokenAddress, ownerAddress, spenderAddress);
}
/**
* ✅ 底层方法:检查 ERC20 代币授权额度(手动指定 spender)
* 适用于任意 spender 地址(Flap、Four、PancakeSwap V2/V3 等)
*
* @param rpcUrl - RPC 节点地址
* @param tokenAddress - 代币合约地址
* @param ownerAddress - 代币持有者地址
* @param spenderAddress - 被授权的合约地址(如 Router、Portal、TokenManager 等)
* @returns 当前授权额度(bigint)
*/
export async function checkAllowanceRaw(rpcUrl, tokenAddress, ownerAddress, spenderAddress) {
const provider = new JsonRpcProvider(rpcUrl);
// ✅ 规范化地址(转为小写,避免 checksum 错误)
const normalizedToken = tokenAddress.toLowerCase();
const normalizedOwner = ownerAddress.toLowerCase();
const normalizedSpender = spenderAddress.toLowerCase();
// 验证地址
await validateContractAddress(provider, normalizedToken, 'Token');
await validateContractAddress(provider, normalizedSpender, 'Spender');
const erc20 = new Contract(normalizedToken, ERC20_ABI, provider);
try {
const allowance = await erc20.allowance(normalizedOwner, normalizedSpender);
return allowance;
}
catch (error) {
throw new Error(`❌ 查询授权额度失败: ${error.message}`);
}
}
/**
* ✅ 智能路由:授权 ERC20 代币(自动选择 spender,自动检查,只在不足时才发送交易)
*
* @param chain - 链名称 ('BSC' | 'BASE' | 'XLAYER' | 'MORPH' | 'MONAD')
* @param platform - 平台名称 ('flap' | 'four' | 'pancake-v2' | 'pancake-v3')
* @param rpcUrl - RPC 节点地址
* @param privateKey - 私钥
* @param tokenAddress - 代币合约地址
* @param amount - 授权数量(bigint),传 'max' 表示最大授权
* @returns 授权结果
*/
export async function approveToken(chain, platform, rpcUrl, privateKey, tokenAddress, amount) {
const spenderAddress = resolveSpenderAddress(chain, platform);
return approveTokenRaw(rpcUrl, privateKey, tokenAddress, spenderAddress, amount);
}
/**
* ✅ 底层方法:授权 ERC20 代币给指定合约(手动指定 spender)
* 适用于任意 spender 地址(Flap、Four、PancakeSwap V2/V3 等)
*
* @param rpcUrl - RPC 节点地址
* @param privateKey - 私钥
* @param tokenAddress - 代币合约地址
* @param spenderAddress - 被授权的合约地址(如 Router、Portal、TokenManager 等)
* @param amount - 授权数量(bigint),传 'max' 或 ethers.MaxUint256 表示最大授权
* @returns 授权结果
*/
export async function approveTokenRaw(rpcUrl, privateKey, tokenAddress, spenderAddress, amount) {
const provider = new JsonRpcProvider(rpcUrl);
const signer = new Wallet(privateKey, provider);
const ownerAddress = signer.address;
// ✅ 规范化地址(转为小写,避免 checksum 错误)
const normalizedToken = tokenAddress.toLowerCase();
const normalizedSpender = spenderAddress.toLowerCase();
// 验证地址
await validateContractAddress(provider, normalizedToken, 'Token');
await validateContractAddress(provider, normalizedSpender, 'Spender');
const erc20 = new Contract(normalizedToken, ERC20_ABI, signer);
const requiredAmount = amount === 'max' ? BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff') : amount;
// 检查当前授权额度
let currentAllowance;
try {
currentAllowance = await erc20.allowance(ownerAddress, normalizedSpender);
}
catch (error) {
throw new Error(`❌ 查询授权额度失败: ${error.message}`);
}
// 如果已经授权足够,直接返回
if (currentAllowance >= requiredAmount) {
return {
alreadyApproved: true,
currentAllowance,
requiredAllowance: requiredAmount
};
}
// 发送授权交易
try {
const tx = await erc20.approve(normalizedSpender, requiredAmount);
const receipt = await tx.wait();
return {
alreadyApproved: false,
currentAllowance,
requiredAllowance: requiredAmount,
txReceipt: receipt
};
}
catch (error) {
throw new Error(`❌ 授权交易失败: ${error.message}`);
}
}
/**
* ✅ 智能路由:批量检查多个钱包的授权额度(自动选择 spender)
*
* @param chain - 链名称 ('BSC' | 'BASE' | 'XLAYER' | 'MORPH' | 'MONAD')
* @param platform - 平台名称 ('flap' | 'four' | 'pancake-v2' | 'pancake-v3')
* @param rpcUrl - RPC 节点地址
* @param tokenAddress - 代币合约地址
* @param ownerAddresses - 代币持有者地址数组
* @returns 每个地址的授权额度数组
*/
export async function checkAllowanceBatch(chain, platform, rpcUrl, tokenAddress, ownerAddresses) {
const spenderAddress = resolveSpenderAddress(chain, platform);
return checkAllowanceBatchRaw(rpcUrl, tokenAddress, ownerAddresses, spenderAddress);
}
/**
* ✅ 底层方法:批量检查多个钱包的授权额度(手动指定 spender)
* 适用于任意 spender 地址(Flap、Four、PancakeSwap V2/V3 等)
*
* @param rpcUrl - RPC 节点地址
* @param tokenAddress - 代币合约地址
* @param ownerAddresses - 代币持有者地址数组
* @param spenderAddress - 被授权的合约地址
* @returns 每个地址的授权额度数组
*/
export async function checkAllowanceBatchRaw(rpcUrl, tokenAddress, ownerAddresses, spenderAddress) {
const provider = new JsonRpcProvider(rpcUrl);
// ✅ 规范化地址(转为小写,避免 checksum 错误)
const normalizedToken = tokenAddress.toLowerCase();
const normalizedOwners = ownerAddresses.map(addr => addr.toLowerCase());
const normalizedSpender = spenderAddress.toLowerCase();
// 验证地址
await validateContractAddress(provider, normalizedToken, 'Token');
await validateContractAddress(provider, normalizedSpender, 'Spender');
return batchCheckAllowances(provider, normalizedToken, normalizedOwners, normalizedSpender);
}
export async function approveTokenBatch(params) {
const { chain, platform, rpcUrl, privateKeys, tokenAddress, amounts, signOnly, gasPriceGwei, gasLimit, chainId } = params;
const spenderAddress = resolveSpenderAddress(chain, platform);
return approveTokenBatchRaw({ rpcUrl, privateKeys, tokenAddress, spenderAddress, amounts, signOnly, gasPriceGwei, gasLimit, chainId });
}
export async function approveTokenBatchRaw(params) {
const { rpcUrl, privateKeys, tokenAddress, spenderAddress, amounts, signOnly, gasPriceGwei, gasLimit, chainId = 56, nonceManager: externalNonceManager } = params;
if (privateKeys.length === 0 || amounts.length !== privateKeys.length) {
throw new Error('❌ 私钥数量和授权数量必须匹配');
}
const provider = new JsonRpcProvider(rpcUrl);
// ✅ 规范化地址(转为小写,避免 checksum 错误)
const normalizedToken = tokenAddress.toLowerCase();
const normalizedSpender = spenderAddress.toLowerCase();
// 验证地址
await validateContractAddress(provider, normalizedToken, 'Token');
await validateContractAddress(provider, normalizedSpender, 'Spender');
// ✅ 优化:批量创建钱包和合约实例
const wallets = privateKeys.map(key => new Wallet(key, provider));
const ownerAddresses = wallets.map(w => w.address);
const requiredAmounts = amounts.map(amount => amount === 'max'
? BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
: amount);
// ==================== signOnly=true:只签名不提交 ====================
if (signOnly) {
// ✅ 使用 NonceManager 管理 nonce(和买卖交易一样)
const nonceManager = externalNonceManager || new NonceManager(provider);
// ✅ 并行获取:当前授权额度 + nonces + gasPrice
const [currentAllowances, nonces, fetchedGasPrice] = await Promise.all([
batchCheckAllowances(provider, normalizedToken, ownerAddresses, normalizedSpender),
nonceManager.getNextNoncesForWallets(wallets), // ✅ 使用 NonceManager 批量获取 nonce
gasPriceGwei ? Promise.resolve(parseUnits(gasPriceGwei.toString(), 'gwei')) : provider.getFeeData().then(fee => fee.gasPrice || parseUnits('3', 'gwei'))
]);
const finalGasPrice = fetchedGasPrice;
const finalGasLimit = BigInt(gasLimit || 100000);
// ✅ ERC20 approve 函数的 ABI 编码
const erc20Interface = new Interface(ERC20_ABI);
// ✅ 并行签名所有需要授权的交易
const signPromises = wallets.map(async (wallet, i) => {
const ownerAddress = ownerAddresses[i];
const currentAllowance = currentAllowances[i];
const requiredAmount = requiredAmounts[i];
// 如果已经授权足够,跳过
if (currentAllowance >= requiredAmount) {
return {
owner: ownerAddress,
alreadyApproved: true,
currentAllowance,
requiredAllowance: requiredAmount,
signedTx: undefined
};
}
// 构建并签名交易
const txData = erc20Interface.encodeFunctionData('approve', [normalizedSpender, requiredAmount]);
const signedTx = await wallet.signTransaction({
to: normalizedToken,
data: txData,
nonce: nonces[i], // ✅ NonceManager 已经处理好递增
gasLimit: finalGasLimit,
gasPrice: finalGasPrice,
chainId,
type: 0 // Legacy 交易
});
return {
owner: ownerAddress,
alreadyApproved: false,
currentAllowance,
requiredAllowance: requiredAmount,
signedTx
};
});
const results = await Promise.all(signPromises);
// ✅ 提取所有签名交易(过滤掉已授权的)
const signedTransactions = results
.filter(r => !r.alreadyApproved && r.signedTx)
.map(r => r.signedTx);
const alreadyApprovedCount = results.filter(r => r.alreadyApproved).length;
const needApproveCount = results.filter(r => !r.alreadyApproved).length;
return {
signedTransactions,
results,
needApproveCount,
alreadyApprovedCount
};
}
// ==================== signOnly=false(默认):直接发送交易 ====================
// ✅ 优化:并行检查所有钱包的授权额度
const currentAllowances = await batchCheckAllowances(provider, normalizedToken, ownerAddresses, normalizedSpender);
// ✅ 优化:并行发送所有需要授权的交易
const approvalPromises = wallets.map(async (wallet, i) => {
const ownerAddress = ownerAddresses[i];
const currentAllowance = currentAllowances[i];
const requiredAmount = requiredAmounts[i];
try {
// 如果已经授权足够,跳过
if (currentAllowance >= requiredAmount) {
return {
owner: ownerAddress,
alreadyApproved: true,
currentAllowance,
requiredAllowance: requiredAmount
};
}
// 发送授权交易
const erc20 = new Contract(normalizedToken, ERC20_ABI, wallet);
const tx = await erc20.approve(normalizedSpender, requiredAmount);
const receipt = await tx.wait();
return {
owner: ownerAddress,
alreadyApproved: false,
currentAllowance,
requiredAllowance: requiredAmount,
txHash: receipt.hash
};
}
catch (error) {
return {
owner: ownerAddress,
alreadyApproved: false,
currentAllowance,
requiredAllowance: requiredAmount,
error: error.message
};
}
});
// ✅ 优化:并行等待所有授权交易完成
const results = await Promise.all(approvalPromises);
// 统计结果
const approvedCount = results.filter(r => !r.alreadyApproved && !r.error).length;
const errorCount = results.filter(r => r.error).length;
// ✅ 只要没有错误,就算成功(包括所有钱包都已授权的情况)
return {
success: errorCount === 0,
approvedCount,
results
};
}