UNPKG

four-flap-meme-sdk

Version:

SDK for Flap bonding curve and four.meme TokenManager

332 lines (331 loc) 12.5 kB
import axios from 'axios'; import { ethers } from 'ethers'; /** * 48.club Bundle 客户端 */ export class Club48Client { constructor(config) { this.endpoint = config?.endpoint || 'https://puissant-bsc.48.club'; this.explorerEndpoint = config?.explorerEndpoint || 'https://explore.48.club'; } /** * 生成 48 Soul Point 签名 * 用于提升会员等级(Signed Guest 及以上) * * @param privateKey 私钥 * @param txs 已签名的交易数组 * @returns 签名字符串 */ static sign48SoulPoint(privateKey, txs) { // 1. 计算每笔交易的哈希 const txHashes = txs.map(tx => ethers.keccak256(tx)); // 2. 连接所有哈希 const concatenated = ethers.concat(txHashes); // 3. 对连接后的数据进行哈希 const messageHash = ethers.keccak256(concatenated); // 4. 使用私钥签名 const wallet = new ethers.Wallet(privateKey); const signature = wallet.signingKey.sign(messageHash).serialized; return signature; } /** * 生成 48SP 签名(timestamp personal_sign 模式) * 常见规则:对当前 Unix 秒级时间戳的字符串做 personal_sign */ static async sign48SoulPointTimestamp(privateKey, timestamp) { const wallet = new ethers.Wallet(privateKey); const ts = String(timestamp ?? Math.floor(Date.now() / 1000)); // ethers v6: signMessage 做 EIP-191 personal_sign return await wallet.signMessage(ts); } /** * 生成 48SP 签名(rawTimestamp 裸签 keccak(timestampString)) */ static sign48SoulPointRawTimestamp(privateKey, timestamp) { const ts = String(timestamp ?? Math.floor(Date.now() / 1000)); const msgHash = ethers.keccak256(ethers.toUtf8Bytes(ts)); const wallet = new ethers.Wallet(privateKey); return wallet.signingKey.sign(msgHash).serialized; } /** * 规范化签名 v 值 * mode = '27_28':将 0/1 规范为 27/28 * mode = '0_1':将 27/28 规范为 0/1 */ static normalizeSignatureV(signature, mode = '27_28') { const bytes = ethers.getBytes(signature); if (bytes.length !== 65) return signature; const v = bytes[64]; let v2 = v; if (mode === '27_28') { if (v === 0 || v === 1) v2 = (v + 27); } else { if (v === 27 || v === 28) v2 = (v - 27); } if (v2 === v) return signature; const out = new Uint8Array(65); out.set(bytes.subarray(0, 64), 0); out[64] = v2; return ethers.hexlify(out); } /** * 发送 Bundle(Puissant) * * @param params Bundle 参数 * @returns Bundle UUID */ async sendBundle(params, opts) { const maxTimestamp = params.maxTimestamp || Math.floor(Date.now() / 1000) + 300; // 默认 5 分钟 const bundleParams = { txs: params.txs }; if (typeof params.maxBlockNumber === 'number') { bundleParams.maxBlockNumber = params.maxBlockNumber; } else { bundleParams.maxTimestamp = maxTimestamp; } // 兼容两种字段名 if (params.revertingTxHashes && params.revertingTxHashes.length > 0) { bundleParams.revertingTxHashes = params.revertingTxHashes; } else if (params.acceptReverting && params.acceptReverting.length > 0) { bundleParams.revertingTxHashes = params.acceptReverting; } else { bundleParams.revertingTxHashes = []; } if (typeof params.noMerge === 'boolean') { bundleParams.noMerge = params.noMerge; } // 添加 Soul Point 签名(优先:调用方直接提供;否则按 opts 生成) if (params.soulPointSignature) { bundleParams["48spSign"] = params.soulPointSignature; } else if (opts && opts.spMode && opts.spMode !== 'none') { if (!opts.spPrivateKey) { throw new Error('48SP mode enabled but spPrivateKey is missing'); } let sig = ''; if (opts.spMode === 'timestampPersonalSign') { sig = await Club48Client.sign48SoulPointTimestamp(opts.spPrivateKey, opts.timestamp); } else if (opts.spMode === 'concatTxHash') { sig = Club48Client.sign48SoulPoint(opts.spPrivateKey, params.txs); } else if (opts.spMode === 'rawTimestamp') { sig = Club48Client.sign48SoulPointRawTimestamp(opts.spPrivateKey, opts.timestamp); } if (opts.spVMode) sig = Club48Client.normalizeSignatureV(sig, opts.spVMode); bundleParams["48spSign"] = sig; } // 添加 backrun 目标(如果提供) if (params.backrunTarget) { bundleParams.backrunTarget = params.backrunTarget; } const response = await axios.post(this.endpoint, { jsonrpc: "2.0", id: 1, method: "eth_sendBundle", params: [bundleParams] }); if (response.data.error) { throw new Error(`48.club bundle failed: ${response.data.error.message}`); } return response.data.result; } /** * 发送私有交易(单笔) * * @param signedTx 已签名的原始交易 * @returns 交易哈希 */ async sendPrivateTransaction(signedTx) { const response = await axios.post(this.endpoint, { jsonrpc: "2.0", id: 1, method: "eth_sendPrivateRawTransaction", params: [signedTx] }); if (response.data.error) { throw new Error(`48.club tx failed: ${response.data.error.message}`); } return response.data.result; } /** * 发送单笔私有交易(含 48SP) * 文档参考:eth_sendPrivateTransactionWith48SP * 规则:使用私钥对当前 Unix 时间戳字符串 personal_sign,作为 48SoulPointMemberSignature * * @param signedTx 已签名原始交易 * @param spPrivateKey 具有 SoulPoint 会员资格的钱包私钥 * @returns 交易哈希 */ async sendPrivateTransactionWith48SP(signedTx, spPrivateKey, opts) { // 1) 生成 48SP 签名(默认使用 timestamp personal_sign) const mode = opts?.spMode ?? 'timestampPersonalSign'; let signature = ''; if (mode === 'timestampPersonalSign') { signature = await Club48Client.sign48SoulPointTimestamp(spPrivateKey, opts?.timestamp); } else { signature = Club48Client.sign48SoulPoint(spPrivateKey, [signedTx]); } if (opts?.spVMode) signature = Club48Client.normalizeSignatureV(signature, opts.spVMode); // 2) 调用 48.club RPC const response = await axios.post(this.endpoint, { jsonrpc: "2.0", id: 1, method: "eth_sendPrivateTransactionWith48SP", params: [signedTx, signature] }); if (response.data.error) { throw new Error(`48.club tx failed: ${response.data.error.message}`); } return response.data.result; } /** * 获取最低 gas price * * @returns 最低 gas price (wei) */ async getMinGasPrice() { const response = await axios.post(this.endpoint, { jsonrpc: "2.0", id: 1, method: "eth_gasPrice" }); if (response.data.error) { throw new Error(`Get gas price failed: ${response.data.error.message}`); } return BigInt(response.data.result); } /** * 查询 Bundle 状态 * 文档参考:https://docs.48.club/puissant-builder/bundle-submission-and-on-chain-status-query * * @param bundleHash Bundle Hash (从 eth_sendBundle 返回的结果) * @returns Bundle 状态 */ async getBundleStatus(bundleHash) { const response = await axios.get(`${this.explorerEndpoint}/v2/bundle?hash=${bundleHash}`); return response.data; } /** * 等待 Bundle 完成 * * @param bundleHash Bundle Hash (从 eth_sendBundle 返回的结果) * @param timeout 超时时间(毫秒),默认 60 秒 * @returns Bundle 状态 */ async waitForBundle(bundleHash, timeout = 60000) { const startTime = Date.now(); let lastStatus = null; while (Date.now() - startTime < timeout) { try { const status = await this.getBundleStatus(bundleHash); lastStatus = status; if (status.status === 'INCLUDED') { return status; } if (status.status === 'FAILED') { throw new Error(`Bundle failed to execute: ${JSON.stringify(status)}`); } // 等待 3 秒后重试 await new Promise(resolve => setTimeout(resolve, 3000)); } catch (error) { // 如果是 404 或网络错误,继续等待(可能是 explorer 还没索引到) if (error.response?.status === 404 || error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') { await new Promise(resolve => setTimeout(resolve, 3000)); continue; } // 其他错误,如果有最后状态且为 INCLUDED,返回成功 if (lastStatus?.status === 'INCLUDED') { return lastStatus; } throw error; } } // 超时:如果最后状态是 INCLUDED,返回成功;否则返回超时但带有最后状态信息 if (lastStatus?.status === 'INCLUDED') { return lastStatus; } // 如果有最后状态,返回它;否则创建一个 PENDING 状态 if (lastStatus) { return lastStatus; } // 创建一个基本的 PENDING 状态 return { status: 'PENDING', hash: bundleHash, maxTimestamp: Math.floor(Date.now() / 1000) + 300, transaction: { from: '0x0000000000000000000000000000000000000000', gasPrice: '0', nonce: 0 } }; } } /** * Builder Control EOA(用于提升排序权重的“小费”收款地址) * 文档: https://docs.48.club/puissant-builder/send-bundle */ export const BUILDER_CONTROL_EOA = '0x4848489f0b2BEdd788c696e2D79b6b69D7484848'; export function createAuctionFeedClient(wsUrl, callbacks) { // 仅导出接口占位,避免引入 ws 依赖;用户可在应用层实现 return { connect: () => { callbacks.onOpen && callbacks.onOpen(); }, close: () => { callbacks.onClose && callbacks.onClose(); } }; } /** * 批量私有交易辅助函数 */ export async function sendBatchPrivateTransactions(signedTxs, spPrivateKey, endpoint = 'https://puissant-bsc.48.club', opts) { // 1. 生成 48SP 签名 const mode = opts?.spMode ?? 'timestampPersonalSign'; let signature = ''; if (mode === 'timestampPersonalSign') { signature = await Club48Client.sign48SoulPointTimestamp(spPrivateKey, opts?.timestamp); } else { signature = Club48Client.sign48SoulPoint(spPrivateKey, signedTxs); } if (opts?.spVMode) signature = Club48Client.normalizeSignatureV(signature, opts.spVMode); // 2. 批量提交 const response = await axios.post(endpoint, { jsonrpc: "2.0", id: 1, method: "eth_sendBatchPrivateTransactionsWith48SP", params: [ signedTxs, // 已签名的交易数组 signature // 48SP 签名 ] }); // 注意:批量方法不返回交易哈希,只确认接收 if (response.data.error) { throw new Error(response.data.error.message); } return true; } /** * 发送 backrun bundle(便捷方法) * - 自动填充 backrunTarget 参数 * - 其他参数透传 * 文档参考: Auction Transaction Feed / SendBundle with backrunTarget */ export async function sendBackrunBundle(client, params) { return await client.sendBundle({ ...params, backrunTarget: params.backrunTarget }); }