four-flap-meme-sdk
Version:
SDK for Flap bonding curve and four.meme TokenManager
452 lines (451 loc) • 16.2 kB
JavaScript
/**
* 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
};
}