UNPKG

four-flap-meme-sdk

Version:

SDK for Flap bonding curve and four.meme TokenManager

452 lines (451 loc) 16.2 kB
/** * Bundle 交易辅助工具 * 提供可复用的 nonce 管理、gas 估算等功能 */ import { ethers, Wallet } from 'ethers'; /** * Nonce 管理器 * 用于在 bundle 交易中管理多个钱包的 nonce * * 策略: * 1. 每次获取 nonce 时查询链上最新状态('latest' 不含 pending) * 2. 仅在同一批次内维护临时递增缓存 * 3. 不持久化缓存,避免因失败交易导致 nonce 过高 */ export class NonceManager { constructor(provider) { // 临时缓存:仅在单次批量调用期间有效 this.tempNonceCache = new Map(); this.provider = provider; } /** * 获取下一个可用的 nonce * @param wallet 钱包对象或地址 * @returns 下一个 nonce */ async getNextNonce(wallet) { const address = typeof wallet === 'string' ? wallet : wallet.address; const key = address.toLowerCase(); // 检查临时缓存 if (this.tempNonceCache.has(key)) { const cachedNonce = this.tempNonceCache.get(key); this.tempNonceCache.set(key, cachedNonce + 1); return cachedNonce; } // ✅ 使用 'latest' 获取 nonce(已确认的交易) // 由于前端已移除 nonce 缓存,SDK 每次都从链上获取最新状态 const onchainNonce = await this.provider.getTransactionCount(address, 'latest'); // 缓存下一个值(仅在当前批次内有效) this.tempNonceCache.set(key, onchainNonce + 1); return onchainNonce; } /** * 批量获取连续 nonce(推荐用于同一地址的批量交易) * @param wallet 钱包对象或地址 * @param count 需要的 nonce 数量 * @returns nonce 数组 */ async getNextNonceBatch(wallet, count) { const address = typeof wallet === 'string' ? wallet : wallet.address; const key = address.toLowerCase(); let startNonce; if (this.tempNonceCache.has(key)) { startNonce = this.tempNonceCache.get(key); } else { startNonce = await this.provider.getTransactionCount(address, 'latest'); } // 更新缓存 this.tempNonceCache.set(key, startNonce + count); // 返回连续 nonce 数组 return Array.from({ length: count }, (_, i) => startNonce + i); } /** * 清空临时缓存(建议在每次 bundle 提交后调用) */ clearTemp() { this.tempNonceCache.clear(); } /** * 获取当前临时缓存的 nonce(调试用) */ getTempCachedNonce(address) { return this.tempNonceCache.get(address.toLowerCase()); } /** * ✅ 批量获取多个不同地址的 nonce(真正的单次 JSON-RPC 批量请求) * 使用 provider.send 发送批量请求,N 个地址只需 1 次网络往返 * @param wallets 钱包数组 * @returns nonce 数组(顺序与 wallets 对应) */ async getNextNoncesForWallets(wallets) { const addresses = wallets.map(w => typeof w === 'string' ? w : w.address); const keys = addresses.map(a => a.toLowerCase()); // 分离:已缓存的 vs 需要查询的 const needQuery = []; const results = new Array(wallets.length); for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (this.tempNonceCache.has(key)) { const cachedNonce = this.tempNonceCache.get(key); results[i] = cachedNonce; this.tempNonceCache.set(key, cachedNonce + 1); } else { needQuery.push({ index: i, address: addresses[i] }); } } // 如果有需要查询的地址,使用真正的 JSON-RPC 批量请求 if (needQuery.length > 0) { let queryResults; try { // ✅ 使用 JSON-RPC 批量请求(单次网络往返) const batchRequests = needQuery.map(({ address }, idx) => ({ method: 'eth_getTransactionCount', params: [address, 'latest'], id: idx + 1, jsonrpc: '2.0' })); // 通过 provider._send 发送批量请求(ethers v6 内部方法) const rawResults = await this.provider._send(batchRequests); // 解析结果 queryResults = rawResults.map((r) => { if (r.result) { return parseInt(r.result, 16); } return 0; }); } catch { // 如果批量请求失败,回退到 Promise.all const queryPromises = needQuery.map(({ address }) => this.provider.getTransactionCount(address, 'latest')); queryResults = await Promise.all(queryPromises); } // 填充结果并更新缓存 for (let i = 0; i < needQuery.length; i++) { const { index, address } = needQuery[i]; const nonce = queryResults[i]; results[index] = nonce; this.tempNonceCache.set(address.toLowerCase(), nonce + 1); } } return results; } } /** * 获取优化后的 Gas Price * @param provider Provider 实例 * @param config Gas 配置 * @returns Gas price */ export async function getOptimizedGasPrice(provider, config) { let gasPrice; if (config?.baseGasPrice) { gasPrice = config.baseGasPrice; } else { const feeData = await provider.getFeeData(); gasPrice = feeData.gasPrice || ethers.parseUnits('3', 'gwei'); } // 应用增幅 const multiplier = config?.multiplierPercent ?? 50; gasPrice = (gasPrice * BigInt(100 + multiplier)) / 100n; // 应用限制 if (config?.minGasPrice && gasPrice < config.minGasPrice) { gasPrice = config.minGasPrice; } if (config?.maxGasPrice && gasPrice > config.maxGasPrice) { gasPrice = config.maxGasPrice; } return gasPrice; } /** * 估算 gas 并应用安全余量 * @param provider Provider 实例 * @param txRequest 交易请求 * @param config Gas 估算配置 * @returns Gas limit */ export async function estimateGasWithSafety(provider, txRequest, config) { const multiplier = config?.multiplier ?? 1.2; const fallback = config?.fallbackGasLimit ?? 600000n; const allowFailure = config?.allowFailure ?? true; try { const estimated = await provider.estimateGas(txRequest); return BigInt(Math.ceil(Number(estimated) * multiplier)); } catch (error) { if (!allowFailure) { throw error; } console.warn(`Gas estimation failed, using fallback ${fallback}:`, error.message); return fallback; } } /** * 批量估算 gas(并行) * @param provider Provider 实例 * @param txRequests 交易请求数组 * @param config Gas 估算配置 * @returns Gas limit 数组 */ export async function estimateGasBatch(provider, txRequests, config) { return await Promise.all(txRequests.map(req => estimateGasWithSafety(provider, req, config))); } /** * 构建标准交易对象 * @param options 交易选项 * @returns 交易对象 */ export function buildTransaction(options) { const tx = { from: options.from, nonce: options.nonce, gasLimit: options.gasLimit, chainId: options.chainId, }; if (options.to) tx.to = options.to; if (options.data) tx.data = options.data; if (options.value) tx.value = options.value; // 根据类型设置 gas price if (options.type === 2) { // EIP-1559 tx.type = 2; tx.maxFeePerGas = options.gasPrice; tx.maxPriorityFeePerGas = options.gasPrice / 10n; // 10% 作为优先费 } else { // Legacy tx.type = 0; tx.gasPrice = options.gasPrice; } return tx; } /** * 获取 Gas Limit * 优先使用 config.gasLimit,否则使用默认值 * multiplier */ export function getGasLimit(config, defaultGas = 500000) { if (config.gasLimit !== undefined) { return typeof config.gasLimit === 'bigint' ? config.gasLimit : BigInt(config.gasLimit); } const multiplier = config.gasLimitMultiplier ?? 1.0; return BigInt(Math.ceil(defaultGas * multiplier)); } /** * 获取 Gas Price 配置(转换为 GasPriceConfig) */ export function getGasPriceConfig(config) { const gasPriceConfig = {}; if (config.minGasPriceGwei !== undefined) { gasPriceConfig.baseGasPrice = ethers.parseUnits(String(config.minGasPriceGwei), 'gwei'); gasPriceConfig.multiplierPercent = 0; } if (config.maxGasPriceGwei !== undefined) { gasPriceConfig.maxGasPrice = ethers.parseUnits(String(config.maxGasPriceGwei), 'gwei'); } return gasPriceConfig; } /** * 获取交易类型 */ export function getTxType(config) { return config.txType ?? 0; } /** * 获取链 ID(从 provider 或 config) */ export async function getChainId(provider, configChainId) { if (configChainId !== undefined) return configChainId; const network = await provider.getNetwork(); return Number(network.chainId); } /** * 并行签名多个交易 * @param wallet 钱包 * @param transactions 交易数组 * @returns 已签名的交易数组 */ export async function signTransactionsBatch(wallet, transactions) { return await Promise.all(transactions.map(tx => wallet.signTransaction(tx))); } /** * 批量签名(支持多个钱包) * @param wallets 钱包数组 * @param transactions 交易数组(必须与钱包一一对应) * @returns 已签名的交易数组 */ export async function signTransactionsBatchMultiWallet(wallets, transactions) { if (wallets.length !== transactions.length) { throw new Error(`Wallet count (${wallets.length}) must match transaction count (${transactions.length})`); } return await Promise.all(transactions.map((tx, i) => wallets[i].signTransaction(tx))); } /** * 验证已签名交易的格式 * @param signedTxs 已签名的交易数组 * @returns 是否全部有效 */ export function validateSignedTransactions(signedTxs) { if (!signedTxs || signedTxs.length === 0) { return false; } for (const tx of signedTxs) { if (typeof tx !== 'string' || !tx.startsWith('0x')) { return false; } } return true; } // ============================================================================ // 交易时间和路径工具 // ============================================================================ import { DEFAULT_DEADLINE_MINUTES } from './constants.js'; /** * 获取交易 deadline(Unix 时间戳秒) * @param minutes 有效分钟数,默认 20 * @returns Unix 时间戳(秒) */ export function getDeadline(minutes = DEFAULT_DEADLINE_MINUTES) { return Math.floor(Date.now() / 1000) + 60 * minutes; } /** * 编码 V3 多跳 path * 格式:token0 (20 bytes) + fee (3 bytes) + token1 (20 bytes) + fee (3 bytes) + token2 (20 bytes) ... * * @param tokens 代币地址数组 [tokenIn, tokenMid1, tokenMid2, ..., tokenOut] * @param fees 费率数组 [fee0, fee1, ...] - 长度应该是 tokens.length - 1 * @returns 编码后的 path(hex string) * * @example * // WBNB → USDT(500) → TOKEN(2500) * encodeV3Path(['0xWBNB...', '0xUSDT...', '0xTOKEN...'], [500, 2500]) */ export function encodeV3Path(tokens, fees) { if (tokens.length < 2) { throw new Error('V3 path 需要至少 2 个代币'); } if (fees.length !== tokens.length - 1) { throw new Error(`费率数量 (${fees.length}) 必须等于代币数量 - 1 (${tokens.length - 1})`); } let path = '0x'; for (let i = 0; i < tokens.length; i++) { // 添加代币地址 (20 bytes) path += tokens[i].slice(2).toLowerCase().padStart(40, '0'); // 添加费率 (3 bytes) - 除了最后一个代币 if (i < fees.length) { path += fees[i].toString(16).padStart(6, '0'); } } return path; } /** * 解码 V3 path 为代币和费率数组 * @param path 编码后的 path * @returns { tokens, fees } */ export function decodeV3Path(path) { const cleanPath = path.startsWith('0x') ? path.slice(2) : path; const tokens = []; const fees = []; let offset = 0; while (offset < cleanPath.length) { // 读取代币地址 (40 hex chars = 20 bytes) tokens.push('0x' + cleanPath.slice(offset, offset + 40)); offset += 40; // 读取费率 (6 hex chars = 3 bytes) if (offset < cleanPath.length) { fees.push(parseInt(cleanPath.slice(offset, offset + 6), 16)); offset += 6; } } return { tokens, fees }; } /** * 强制 2 跳的利润转账常量 */ export const PROFIT_HOP_COUNT = 2; /** * 生成利润多跳转账交易 * * 流程(2 跳): * 1. 支付者 → 中转1(gas + 利润 + 中转1的gas) * 2. 中转1 → 中转2(gas + 利润) * 3. 中转2 → 利润地址(利润) * * @param config 配置参数 * @returns 签名交易和中转钱包私钥 */ export async function buildProfitHopTransactions(config) { const { provider, payerWallet, profitAmount, profitRecipient, hopCount = PROFIT_HOP_COUNT, gasPrice, chainId, txType, startNonce } = config; // 如果利润为 0,返回空结果 if (profitAmount <= 0n) { return { signedTransactions: [], hopWallets: [], totalNonceUsed: 0 }; } // 固定 gas limit(原生代币转账,预留少量缓冲) const nativeTransferGasLimit = 21055n; const gasFeePerHop = nativeTransferGasLimit * gasPrice; // 生成中转钱包 const hopWallets = []; const hopPrivateKeys = []; for (let i = 0; i < hopCount; i++) { const wallet = Wallet.createRandom(); hopWallets.push(new Wallet(wallet.privateKey, provider)); hopPrivateKeys.push(wallet.privateKey); } // 计算每个中转钱包需要的金额(从后往前) // 最后一个中转钱包:只需要利润 + 自己的 gas // 其他中转钱包:利润 + 自己的 gas + 下一个钱包需要的金额 const hopAmounts = []; for (let i = hopCount - 1; i >= 0; i--) { if (i === hopCount - 1) { // 最后一个中转钱包:利润 + 自己转账的 gas hopAmounts.unshift(profitAmount + gasFeePerHop); } else { // 其他中转钱包:下一个需要的金额 + 自己转账的 gas hopAmounts.unshift(hopAmounts[0] + gasFeePerHop); } } const signedTxs = []; const nonceManager = new NonceManager(provider); // 获取支付者的 nonce const payerNonce = startNonce ?? await nonceManager.getNextNonce(payerWallet); // 1. 支付者 → 第一个中转钱包(包含所有后续的 gas 和利润) const payerTx = await payerWallet.signTransaction({ to: hopWallets[0].address, value: hopAmounts[0], nonce: payerNonce, gasPrice, gasLimit: nativeTransferGasLimit, chainId, type: txType }); signedTxs.push(payerTx); // 2. 中转钱包逐层传递 for (let i = 0; i < hopCount; i++) { const fromWallet = hopWallets[i]; const isLastHop = i === hopCount - 1; const toAddress = isLastHop ? profitRecipient : hopWallets[i + 1].address; const value = isLastHop ? profitAmount : hopAmounts[i + 1]; const hopTx = await fromWallet.signTransaction({ to: toAddress, value, nonce: 0, // 中转钱包都是新生成的,nonce 从 0 开始 gasPrice, gasLimit: nativeTransferGasLimit, chainId, type: txType }); signedTxs.push(hopTx); } return { signedTransactions: signedTxs, hopWallets: hopPrivateKeys, totalNonceUsed: 1 // 支付者只用了 1 个 nonce }; }