four-flap-meme-sdk 1.4.91 → 1.4.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +24 -0
- package/dist/xlayer/aa-account.d.ts +240 -0
- package/dist/xlayer/aa-account.js +510 -0
- package/dist/xlayer/bundle.d.ts +93 -0
- package/dist/xlayer/bundle.js +472 -0
- package/dist/xlayer/bundler.d.ts +111 -0
- package/dist/xlayer/bundler.js +162 -0
- package/dist/xlayer/constants.d.ts +75 -0
- package/dist/xlayer/constants.js +145 -0
- package/dist/xlayer/dex.d.ts +132 -0
- package/dist/xlayer/dex.js +335 -0
- package/dist/xlayer/examples/bundle-buy-sell.d.ts +11 -0
- package/dist/xlayer/examples/bundle-buy-sell.js +194 -0
- package/dist/xlayer/index.d.ts +75 -0
- package/dist/xlayer/index.js +130 -0
- package/dist/xlayer/portal-ops.d.ts +152 -0
- package/dist/xlayer/portal-ops.js +239 -0
- package/dist/xlayer/types.d.ts +298 -0
- package/dist/xlayer/types.js +6 -0
- package/dist/xlayer/volume.d.ts +65 -0
- package/dist/xlayer/volume.js +235 -0
- package/package.json +1 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer 捆绑交易 SDK
|
|
3
|
+
*
|
|
4
|
+
* 通过 ERC-4337 handleOps 实现多个地址的原子化交易:
|
|
5
|
+
* - 捆绑买入:多个 sender 同一笔 handleOps 买入
|
|
6
|
+
* - 捆绑卖出:多个 sender 同一笔 handleOps 卖出
|
|
7
|
+
* - 买卖一体化:买入 -> 授权 -> 卖出 -> 归集
|
|
8
|
+
* - OKB 归集:将 sender 的 OKB 转回 owner
|
|
9
|
+
*/
|
|
10
|
+
import { Interface, Contract } from 'ethers';
|
|
11
|
+
import { FLAP_PORTAL, ENTRYPOINT_ABI, DEFAULT_CALL_GAS_LIMIT_SELL, DEFAULT_WITHDRAW_RESERVE, } from './constants.js';
|
|
12
|
+
import { AAAccountManager, encodeExecute, createWallet } from './aa-account.js';
|
|
13
|
+
import { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, PortalQuery, parseOkb, formatOkb, } from './portal-ops.js';
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// 捆绑交易执行器
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* XLayer 捆绑交易执行器
|
|
19
|
+
*
|
|
20
|
+
* 核心设计原则:
|
|
21
|
+
* 1. 用户只需操控 AA 地址(Sender)
|
|
22
|
+
* 2. 如果指定了 Paymaster,不需要往 AA 转手续费
|
|
23
|
+
* 3. 多个 UserOp 放入同一笔 handleOps 保证原子性
|
|
24
|
+
*/
|
|
25
|
+
export class BundleExecutor {
|
|
26
|
+
constructor(config = {}) {
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.aaManager = new AAAccountManager(config);
|
|
29
|
+
this.portalQuery = new PortalQuery({
|
|
30
|
+
rpcUrl: config.rpcUrl,
|
|
31
|
+
chainId: config.chainId,
|
|
32
|
+
});
|
|
33
|
+
this.portalAddress = FLAP_PORTAL;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 获取 AA 管理器
|
|
37
|
+
*/
|
|
38
|
+
getAAManager() {
|
|
39
|
+
return this.aaManager;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 获取 Portal 查询器
|
|
43
|
+
*/
|
|
44
|
+
getPortalQuery() {
|
|
45
|
+
return this.portalQuery;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 执行 handleOps 并解析结果
|
|
49
|
+
*/
|
|
50
|
+
async runHandleOps(label, ops, bundlerSigner, beneficiary) {
|
|
51
|
+
if (ops.length === 0) {
|
|
52
|
+
console.log(`\n[${label}] 没有 ops,跳过`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const provider = this.aaManager.getProvider();
|
|
56
|
+
const entryPointAddress = this.aaManager.getEntryPointAddress();
|
|
57
|
+
const feeData = await provider.getFeeData();
|
|
58
|
+
console.log(`\n[${label}] 发送 handleOps,ops=${ops.length} ...`);
|
|
59
|
+
// 使用 bundlerSigner 创建新的合约实例以调用 handleOps
|
|
60
|
+
const entryPointWithSigner = new Contract(entryPointAddress, ENTRYPOINT_ABI, bundlerSigner);
|
|
61
|
+
const tx = await entryPointWithSigner.handleOps(ops, beneficiary, { gasPrice: feeData.gasPrice ?? 100000000n });
|
|
62
|
+
console.log(`[${label}] txHash:`, tx.hash);
|
|
63
|
+
const receipt = await tx.wait();
|
|
64
|
+
console.log(`[${label}] mined block=${receipt.blockNumber} status=${receipt.status}`);
|
|
65
|
+
// 解析 UserOperationEvent
|
|
66
|
+
const epIface = new Interface(ENTRYPOINT_ABI);
|
|
67
|
+
const userOpEvents = [];
|
|
68
|
+
for (const log of receipt.logs) {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = epIface.parseLog(log);
|
|
71
|
+
if (parsed?.name === 'UserOperationEvent') {
|
|
72
|
+
const e = parsed.args;
|
|
73
|
+
userOpEvents.push({
|
|
74
|
+
userOpHash: e.userOpHash,
|
|
75
|
+
sender: e.sender,
|
|
76
|
+
paymaster: e.paymaster,
|
|
77
|
+
success: e.success,
|
|
78
|
+
actualGasCost: e.actualGasCost,
|
|
79
|
+
actualGasUsed: e.actualGasUsed,
|
|
80
|
+
});
|
|
81
|
+
console.log(` - userOpHash=${e.userOpHash} sender=${e.sender} success=${e.success} gasUsed=${e.actualGasUsed}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
}
|
|
86
|
+
// 检查是否有失败的 UserOp
|
|
87
|
+
const failed = userOpEvents.filter((e) => !e.success);
|
|
88
|
+
if (failed.length > 0) {
|
|
89
|
+
console.warn(`[${label}] 有 ${failed.length} 个 UserOp 执行失败`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
txHash: tx.hash,
|
|
93
|
+
blockNumber: receipt.blockNumber,
|
|
94
|
+
status: receipt.status,
|
|
95
|
+
userOpEvents,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 构建买入 UserOp
|
|
100
|
+
*/
|
|
101
|
+
async buildBuyUserOp(ownerWallet, tokenAddress, buyAmountWei, ownerName) {
|
|
102
|
+
const accountInfo = await this.aaManager.getAccountInfo(ownerWallet.address);
|
|
103
|
+
const initCode = accountInfo.deployed
|
|
104
|
+
? '0x'
|
|
105
|
+
: this.aaManager.generateInitCode(ownerWallet.address);
|
|
106
|
+
// 构建 callData: execute(portal, value=buyAmountWei, swapExactInput)
|
|
107
|
+
const swapData = encodeBuyCall(tokenAddress, buyAmountWei, 0n);
|
|
108
|
+
const callData = encodeExecute(this.portalAddress, buyAmountWei, swapData);
|
|
109
|
+
// 估算前确保 sender 有足够余额(用于模拟)
|
|
110
|
+
await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, buyAmountWei + parseOkb('0.0003'), `${ownerName ?? 'owner'}/buy-prefund-before-estimate`);
|
|
111
|
+
// 使用 Bundler 估算
|
|
112
|
+
const { userOp, prefundWei } = await this.aaManager.buildUserOpWithBundlerEstimate({
|
|
113
|
+
ownerWallet,
|
|
114
|
+
sender: accountInfo.sender,
|
|
115
|
+
callData,
|
|
116
|
+
nonce: accountInfo.nonce,
|
|
117
|
+
initCode,
|
|
118
|
+
});
|
|
119
|
+
// 补足 prefund + 买入金额
|
|
120
|
+
await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, buyAmountWei + prefundWei + parseOkb('0.0002'), `${ownerName ?? 'owner'}/buy-fund`);
|
|
121
|
+
// 签名
|
|
122
|
+
const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
|
|
123
|
+
return {
|
|
124
|
+
...signed,
|
|
125
|
+
prefundWei,
|
|
126
|
+
ownerName,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 构建授权 UserOp
|
|
131
|
+
*/
|
|
132
|
+
async buildApproveUserOp(ownerWallet, tokenAddress, spender, nonce, ownerName) {
|
|
133
|
+
const accountInfo = await this.aaManager.getAccountInfo(ownerWallet.address);
|
|
134
|
+
const approveData = encodeApproveCall(spender);
|
|
135
|
+
const callData = encodeExecute(tokenAddress, 0n, approveData);
|
|
136
|
+
await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/approve-prefund`);
|
|
137
|
+
const { userOp, prefundWei } = await this.aaManager.buildUserOpWithLocalEstimate({
|
|
138
|
+
ownerWallet,
|
|
139
|
+
sender: accountInfo.sender,
|
|
140
|
+
callData,
|
|
141
|
+
nonce,
|
|
142
|
+
});
|
|
143
|
+
await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/approve-fund`);
|
|
144
|
+
const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
|
|
145
|
+
return { ...signed, prefundWei, ownerName };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 构建卖出 UserOp
|
|
149
|
+
*/
|
|
150
|
+
async buildSellUserOp(ownerWallet, tokenAddress, sellAmount, nonce, needApprove, ownerName) {
|
|
151
|
+
const accountInfo = await this.aaManager.getAccountInfo(ownerWallet.address);
|
|
152
|
+
const sellData = encodeSellCall(tokenAddress, sellAmount, 0n);
|
|
153
|
+
const callData = encodeExecute(this.portalAddress, 0n, sellData);
|
|
154
|
+
await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, parseOkb('0.0003'), `${ownerName ?? 'owner'}/sell-prefund`);
|
|
155
|
+
// 如果需要 approve(还未执行),estimateGas 会 revert,使用固定值
|
|
156
|
+
const { userOp, prefundWei } = needApprove
|
|
157
|
+
? await this.aaManager.buildUserOpWithLocalEstimate({
|
|
158
|
+
ownerWallet,
|
|
159
|
+
sender: accountInfo.sender,
|
|
160
|
+
callData,
|
|
161
|
+
nonce,
|
|
162
|
+
callGasLimit: DEFAULT_CALL_GAS_LIMIT_SELL,
|
|
163
|
+
})
|
|
164
|
+
: await this.aaManager.buildUserOpWithLocalEstimate({
|
|
165
|
+
ownerWallet,
|
|
166
|
+
sender: accountInfo.sender,
|
|
167
|
+
callData,
|
|
168
|
+
nonce,
|
|
169
|
+
});
|
|
170
|
+
await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/sell-fund`);
|
|
171
|
+
const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
|
|
172
|
+
return { ...signed, prefundWei, ownerName };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 构建归集 UserOp(将 OKB 从 sender 转回 owner)
|
|
176
|
+
*/
|
|
177
|
+
async buildWithdrawUserOp(ownerWallet, reserveWei, ownerName) {
|
|
178
|
+
const accountInfo = await this.aaManager.getAccountInfo(ownerWallet.address);
|
|
179
|
+
const senderBalance = await this.portalQuery.getOkbBalance(accountInfo.sender);
|
|
180
|
+
if (senderBalance <= reserveWei) {
|
|
181
|
+
console.log(`\n[${ownerName ?? 'owner'}] sender OKB 太少,跳过归集:${formatOkb(senderBalance)} OKB`);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
// 先估算 prefund
|
|
185
|
+
const tempCallData = encodeExecute(ownerWallet.address, 0n, '0x');
|
|
186
|
+
await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/withdraw-prefund`);
|
|
187
|
+
const { prefundWei } = await this.aaManager.buildUserOpWithLocalEstimate({
|
|
188
|
+
ownerWallet,
|
|
189
|
+
sender: accountInfo.sender,
|
|
190
|
+
callData: tempCallData,
|
|
191
|
+
nonce: accountInfo.nonce,
|
|
192
|
+
});
|
|
193
|
+
// 计算可归集金额
|
|
194
|
+
const currentBalance = await this.portalQuery.getOkbBalance(accountInfo.sender);
|
|
195
|
+
const withdrawAmount = currentBalance > prefundWei + reserveWei
|
|
196
|
+
? currentBalance - prefundWei - reserveWei
|
|
197
|
+
: 0n;
|
|
198
|
+
if (withdrawAmount <= 0n) {
|
|
199
|
+
console.log(`\n[${ownerName ?? 'owner'}] 归集后可转出=0(余额不足以覆盖 prefund+reserve)`);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
// 真正的 callData
|
|
203
|
+
const callData = encodeExecute(ownerWallet.address, withdrawAmount, '0x');
|
|
204
|
+
const { userOp } = await this.aaManager.buildUserOpWithLocalEstimate({
|
|
205
|
+
ownerWallet,
|
|
206
|
+
sender: accountInfo.sender,
|
|
207
|
+
callData,
|
|
208
|
+
nonce: accountInfo.nonce,
|
|
209
|
+
});
|
|
210
|
+
console.log(`\n[${ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
|
|
211
|
+
const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
|
|
212
|
+
return { ...signed, prefundWei, ownerName };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* 构建代币转账 UserOp(将代币从 sender 转回 owner)
|
|
216
|
+
*/
|
|
217
|
+
async buildTransferTokenUserOp(ownerWallet, tokenAddress, ownerName) {
|
|
218
|
+
const accountInfo = await this.aaManager.getAccountInfo(ownerWallet.address);
|
|
219
|
+
const tokenBalance = await this.portalQuery.getTokenBalance(tokenAddress, accountInfo.sender);
|
|
220
|
+
if (tokenBalance === 0n) {
|
|
221
|
+
console.log(`\n[${ownerName ?? 'owner'}] sender 没有代币,跳过转账`);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const transferData = encodeTransferCall(ownerWallet.address, tokenBalance);
|
|
225
|
+
const callData = encodeExecute(tokenAddress, 0n, transferData);
|
|
226
|
+
await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, parseOkb('0.0002'), `${ownerName ?? 'owner'}/transfer-prefund`);
|
|
227
|
+
const { userOp, prefundWei } = await this.aaManager.buildUserOpWithBundlerEstimate({
|
|
228
|
+
ownerWallet,
|
|
229
|
+
sender: accountInfo.sender,
|
|
230
|
+
callData,
|
|
231
|
+
nonce: accountInfo.nonce,
|
|
232
|
+
});
|
|
233
|
+
await this.aaManager.ensureSenderBalance(ownerWallet, accountInfo.sender, prefundWei + parseOkb('0.00005'), `${ownerName ?? 'owner'}/transfer-fund`);
|
|
234
|
+
console.log(`\n[${ownerName ?? 'owner'}] transfer token: ${tokenBalance.toString()}`);
|
|
235
|
+
const signed = await this.aaManager.signUserOp(userOp, ownerWallet);
|
|
236
|
+
return { ...signed, prefundWei, ownerName };
|
|
237
|
+
}
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// 公开 API
|
|
240
|
+
// ============================================================================
|
|
241
|
+
/**
|
|
242
|
+
* 捆绑买入
|
|
243
|
+
*
|
|
244
|
+
* 多个地址同一笔 handleOps 买入代币
|
|
245
|
+
*/
|
|
246
|
+
async bundleBuy(params) {
|
|
247
|
+
const { tokenAddress, privateKeys, buyAmounts, transferBackToOwner, config } = params;
|
|
248
|
+
if (privateKeys.length !== buyAmounts.length) {
|
|
249
|
+
throw new Error('私钥数量和购买金额数量必须一致');
|
|
250
|
+
}
|
|
251
|
+
// 使用第一个 owner 作为 bundler signer
|
|
252
|
+
const wallets = privateKeys.map((pk) => createWallet(pk, this.config));
|
|
253
|
+
const bundlerSigner = wallets[0];
|
|
254
|
+
const beneficiary = bundlerSigner.address;
|
|
255
|
+
console.log('=== XLayer Bundle Buy ===');
|
|
256
|
+
console.log('token:', tokenAddress);
|
|
257
|
+
console.log('owners:', wallets.length);
|
|
258
|
+
// 1. 构建买入 UserOps
|
|
259
|
+
const buyOps = [];
|
|
260
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
261
|
+
const wallet = wallets[i];
|
|
262
|
+
const buyWei = parseOkb(buyAmounts[i]);
|
|
263
|
+
const signed = await this.buildBuyUserOp(wallet, tokenAddress, buyWei, `owner${i + 1}`);
|
|
264
|
+
buyOps.push(signed.userOp);
|
|
265
|
+
}
|
|
266
|
+
// 2. 执行买入
|
|
267
|
+
const buyResult = await this.runHandleOps('buyBundle', buyOps, bundlerSigner, beneficiary);
|
|
268
|
+
if (!buyResult) {
|
|
269
|
+
throw new Error('买入交易失败');
|
|
270
|
+
}
|
|
271
|
+
// 3. 获取代币余额
|
|
272
|
+
const senders = await Promise.all(wallets.map((w) => this.aaManager.predictSenderAddress(w.address)));
|
|
273
|
+
const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
|
|
274
|
+
// 4. 可选:转账代币回 owner
|
|
275
|
+
let transferResult;
|
|
276
|
+
if (transferBackToOwner) {
|
|
277
|
+
const transferOps = [];
|
|
278
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
279
|
+
const signed = await this.buildTransferTokenUserOp(wallets[i], tokenAddress, `owner${i + 1}`);
|
|
280
|
+
if (signed) {
|
|
281
|
+
transferOps.push(signed.userOp);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (transferOps.length > 0) {
|
|
285
|
+
transferResult = await this.runHandleOps('transferBundle', transferOps, bundlerSigner, beneficiary) ?? undefined;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return { buyResult, transferResult, tokenBalances };
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* 捆绑卖出
|
|
292
|
+
*
|
|
293
|
+
* 多个地址同一笔 handleOps 卖出代币
|
|
294
|
+
*/
|
|
295
|
+
async bundleSell(params) {
|
|
296
|
+
const { tokenAddress, privateKeys, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
|
|
297
|
+
const wallets = privateKeys.map((pk) => createWallet(pk, this.config));
|
|
298
|
+
const bundlerSigner = wallets[0];
|
|
299
|
+
const beneficiary = bundlerSigner.address;
|
|
300
|
+
const reserveWei = parseOkb(withdrawReserve);
|
|
301
|
+
console.log('=== XLayer Bundle Sell ===');
|
|
302
|
+
console.log('token:', tokenAddress);
|
|
303
|
+
console.log('owners:', wallets.length);
|
|
304
|
+
console.log('sellPercent:', sellPercent);
|
|
305
|
+
// 获取 sender 列表和余额
|
|
306
|
+
const senders = await Promise.all(wallets.map((w) => this.aaManager.predictSenderAddress(w.address)));
|
|
307
|
+
const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
|
|
308
|
+
const allowances = await this.portalQuery.getMultipleAllowances(tokenAddress, senders);
|
|
309
|
+
// 1. 检查授权,必要时先 approve
|
|
310
|
+
const approveOps = [];
|
|
311
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
312
|
+
const sender = senders[i];
|
|
313
|
+
const balance = tokenBalances.get(sender) ?? 0n;
|
|
314
|
+
const allowance = allowances.get(sender) ?? 0n;
|
|
315
|
+
if (balance === 0n)
|
|
316
|
+
continue;
|
|
317
|
+
if (allowance >= balance)
|
|
318
|
+
continue;
|
|
319
|
+
const accountInfo = await this.aaManager.getAccountInfo(wallets[i].address);
|
|
320
|
+
const signed = await this.buildApproveUserOp(wallets[i], tokenAddress, this.portalAddress, accountInfo.nonce, `owner${i + 1}`);
|
|
321
|
+
approveOps.push(signed.userOp);
|
|
322
|
+
}
|
|
323
|
+
let approveResult;
|
|
324
|
+
if (approveOps.length > 0) {
|
|
325
|
+
approveResult = await this.runHandleOps('approveBundle', approveOps, bundlerSigner, beneficiary) ?? undefined;
|
|
326
|
+
}
|
|
327
|
+
// 2. 卖出
|
|
328
|
+
const sellOps = [];
|
|
329
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
330
|
+
const sender = senders[i];
|
|
331
|
+
const balance = tokenBalances.get(sender) ?? 0n;
|
|
332
|
+
if (balance === 0n)
|
|
333
|
+
continue;
|
|
334
|
+
const sellAmount = (balance * BigInt(sellPercent)) / 100n;
|
|
335
|
+
if (sellAmount === 0n)
|
|
336
|
+
continue;
|
|
337
|
+
const accountInfo = await this.aaManager.getAccountInfo(wallets[i].address);
|
|
338
|
+
const allowance = allowances.get(sender) ?? 0n;
|
|
339
|
+
const needApprove = allowance < sellAmount && approveOps.length > 0;
|
|
340
|
+
const signed = await this.buildSellUserOp(wallets[i], tokenAddress, sellAmount, accountInfo.nonce, needApprove, `owner${i + 1}`);
|
|
341
|
+
sellOps.push(signed.userOp);
|
|
342
|
+
}
|
|
343
|
+
const sellResult = await this.runHandleOps('sellBundle', sellOps, bundlerSigner, beneficiary);
|
|
344
|
+
if (!sellResult) {
|
|
345
|
+
throw new Error('卖出交易失败');
|
|
346
|
+
}
|
|
347
|
+
// 3. 可选:归集 OKB
|
|
348
|
+
let withdrawResult;
|
|
349
|
+
if (withdrawToOwner) {
|
|
350
|
+
const withdrawOps = [];
|
|
351
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
352
|
+
const signed = await this.buildWithdrawUserOp(wallets[i], reserveWei, `owner${i + 1}`);
|
|
353
|
+
if (signed) {
|
|
354
|
+
withdrawOps.push(signed.userOp);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (withdrawOps.length > 0) {
|
|
358
|
+
withdrawResult = await this.runHandleOps('withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { approveResult, sellResult, withdrawResult };
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* 捆绑买卖(先买后卖)
|
|
365
|
+
*
|
|
366
|
+
* 完整流程:买入 -> 授权 -> 卖出 -> 归集
|
|
367
|
+
*/
|
|
368
|
+
async bundleBuySell(params) {
|
|
369
|
+
const { tokenAddress, privateKeys, buyAmounts, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
|
|
370
|
+
if (privateKeys.length !== buyAmounts.length) {
|
|
371
|
+
throw new Error('私钥数量和购买金额数量必须一致');
|
|
372
|
+
}
|
|
373
|
+
const wallets = privateKeys.map((pk) => createWallet(pk, this.config));
|
|
374
|
+
const bundlerSigner = wallets[0];
|
|
375
|
+
const beneficiary = bundlerSigner.address;
|
|
376
|
+
const reserveWei = parseOkb(withdrawReserve);
|
|
377
|
+
console.log('=== XLayer Bundle Buy -> Sell ===');
|
|
378
|
+
console.log('token:', tokenAddress);
|
|
379
|
+
console.log('owners:', wallets.length);
|
|
380
|
+
console.log('sellPercent:', sellPercent);
|
|
381
|
+
// 获取 sender 列表
|
|
382
|
+
const senders = await Promise.all(wallets.map((w) => this.aaManager.predictSenderAddress(w.address)));
|
|
383
|
+
// 1. 买入
|
|
384
|
+
const buyOps = [];
|
|
385
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
386
|
+
const buyWei = parseOkb(buyAmounts[i]);
|
|
387
|
+
const signed = await this.buildBuyUserOp(wallets[i], tokenAddress, buyWei, `owner${i + 1}`);
|
|
388
|
+
buyOps.push(signed.userOp);
|
|
389
|
+
}
|
|
390
|
+
const buyResult = await this.runHandleOps('buyBundle', buyOps, bundlerSigner, beneficiary);
|
|
391
|
+
if (!buyResult) {
|
|
392
|
+
throw new Error('买入交易失败');
|
|
393
|
+
}
|
|
394
|
+
// 获取买入后的代币余额
|
|
395
|
+
const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
|
|
396
|
+
const allowances = await this.portalQuery.getMultipleAllowances(tokenAddress, senders);
|
|
397
|
+
// 2. 授权 + 卖出(可以合并到同一笔 handleOps)
|
|
398
|
+
const sellOps = [];
|
|
399
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
400
|
+
const sender = senders[i];
|
|
401
|
+
const balance = tokenBalances.get(sender) ?? 0n;
|
|
402
|
+
if (balance === 0n) {
|
|
403
|
+
console.log(`[owner${i + 1}] 没买到代币,跳过卖出`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const allowance = allowances.get(sender) ?? 0n;
|
|
407
|
+
let baseNonce = (await this.aaManager.getAccountInfo(wallets[i].address)).nonce;
|
|
408
|
+
// 如果需要授权,先添加 approve op
|
|
409
|
+
const needApprove = allowance < balance;
|
|
410
|
+
if (needApprove) {
|
|
411
|
+
const approveOp = await this.buildApproveUserOp(wallets[i], tokenAddress, this.portalAddress, baseNonce, `owner${i + 1}`);
|
|
412
|
+
sellOps.push(approveOp.userOp);
|
|
413
|
+
baseNonce = baseNonce + 1n;
|
|
414
|
+
}
|
|
415
|
+
// 添加 sell op
|
|
416
|
+
const sellAmount = (balance * BigInt(sellPercent)) / 100n;
|
|
417
|
+
const sellOp = await this.buildSellUserOp(wallets[i], tokenAddress, sellAmount, baseNonce, needApprove, `owner${i + 1}`);
|
|
418
|
+
sellOps.push(sellOp.userOp);
|
|
419
|
+
}
|
|
420
|
+
const sellResult = await this.runHandleOps('sellBundle', sellOps, bundlerSigner, beneficiary);
|
|
421
|
+
if (!sellResult) {
|
|
422
|
+
throw new Error('卖出交易失败');
|
|
423
|
+
}
|
|
424
|
+
// 3. 可选:归集 OKB
|
|
425
|
+
let withdrawResult;
|
|
426
|
+
if (withdrawToOwner) {
|
|
427
|
+
const withdrawOps = [];
|
|
428
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
429
|
+
const signed = await this.buildWithdrawUserOp(wallets[i], reserveWei, `owner${i + 1}`);
|
|
430
|
+
if (signed) {
|
|
431
|
+
withdrawOps.push(signed.userOp);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (withdrawOps.length > 0) {
|
|
435
|
+
withdrawResult = await this.runHandleOps('withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// 最终余额
|
|
439
|
+
const finalBalances = await this.portalQuery.getMultipleOkbBalances(senders);
|
|
440
|
+
return { buyResult, sellResult, withdrawResult, finalBalances };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// 便捷函数
|
|
445
|
+
// ============================================================================
|
|
446
|
+
/**
|
|
447
|
+
* 创建捆绑交易执行器
|
|
448
|
+
*/
|
|
449
|
+
export function createBundleExecutor(config) {
|
|
450
|
+
return new BundleExecutor(config);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* 快速捆绑买入
|
|
454
|
+
*/
|
|
455
|
+
export async function bundleBuy(params) {
|
|
456
|
+
const executor = createBundleExecutor(params.config);
|
|
457
|
+
return executor.bundleBuy(params);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* 快速捆绑卖出
|
|
461
|
+
*/
|
|
462
|
+
export async function bundleSell(params) {
|
|
463
|
+
const executor = createBundleExecutor(params.config);
|
|
464
|
+
return executor.bundleSell(params);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* 快速捆绑买卖
|
|
468
|
+
*/
|
|
469
|
+
export async function bundleBuySell(params) {
|
|
470
|
+
const executor = createBundleExecutor(params.config);
|
|
471
|
+
return executor.bundleBuySell(params);
|
|
472
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer Bundler 客户端
|
|
3
|
+
*
|
|
4
|
+
* 与 Particle Bundler 交互,提供 ERC-4337 相关 RPC 方法
|
|
5
|
+
*/
|
|
6
|
+
import type { UserOperation, GasEstimate } from './types.js';
|
|
7
|
+
export interface BundlerConfig {
|
|
8
|
+
/** Bundler URL */
|
|
9
|
+
url?: string;
|
|
10
|
+
/** Chain ID */
|
|
11
|
+
chainId?: number;
|
|
12
|
+
/** EntryPoint 地址 */
|
|
13
|
+
entryPoint?: string;
|
|
14
|
+
/** 请求超时时间(毫秒) */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
/** 额外请求头 */
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
export interface BundlerReceipt {
|
|
20
|
+
userOpHash: string;
|
|
21
|
+
sender: string;
|
|
22
|
+
nonce: string;
|
|
23
|
+
actualGasCost: string;
|
|
24
|
+
actualGasUsed: string;
|
|
25
|
+
success: boolean;
|
|
26
|
+
logs: any[];
|
|
27
|
+
receipt: {
|
|
28
|
+
transactionHash: string;
|
|
29
|
+
transactionIndex: number;
|
|
30
|
+
blockHash: string;
|
|
31
|
+
blockNumber: number;
|
|
32
|
+
from: string;
|
|
33
|
+
to: string;
|
|
34
|
+
gasUsed: string;
|
|
35
|
+
status: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Particle Bundler 客户端
|
|
40
|
+
*
|
|
41
|
+
* 提供与 ERC-4337 Bundler 交互的方法:
|
|
42
|
+
* - eth_supportedEntryPoints
|
|
43
|
+
* - eth_estimateUserOperationGas
|
|
44
|
+
* - eth_sendUserOperation
|
|
45
|
+
* - eth_getUserOperationReceipt
|
|
46
|
+
* - eth_getUserOperationByHash
|
|
47
|
+
*/
|
|
48
|
+
export declare class BundlerClient {
|
|
49
|
+
private url;
|
|
50
|
+
private chainId;
|
|
51
|
+
private entryPoint;
|
|
52
|
+
private timeoutMs;
|
|
53
|
+
private headers;
|
|
54
|
+
constructor(config?: BundlerConfig);
|
|
55
|
+
/**
|
|
56
|
+
* 发送 JSON-RPC 请求到 Bundler
|
|
57
|
+
*/
|
|
58
|
+
private rpc;
|
|
59
|
+
/**
|
|
60
|
+
* 获取支持的 EntryPoint 列表
|
|
61
|
+
*/
|
|
62
|
+
getSupportedEntryPoints(): Promise<string[]>;
|
|
63
|
+
/**
|
|
64
|
+
* 估算 UserOperation Gas
|
|
65
|
+
*
|
|
66
|
+
* @param userOp 未签名的 UserOperation(signature 可为空)
|
|
67
|
+
* @returns Gas 估算结果
|
|
68
|
+
*/
|
|
69
|
+
estimateUserOperationGas(userOp: UserOperation): Promise<GasEstimate>;
|
|
70
|
+
/**
|
|
71
|
+
* 发送 UserOperation
|
|
72
|
+
*
|
|
73
|
+
* @param userOp 已签名的 UserOperation
|
|
74
|
+
* @returns userOpHash
|
|
75
|
+
*/
|
|
76
|
+
sendUserOperation(userOp: UserOperation): Promise<string>;
|
|
77
|
+
/**
|
|
78
|
+
* 获取 UserOperation 回执
|
|
79
|
+
*
|
|
80
|
+
* @param userOpHash UserOperation 哈希
|
|
81
|
+
* @returns 回执(如果还未被打包则返回 null)
|
|
82
|
+
*/
|
|
83
|
+
getUserOperationReceipt(userOpHash: string): Promise<BundlerReceipt | null>;
|
|
84
|
+
/**
|
|
85
|
+
* 根据 hash 获取 UserOperation
|
|
86
|
+
*
|
|
87
|
+
* @param userOpHash UserOperation 哈希
|
|
88
|
+
*/
|
|
89
|
+
getUserOperationByHash(userOpHash: string): Promise<any>;
|
|
90
|
+
/**
|
|
91
|
+
* 等待 UserOperation 被打包
|
|
92
|
+
*
|
|
93
|
+
* @param userOpHash UserOperation 哈希
|
|
94
|
+
* @param maxTries 最大轮询次数(默认 80)
|
|
95
|
+
* @param intervalMs 轮询间隔(默认 1500ms)
|
|
96
|
+
* @returns 回执
|
|
97
|
+
*/
|
|
98
|
+
waitForUserOperationReceipt(userOpHash: string, maxTries?: number, intervalMs?: number): Promise<BundlerReceipt | null>;
|
|
99
|
+
/**
|
|
100
|
+
* 获取当前使用的 EntryPoint 地址
|
|
101
|
+
*/
|
|
102
|
+
getEntryPoint(): string;
|
|
103
|
+
/**
|
|
104
|
+
* 获取当前链 ID
|
|
105
|
+
*/
|
|
106
|
+
getChainId(): number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 创建默认的 Bundler 客户端
|
|
110
|
+
*/
|
|
111
|
+
export declare function createBundlerClient(config?: BundlerConfig): BundlerClient;
|