UNPKG

four-flap-meme-sdk

Version:

SDK for Flap bonding curve and four.meme TokenManager

584 lines (583 loc) 25.4 kB
import { ethers, Wallet, JsonRpcProvider } from 'ethers'; import { Club48Client, sendBatchPrivateTransactions, BUILDER_CONTROL_EOA } from '../clients/club48.js'; import { FLAP_PORTAL_ADDRESSES, FLAP_ORIGINAL_PORTAL_ADDRESSES } from './constants.js'; // Portal ABI (简化版,仅包含需要的方法) const PORTAL_ABI = [ 'function newToken(string name, string symbol, string meta) external payable returns (address)', 'function newTokenV2((string name,string symbol,string meta,uint8 dexThresh,bytes32 salt,uint16 taxRate,uint8 migratorType,address quoteToken,uint256 quoteAmt,address beneficiary,bytes permitData)) external payable returns (address)', 'function swapExactInputBuy((address inputToken,address outputToken,uint256 inputAmount,uint256 minOutputAmount,bytes permitData)) external payable returns (uint256)', 'function swapExactInputSell((address inputToken,address outputToken,uint256 inputAmount,uint256 minOutputAmount,bytes permitData)) external returns (uint256)', 'function quoteExactInput((address inputToken,address outputToken,uint256 inputAmount)) external returns (uint256)' ]; /** * Flap Protocol Bundle 交易方法 * 使用 48.club Bundle 服务实现原子化交易 */ // 错误信息国际化 const ERRORS = { NO_PRIVATE_KEY: { en: 'At least 1 private key (creator) is required', zh: '至少需要 1 个私钥(创建者)' }, AMOUNT_MISMATCH: { en: (buyCount, buyerCount) => `Buy amount count (${buyCount}) must equal buyer count (${buyerCount})`, zh: (buyCount, buyerCount) => `购买金额数量(${buyCount})必须等于买家数量(${buyerCount})` }, KEY_AMOUNT_MISMATCH: { en: 'Private key count and amount count must match', zh: '私钥和购买金额数量必须一致' }, SELL_KEY_AMOUNT_MISMATCH: { en: 'Private key count and sell amount count must match', zh: '私钥和卖出数量必须一致' } }; // 获取错误信息(根据环境语言) function getErrorMessage(key, ...args) { const lang = (process.env.LANG || process.env.LANGUAGE || 'en').toLowerCase().includes('zh') ? 'zh' : 'en'; const message = ERRORS[key][lang]; return typeof message === 'function' ? message(...args) : message; } // ======================================== // 辅助函数:估算 gas 并应用配置 // ======================================== /** * 估算 gas 并应用用户配置的倍数 */ async function estimateGasWithMultiplier(provider, txRequest, multiplier) { const estimated = await provider.estimateGas(txRequest); const mult = multiplier ?? 1.2; // 默认 20% 安全余量 return BigInt(Math.ceil(Number(estimated) * mult)); } /** * 获取交易类型(用户可配置) */ function getTxType(config) { return config.txType ?? 0; // 默认 Legacy (type 0),适用于 48.club/BSC } /** * 构建完整的交易请求(包含 gas 估算和交易类型) */ async function buildTxRequest(provider, unsigned, from, nonce, gasPrice, config, value) { const gasLimit = await estimateGasWithMultiplier(provider, { ...unsigned, from, value }, config.gasLimitMultiplier); return { ...unsigned, from, nonce, gasLimit, gasPrice, chainId: 56, type: getTxType(config) }; } /** * Flap Protocol: 创建代币 + 捆绑购买 */ export async function createTokenWithBundleBuy(params) { const { chain, privateKeys, buyAmounts, tokenInfo, quoteToken, tokenAddress, config } = params; // 默认使用 BNB (0地址) const quoteTokenAddress = quoteToken || '0x0000000000000000000000000000000000000000'; if (privateKeys.length === 0) { throw new Error(getErrorMessage('NO_PRIVATE_KEY')); } if (buyAmounts.length !== privateKeys.length - 1) { throw new Error(getErrorMessage('AMOUNT_MISMATCH', buyAmounts.length, privateKeys.length - 1)); } const provider = new JsonRpcProvider(config.rpcUrl); const devWallet = new Wallet(privateKeys[0], provider); const club48 = new Club48Client({ endpoint: config.club48Endpoint, explorerEndpoint: config.club48ExplorerEndpoint }); const blockOffset = config.bundleBlockOffset ?? 100; // 1. 构建交易(为重复地址分配顺序 nonce) const portalAddr = FLAP_PORTAL_ADDRESSES[chain]; const originalPortalAddr = FLAP_ORIGINAL_PORTAL_ADDRESSES[chain]; const gasPrice = await club48.getMinGasPrice(); const signedTxs = []; const nextNonceMap = new Map(); const getNextNonce = async (w) => { const key = w.address.toLowerCase(); const cached = nextNonceMap.get(key); if (cached !== undefined) { const n = cached; nextNonceMap.set(key, cached + 1); return n; } const onchain = await w.getNonce(); nextNonceMap.set(key, onchain + 1); return onchain; }; // 1.1 创建交易(改为在原始 Portal 上执行 V2 创建,支持 salt/vanity) const portal = new ethers.Contract(originalPortalAddr, PORTAL_ABI, devWallet); const createTxUnsigned = await portal.newTokenV2.populateTransaction({ name: tokenInfo.name, symbol: tokenInfo.symbol, meta: tokenInfo.meta, dexThresh: (params.dexThresh ?? 1) & 0xff, salt: params.salt ?? '0x' + '00'.repeat(32), taxRate: (params.taxRate ?? 0) & 0xffff, migratorType: (params.migratorType ?? 0) & 0xff, quoteToken: params.quoteToken || '0x0000000000000000000000000000000000000000', quoteAmt: 0n, beneficiary: new Wallet(privateKeys[0]).address, permitData: '0x' }); // 估算 gas 并应用用户配置的安全余量 const createGasLimit = await estimateGasWithMultiplier(provider, { ...createTxUnsigned, from: devWallet.address }, config.gasLimitMultiplier); const createTxRequest = { ...createTxUnsigned, from: devWallet.address, nonce: await getNextNonce(devWallet), gasLimit: createGasLimit, gasPrice: gasPrice, chainId: 56, // BSC type: getTxType(config) // 用户可配置:0=Legacy(默认),2=EIP-1559 }; const signedCreateTx = await devWallet.signTransaction(createTxRequest); signedTxs.push(signedCreateTx); // 可选:添加 tipTx 提升排序 if (config.tipAmountWei && config.tipAmountWei > 0n) { const tipTx = await devWallet.signTransaction({ to: BUILDER_CONTROL_EOA, value: config.tipAmountWei, gasPrice, gasLimit: 21000n, chainId: 56, type: 0, // Legacy transaction nonce: await getNextNonce(devWallet) }); signedTxs.push(tipTx); } // 1.2 购买交易 const buyTxs = []; for (let i = 0; i < buyAmounts.length; i++) { const buyerWallet = new Wallet(privateKeys[i + 1], provider); const amountIn = ethers.parseEther(buyAmounts[i]); // 滑点保护(默认 1%) const slippageBps = Math.max(0, Math.min(5000, params.config.slippageBps ?? 100)); let minAmountOut = 0n; try { const quote = new ethers.Contract(portalAddr, PORTAL_ABI, buyerWallet); const exactOut = await quote.quoteExactInput.staticCall({ inputToken: '0x0000000000000000000000000000000000000000', outputToken: tokenAddress, inputAmount: amountIn }); const keep = BigInt(10000 - slippageBps); minAmountOut = (exactOut * keep) / 10000n; } catch { } const buyPortal = new ethers.Contract(portalAddr, PORTAL_ABI, buyerWallet); const buyTxUnsigned = await buyPortal.swapExactInputBuy.populateTransaction({ inputToken: '0x0000000000000000000000000000000000000000', outputToken: tokenAddress, inputAmount: 0n, minOutputAmount: minAmountOut, permitData: '0x', }, { value: amountIn }); // 估算 gas 并应用用户配置的安全余量;若因代币尚未创建(同一 bundle 内先创建后购买)导致预估回滚,则使用保守兜底值 let buyGasLimit; try { buyGasLimit = await estimateGasWithMultiplier(provider, { ...buyTxUnsigned, from: buyerWallet.address, value: amountIn }, config.gasLimitMultiplier); } catch { // 兜底:在 BSC 上对 swapExactInputBuy 使用相对保守的 gas 上限 buyGasLimit = 600000n; } const buyTxRequest = { ...buyTxUnsigned, from: buyerWallet.address, nonce: await getNextNonce(buyerWallet), gasLimit: buyGasLimit, gasPrice: gasPrice, chainId: 56, // BSC type: getTxType(config) // 用户可配置 }; const signedBuyTx = await buyerWallet.signTransaction(buyTxRequest); signedTxs.push(signedBuyTx); buyTxs.push(signedBuyTx); } // 2. 提交 Bundle const bundleUuid = await club48.sendBundle({ txs: signedTxs, maxBlockNumber: (await provider.getBlockNumber()) + blockOffset, noMerge: config.noMerge }, { spMode: params.config.spMode ?? 'timestampPersonalSign', spPrivateKey: params.config.spPrivateKey || privateKeys[0], spVMode: params.config.spVMode }); // 3. 等待完成 const status = await club48.waitForBundle(bundleUuid, params.config.waitTimeoutMs ?? 60000); return { bundleUuid, tokenAddress, status, createTx: signedCreateTx, buyTxs }; } /** * Flap Protocol: 批量购买 */ export async function batchBuyWithBundle(params) { const { chain, privateKeys, buyAmounts, tokenAddress, config } = params; if (privateKeys.length === 0 || buyAmounts.length !== privateKeys.length) { throw new Error(getErrorMessage('KEY_AMOUNT_MISMATCH')); } const provider = new JsonRpcProvider(config.rpcUrl); const club48 = new Club48Client({ endpoint: config.club48Endpoint, explorerEndpoint: config.club48ExplorerEndpoint }); const blockOffset = config.bundleBlockOffset ?? 100; const portalAddr = FLAP_PORTAL_ADDRESSES[chain]; const gasPrice = await club48.getMinGasPrice(); const signedTxs = []; const nextNonceMap = new Map(); const getNextNonce = async (w) => { const key = w.address.toLowerCase(); const cached = nextNonceMap.get(key); if (cached !== undefined) { const n = cached; nextNonceMap.set(key, cached + 1); return n; } const onchain = await w.getNonce(); nextNonceMap.set(key, onchain + 1); return onchain; }; // 可选:添加 tipTx 提升排序(在第一位) if (config.tipAmountWei && config.tipAmountWei > 0n) { const tipWallet = new Wallet(privateKeys[0], provider); const tipTx = await tipWallet.signTransaction({ to: BUILDER_CONTROL_EOA, value: config.tipAmountWei, gasPrice, gasLimit: 21000n, chainId: 56, type: 0, // Legacy transaction nonce: await getNextNonce(tipWallet) }); signedTxs.unshift(tipTx); } for (let i = 0; i < privateKeys.length; i++) { const buyerWallet = new Wallet(privateKeys[i], provider); const amountIn = ethers.parseEther(buyAmounts[i]); // 滑点保护(默认 1%) const slippageBps = Math.max(0, Math.min(5000, params.config.slippageBps ?? 100)); let minAmountOut = 0n; // 兼容非关键失败 try { const quote = new ethers.Contract(portalAddr, PORTAL_ABI, buyerWallet); const exactOut = await quote.quoteExactInput.staticCall({ inputToken: '0x0000000000000000000000000000000000000000', outputToken: tokenAddress, inputAmount: amountIn }); const keep = BigInt(10000 - slippageBps); minAmountOut = (exactOut * keep) / 10000n; } catch { } const portal = new ethers.Contract(portalAddr, PORTAL_ABI, buyerWallet); const buyTxUnsigned = await portal.swapExactInputBuy.populateTransaction({ inputToken: '0x0000000000000000000000000000000000000000', outputToken: tokenAddress, inputAmount: 0n, minOutputAmount: minAmountOut, permitData: '0x', }, { value: amountIn }); // 估算 gas 并添加 20% 安全余量 const estimatedGas = await provider.estimateGas({ ...buyTxUnsigned, from: buyerWallet.address, value: amountIn }); const gasLimit = (estimatedGas * 120n) / 100n; const buyTxRequest = { ...buyTxUnsigned, from: buyerWallet.address, nonce: await getNextNonce(buyerWallet), gasLimit: gasLimit, gasPrice: gasPrice, chainId: 56, // BSC type: 0 // Legacy transaction }; const signedBuyTx = await buyerWallet.signTransaction(buyTxRequest); signedTxs.push(signedBuyTx); } const bundleUuid = await club48.sendBundle({ txs: signedTxs, maxBlockNumber: (await provider.getBlockNumber()) + blockOffset, noMerge: config.noMerge }, { spMode: params.config.spMode ?? 'timestampPersonalSign', spPrivateKey: params.config.spPrivateKey || privateKeys[0], spVMode: params.config.spVMode }); const status = await club48.waitForBundle(bundleUuid); return { bundleUuid, status, buyTxs: signedTxs }; } /** * Flap Protocol: 批量卖出 */ export async function batchSellWithBundle(params) { const { chain, privateKeys, sellAmounts, tokenAddress, config } = params; if (privateKeys.length === 0 || sellAmounts.length !== privateKeys.length) { throw new Error(getErrorMessage('SELL_KEY_AMOUNT_MISMATCH')); } const provider = new JsonRpcProvider(config.rpcUrl); const club48 = new Club48Client({ endpoint: config.club48Endpoint, explorerEndpoint: config.club48ExplorerEndpoint }); const blockOffset = config.bundleBlockOffset ?? 100; const portalAddr = FLAP_PORTAL_ADDRESSES[chain]; const gasPrice = await club48.getMinGasPrice(); const signedTxs = []; const nextNonceMap = new Map(); const getNextNonce = async (w) => { const key = w.address.toLowerCase(); const cached = nextNonceMap.get(key); if (cached !== undefined) { const n = cached; nextNonceMap.set(key, cached + 1); return n; } const onchain = await w.getNonce(); nextNonceMap.set(key, onchain + 1); return onchain; }; // 可选 tipTx if (config.tipAmountWei && config.tipAmountWei > 0n) { const tipWallet = new Wallet(privateKeys[0], provider); const tipTx = await tipWallet.signTransaction({ to: BUILDER_CONTROL_EOA, value: config.tipAmountWei, gasPrice, gasLimit: 21000n, chainId: 56, type: 0, // Legacy transaction nonce: await getNextNonce(tipWallet) }); signedTxs.unshift(tipTx); } for (let i = 0; i < privateKeys.length; i++) { const sellerWallet = new Wallet(privateKeys[i], provider); const amountIn = ethers.parseUnits(sellAmounts[i], 18); const minAmountOut = 0n; // TODO: 调用 quoteExactInput const portal = new ethers.Contract(portalAddr, PORTAL_ABI, sellerWallet); const sellTxUnsigned = await portal.swapExactInputSell.populateTransaction({ inputToken: tokenAddress, outputToken: '0x0000000000000000000000000000000000000000', inputAmount: amountIn, minOutputAmount: minAmountOut, permitData: '0x', }); // 估算 gas 并添加 20% 安全余量 const estimatedGas = await provider.estimateGas({ ...sellTxUnsigned, from: sellerWallet.address }); const gasLimit = (estimatedGas * 120n) / 100n; const sellTxRequest = { ...sellTxUnsigned, from: sellerWallet.address, nonce: await getNextNonce(sellerWallet), gasLimit: gasLimit, gasPrice: gasPrice, chainId: 56, // BSC type: 0 // Legacy transaction }; const signedSellTx = await sellerWallet.signTransaction(sellTxRequest); signedTxs.push(signedSellTx); } const bundleUuid = await club48.sendBundle({ txs: signedTxs, maxBlockNumber: (await provider.getBlockNumber()) + blockOffset, noMerge: config.noMerge }, { spMode: params.config.spMode ?? 'timestampPersonalSign', spPrivateKey: params.config.spPrivateKey || privateKeys[0], spVMode: params.config.spVMode }); const status = await club48.waitForBundle(bundleUuid); return { bundleUuid, status, sellTxs: signedTxs }; } export async function flapPrivateBuy(params) { const { chain, privateKey, tokenAddress, amountIn, to, config, spPrivateKey } = params; const provider = new JsonRpcProvider(config.rpcUrl); const wallet = new Wallet(privateKey, provider); const club48 = new Club48Client({ endpoint: config.club48Endpoint, explorerEndpoint: config.club48ExplorerEndpoint }); const portalAddr = FLAP_PORTAL_ADDRESSES[chain]; const gasPrice = await club48.getMinGasPrice(); const amountWei = ethers.parseEther(amountIn); const minAmountOut = 0n; // 不加滑点 const portal = new ethers.Contract(portalAddr, PORTAL_ABI, wallet); const unsigned = await portal.swapExactInputBuy.populateTransaction({ inputToken: '0x0000000000000000000000000000000000000000', outputToken: tokenAddress, inputAmount: 0n, minOutputAmount: minAmountOut, permitData: '0x', }, { value: amountWei }); // 估算 gas 并添加 20% 安全余量 const estimatedGas = await provider.estimateGas({ ...unsigned, from: wallet.address, value: amountWei }); const gasLimit = (estimatedGas * 120n) / 100n; const req = { ...unsigned, from: wallet.address, nonce: await wallet.getNonce(), gasLimit: gasLimit, gasPrice, chainId: 56, type: 0 // Legacy transaction }; const signed = await wallet.signTransaction(req); if (spPrivateKey) return await club48.sendPrivateTransactionWith48SP(signed, spPrivateKey); return await club48.sendPrivateTransaction(signed); } export async function flapPrivateSell(params) { const { chain, privateKey, tokenAddress, amount, minEth, config, spPrivateKey } = params; const provider = new JsonRpcProvider(config.rpcUrl); const wallet = new Wallet(privateKey, provider); const club48 = new Club48Client({ endpoint: config.club48Endpoint, explorerEndpoint: config.club48ExplorerEndpoint }); const portalAddr = FLAP_PORTAL_ADDRESSES[chain]; const gasPrice = await club48.getMinGasPrice(); const amountWei = ethers.parseUnits(amount, 18); const minOut = minEth ?? 0n; const portal = new ethers.Contract(portalAddr, PORTAL_ABI, wallet); const unsigned = await portal.swapExactInputSell.populateTransaction({ inputToken: tokenAddress, outputToken: '0x0000000000000000000000000000000000000000', inputAmount: amountWei, minOutputAmount: minOut, permitData: '0x', }); // 估算 gas 并添加 20% 安全余量 const estimatedGas = await provider.estimateGas({ ...unsigned, from: wallet.address }); const gasLimit = (estimatedGas * 120n) / 100n; const req = { ...unsigned, from: wallet.address, nonce: await wallet.getNonce(), gasLimit: gasLimit, gasPrice, chainId: 56, type: 0 // Legacy transaction }; const signed = await wallet.signTransaction(req); if (spPrivateKey) return await club48.sendPrivateTransactionWith48SP(signed, spPrivateKey); return await club48.sendPrivateTransaction(signed); } export async function flapBatchPrivateBuy(params) { const { chain, privateKeys, amountsIn, tokenAddress, config, spPrivateKey } = params; if (privateKeys.length !== amountsIn.length) throw new Error('privateKeys and amountsIn length mismatch'); const provider = new JsonRpcProvider(config.rpcUrl); const club48 = new Club48Client({ endpoint: config.club48Endpoint, explorerEndpoint: config.club48ExplorerEndpoint }); const portalAddr = FLAP_PORTAL_ADDRESSES[chain]; const gasPrice = await club48.getMinGasPrice(); const signedTxs = []; const nextNonceMap = new Map(); const getNextNonce = async (w) => { const key = w.address.toLowerCase(); const cached = nextNonceMap.get(key); if (cached !== undefined) { const n = cached; nextNonceMap.set(key, cached + 1); return n; } const onchain = await w.getNonce(); nextNonceMap.set(key, onchain + 1); return onchain; }; for (let i = 0; i < privateKeys.length; i++) { const w = new Wallet(privateKeys[i], provider); const amountWei = ethers.parseEther(amountsIn[i]); const portal = new ethers.Contract(portalAddr, PORTAL_ABI, w); const unsigned = await portal.swapExactInputBuy.populateTransaction({ inputToken: '0x0000000000000000000000000000000000000000', outputToken: tokenAddress, inputAmount: 0n, minOutputAmount: 0n, permitData: '0x', }, { value: amountWei }); // 估算 gas 并添加 20% 安全余量 const estimatedGas = await provider.estimateGas({ ...unsigned, from: w.address, value: amountWei }); const gasLimit = (estimatedGas * 120n) / 100n; const req = { ...unsigned, from: w.address, nonce: await getNextNonce(w), gasLimit, gasPrice, chainId: 56, type: 0 }; signedTxs.push(await w.signTransaction(req)); } return await sendBatchPrivateTransactions(signedTxs, spPrivateKey, config.club48Endpoint || 'https://puissant-bsc.48.club'); } export async function flapBatchPrivateSell(params) { const { chain, privateKeys, amounts, tokenAddress, config, spPrivateKey, minEthEach } = params; if (privateKeys.length !== amounts.length) throw new Error('privateKeys and amounts length mismatch'); const provider = new JsonRpcProvider(config.rpcUrl); const club48 = new Club48Client({ endpoint: config.club48Endpoint, explorerEndpoint: config.club48ExplorerEndpoint }); const portalAddr = FLAP_PORTAL_ADDRESSES[chain]; const gasPrice = await club48.getMinGasPrice(); const minOut = minEthEach ?? 0n; const signedTxs = []; const nextNonceMap = new Map(); const getNextNonce = async (w) => { const key = w.address.toLowerCase(); const cached = nextNonceMap.get(key); if (cached !== undefined) { const n = cached; nextNonceMap.set(key, cached + 1); return n; } const onchain = await w.getNonce(); nextNonceMap.set(key, onchain + 1); return onchain; }; for (let i = 0; i < privateKeys.length; i++) { const w = new Wallet(privateKeys[i], provider); const amountWei = ethers.parseUnits(amounts[i], 18); const portal = new ethers.Contract(portalAddr, PORTAL_ABI, w); const unsigned = await portal.swapExactInputSell.populateTransaction({ inputToken: tokenAddress, outputToken: '0x0000000000000000000000000000000000000000', inputAmount: amountWei, minOutputAmount: minOut, permitData: '0x', }); // 估算 gas 并添加 20% 安全余量 const estimatedGas = await provider.estimateGas({ ...unsigned, from: w.address }); const gasLimit = (estimatedGas * 120n) / 100n; const req = { ...unsigned, from: w.address, nonce: await getNextNonce(w), gasLimit, gasPrice, chainId: 56, type: 0 }; signedTxs.push(await w.signTransaction(req)); } // 使用 bundle 通道提交(统一与捆绑买保持一致) const blockOffset = config.bundleBlockOffset ?? 100; const bundleUuid = await club48.sendBundle({ txs: signedTxs, maxBlockNumber: (await provider.getBlockNumber()) + blockOffset, noMerge: config.noMerge }, { // 若传入 sp 配置,则用于生成 48SP 签名提升打包优先级(与批量买一致) spMode: params.config.spMode ?? 'timestampPersonalSign', spPrivateKey: params.config.spPrivateKey || 'https://puissant-bsc.48.club', spVMode: params.config.spVMode }); try { const status = await club48.waitForBundle(bundleUuid); return { submitted: true, bundleUuid, status, sellTxs: signedTxs }; } catch { // 超时或查询失败:为避免误报,返回已提交与原始交易,状态暂缺 return { submitted: true, bundleUuid, sellTxs: signedTxs }; } }