four-flap-meme-sdk
Version:
SDK for Flap bonding curve and four.meme TokenManager
332 lines (331 loc) • 12.5 kB
JavaScript
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 });
}