four-flap-meme-sdk 1.5.32 → 1.5.34

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.
@@ -3,15 +3,37 @@
3
3
  */
4
4
  import { Wallet, ethers } from 'ethers';
5
5
  import { AANonceMap, } from './types.js';
6
- import { POTATOSWAP_V2_ROUTER, WOKB, } from './constants.js';
6
+ import { POTATOSWAP_V2_ROUTER, WOKB, MULTICALL3, } from './constants.js';
7
7
  import { AAAccountManager, encodeExecute } from './aa-account.js';
8
8
  import { encodeApproveCall, } from './portal-ops.js';
9
9
  import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, } from './dex.js';
10
10
  import { BundleExecutor } from './bundle.js';
11
11
  import { PROFIT_CONFIG } from '../utils/constants.js';
12
+ const multicallIface = new ethers.Interface([
13
+ 'function aggregate3Value((address target,bool allowFailure,uint256 value,bytes callData)[] calls) payable returns ((bool success,bytes returnData)[] returnData)',
14
+ ]);
15
+ function chunkArray(arr, size) {
16
+ const out = [];
17
+ const n = Math.max(1, Math.floor(size));
18
+ for (let i = 0; i < arr.length; i += n)
19
+ out.push(arr.slice(i, i + n));
20
+ return out;
21
+ }
22
+ function encodeNativeDisperseViaMulticall3(params) {
23
+ const calls = params.to.map((target, idx) => ({
24
+ target,
25
+ allowFailure: false,
26
+ value: params.values[idx] ?? 0n,
27
+ callData: '0x',
28
+ }));
29
+ const totalValue = params.values.reduce((a, b) => a + (b ?? 0n), 0n);
30
+ const data = multicallIface.encodeFunctionData('aggregate3Value', [calls]);
31
+ return { totalValue, data };
32
+ }
12
33
  function resolveProfitSettings(config) {
13
34
  const extractProfit = config?.extractProfit !== false; // 默认 true(对齐 BSC)
14
- const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS;
35
+ // 对齐 BSC “捆绑换手模式”默认费率
36
+ const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
15
37
  const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
16
38
  const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
17
39
  return { extractProfit, profitBps, profitRecipient };
@@ -54,7 +76,7 @@ export class AADexSwapExecutor {
54
76
  * AA 外盘单钱包换手签名
55
77
  */
56
78
  async bundleSwapSign(params) {
57
- const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
79
+ const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
58
80
  const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
59
81
  const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
60
82
  const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn });
@@ -101,16 +123,7 @@ export class AADexSwapExecutor {
101
123
  const allowance = await this.aaManager.getErc20Allowance(tokenAddress, sellerSender, effectiveRouter);
102
124
  needApprove = allowance < sellAmountWei;
103
125
  }
104
- // 报价
105
- let finalBuyAmountWei;
106
- if (buyAmountOkb) {
107
- finalBuyAmountWei = ethers.parseEther(String(buyAmountOkb));
108
- }
109
- else {
110
- const quoted = await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
111
- finalBuyAmountWei = (quoted * BigInt(10000 - slippageBps)) / 10000n;
112
- }
113
- // 用 quoteTokenToOkb 估算卖出输出(用于利润提取)
126
+ // 估算卖出输出(用于利润提取与 buy 预算)
114
127
  const quotedSellOutWei = await (async () => {
115
128
  try {
116
129
  return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
@@ -119,7 +132,18 @@ export class AADexSwapExecutor {
119
132
  return 0n;
120
133
  }
121
134
  })();
122
- const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
135
+ // buyAmount:若未传则按 quotedSellOut 计算;利润从 quotedSellOut 中刮取,但要保证买入资金充足
136
+ const requestedBuyWei = buyAmountOkb
137
+ ? ethers.parseEther(String(buyAmountOkb))
138
+ : (quotedSellOutWei * BigInt(10000 - slippageBps)) / 10000n;
139
+ if (quotedSellOutWei > 0n && requestedBuyWei > quotedSellOutWei) {
140
+ throw new Error('AA 捆绑换手:buyAmountOkb 超过预估卖出输出(quotedSellOut)');
141
+ }
142
+ const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
143
+ const profitCap = quotedSellOutWei > requestedBuyWei ? (quotedSellOutWei - requestedBuyWei) : 0n;
144
+ const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
145
+ const finalBuyAmountWei = requestedBuyWei;
146
+ const hopCount = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
123
147
  const outOps = [];
124
148
  if (needApprove) {
125
149
  const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
@@ -158,6 +182,19 @@ export class AADexSwapExecutor {
158
182
  });
159
183
  outOps.push(signedProfit.userOp);
160
184
  }
185
+ // ✅ 卖出所得 OKB 转给买方 AA(Sender)
186
+ if (sellerSender.toLowerCase() !== buyerSender.toLowerCase() && finalBuyAmountWei > 0n) {
187
+ const transferCallData = encodeExecute(buyerSender, finalBuyAmountWei, '0x');
188
+ const signedTransfer = await this.aaManager.buildUserOpWithState({
189
+ ownerWallet: sellerOwner,
190
+ sender: sellerSender,
191
+ nonce: nonceMap.next(sellerSender),
192
+ initCode: consumeInitCode(sellerSender),
193
+ callData: transferCallData,
194
+ signOnly: true,
195
+ });
196
+ outOps.push(signedTransfer.userOp);
197
+ }
161
198
  // Buy op
162
199
  const buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], buyerSender, getDexDeadline());
163
200
  const buyCallData = encodeExecute(effectiveRouter, finalBuyAmountWei, buySwapData);
@@ -208,6 +245,7 @@ export class AADexSwapExecutor {
208
245
  profitRecipient,
209
246
  profitWei: profitWei.toString(),
210
247
  quotedSellOutWei: quotedSellOutWei.toString(),
248
+ disperseHopCount: String(hopCount),
211
249
  },
212
250
  };
213
251
  }
@@ -215,7 +253,7 @@ export class AADexSwapExecutor {
215
253
  * AA 外盘批量换手签名
216
254
  */
217
255
  async bundleBatchSwapSign(params) {
218
- const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
256
+ const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
219
257
  const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
220
258
  const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
221
259
  const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn });
@@ -286,7 +324,10 @@ export class AADexSwapExecutor {
286
324
  signOnly: true,
287
325
  });
288
326
  outOps.push(signedSell.userOp);
289
- // Profit op:估算卖出输出,按比例刮取
327
+ // 先计算 buyAmountsWei(用于后续分发与校验)
328
+ const buyAmountsWei = buyAmountsOkb.map(a => ethers.parseEther(a));
329
+ const totalBuyWei = buyAmountsWei.reduce((a, b) => a + (b ?? 0n), 0n);
330
+ // Profit op:估算卖出输出,按比例刮取(但必须保证分发/买入资金充足)
290
331
  const quotedSellOutWei = await (async () => {
291
332
  try {
292
333
  return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
@@ -295,7 +336,12 @@ export class AADexSwapExecutor {
295
336
  return 0n;
296
337
  }
297
338
  })();
298
- const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
339
+ if (quotedSellOutWei > 0n && totalBuyWei > quotedSellOutWei) {
340
+ throw new Error('AA 批量换手:buyAmountsOkb 总和超过预估卖出输出(quotedSellOut)');
341
+ }
342
+ const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
343
+ const profitCap = quotedSellOutWei > totalBuyWei ? (quotedSellOutWei - totalBuyWei) : 0n;
344
+ const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
299
345
  if (extractProfit && profitWei > 0n) {
300
346
  const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
301
347
  const signedProfit = await this.aaManager.buildUserOpWithState({
@@ -308,8 +354,92 @@ export class AADexSwapExecutor {
308
354
  });
309
355
  outOps.push(signedProfit.userOp);
310
356
  }
311
- // Batch Buy ops
312
- const buyAmountsWei = buyAmountsOkb.map(a => ethers.parseEther(a));
357
+ // 卖出所得 OKB 分发给多个买方 AA(Sender)(支持多跳)
358
+ const buyerSenders = buyerAis.map(ai => ai.sender);
359
+ const hopCountRaw = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
360
+ const hopCount = Math.min(hopCountRaw, buyerSenders.length);
361
+ const maxPerOp = Math.max(1, Math.floor(Number(effectiveConfig.maxTransfersPerUserOpNative ?? 30)));
362
+ if (buyerSenders.length > 0 && totalBuyWei > 0n) {
363
+ if (hopCount <= 0) {
364
+ const items = buyerSenders.map((to, i) => ({ to, value: buyAmountsWei[i] ?? 0n })).filter(x => x.value > 0n);
365
+ const chunks = chunkArray(items, maxPerOp);
366
+ for (const ch of chunks) {
367
+ const { totalValue, data } = encodeNativeDisperseViaMulticall3({
368
+ to: ch.map(x => x.to),
369
+ values: ch.map(x => x.value),
370
+ });
371
+ const callData = encodeExecute(MULTICALL3, totalValue, data);
372
+ const signedDisperse = await this.aaManager.buildUserOpWithState({
373
+ ownerWallet: sellerOwner,
374
+ sender: sellerAi.sender,
375
+ nonce: nonceMap.next(sellerAi.sender),
376
+ initCode: consumeInitCode(sellerAi.sender),
377
+ callData,
378
+ signOnly: true,
379
+ });
380
+ outOps.push(signedDisperse.userOp);
381
+ }
382
+ }
383
+ else {
384
+ const hopSenders = buyerSenders.slice(0, hopCount);
385
+ const hopOwners = buyerOwners.slice(0, hopCount);
386
+ const hop0 = hopSenders[0];
387
+ const callData0 = encodeExecute(hop0, totalBuyWei, '0x');
388
+ const signedToHop0 = await this.aaManager.buildUserOpWithState({
389
+ ownerWallet: sellerOwner,
390
+ sender: sellerAi.sender,
391
+ nonce: nonceMap.next(sellerAi.sender),
392
+ initCode: consumeInitCode(sellerAi.sender),
393
+ callData: callData0,
394
+ signOnly: true,
395
+ });
396
+ outOps.push(signedToHop0.userOp);
397
+ let prefixKept = 0n;
398
+ for (let j = 0; j < hopSenders.length - 1; j++) {
399
+ const sender = hopSenders[j];
400
+ const next = hopSenders[j + 1];
401
+ const keep = buyAmountsWei[j] ?? 0n;
402
+ prefixKept += keep;
403
+ const remaining = totalBuyWei - prefixKept;
404
+ if (remaining <= 0n)
405
+ break;
406
+ const callData = encodeExecute(next, remaining, '0x');
407
+ const signedHop = await this.aaManager.buildUserOpWithState({
408
+ ownerWallet: hopOwners[j],
409
+ sender,
410
+ nonce: nonceMap.next(sender),
411
+ initCode: consumeInitCode(sender),
412
+ callData,
413
+ signOnly: true,
414
+ });
415
+ outOps.push(signedHop.userOp);
416
+ }
417
+ const lastHopIdx = hopSenders.length - 1;
418
+ const lastHopSender = hopSenders[lastHopIdx];
419
+ const lastHopOwner = hopOwners[lastHopIdx];
420
+ const rest = buyerSenders.slice(hopSenders.length);
421
+ const restAmounts = buyAmountsWei.slice(hopSenders.length);
422
+ const restItems = rest.map((to, i) => ({ to, value: restAmounts[i] ?? 0n })).filter(x => x.value > 0n);
423
+ const restChunks = chunkArray(restItems, maxPerOp);
424
+ for (const ch of restChunks) {
425
+ const { totalValue, data } = encodeNativeDisperseViaMulticall3({
426
+ to: ch.map(x => x.to),
427
+ values: ch.map(x => x.value),
428
+ });
429
+ const callData = encodeExecute(MULTICALL3, totalValue, data);
430
+ const signedDisperse = await this.aaManager.buildUserOpWithState({
431
+ ownerWallet: lastHopOwner,
432
+ sender: lastHopSender,
433
+ nonce: nonceMap.next(lastHopSender),
434
+ initCode: consumeInitCode(lastHopSender),
435
+ callData,
436
+ signOnly: true,
437
+ });
438
+ outOps.push(signedDisperse.userOp);
439
+ }
440
+ }
441
+ }
442
+ // Batch Buy ops(分发后再执行)
313
443
  for (let i = 0; i < buyerOwners.length; i++) {
314
444
  const ai = buyerAis[i];
315
445
  const buyWei = buyAmountsWei[i];
@@ -363,6 +493,7 @@ export class AADexSwapExecutor {
363
493
  profitRecipient,
364
494
  profitWei: profitWei.toString(),
365
495
  quotedSellOutWei: quotedSellOutWei.toString(),
496
+ disperseHopCount: String(hopCount),
366
497
  },
367
498
  };
368
499
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * XLayer 外盘(PotatoSwap/DEX)捆绑交易 SDK(AA / ERC-4337)
3
+ *
4
+ * 目标:对齐内盘 `bundle.ts` 的能力,但 Router/报价/授权逻辑走 DEX。
5
+ * - 捆绑买卖(买入 ->(可选授权)-> 卖出 ->(可选归集))
6
+ * - 自动归集 OKB 到 owner
7
+ * - ✅ 支持“刮取利润”:在归集时从可转出金额中按 bps 拆分转到 PROFIT_CONFIG.RECIPIENT
8
+ */
9
+ import type { XLayerConfig, BundleBuySellResult, BundleBuySellParams } from './types.js';
10
+ export interface DexBundleConfig extends XLayerConfig {
11
+ dexKey?: string;
12
+ routerAddress?: string;
13
+ deadlineMinutes?: number;
14
+ }
15
+ export interface DexBundleBuySellParams extends BundleBuySellParams {
16
+ dexKey?: string;
17
+ routerAddress?: string;
18
+ deadlineMinutes?: number;
19
+ }
20
+ /**
21
+ * 外盘捆绑交易执行器
22
+ */
23
+ export declare class DexBundleExecutor {
24
+ private aaManager;
25
+ private portalQuery;
26
+ private dexQuery;
27
+ private config;
28
+ constructor(config?: DexBundleConfig);
29
+ private getEffectiveRouter;
30
+ private getDeadlineMinutes;
31
+ /**
32
+ * 执行 handleOps 并解析结果
33
+ */
34
+ private runHandleOps;
35
+ private buildWithdrawUserOpWithState;
36
+ /**
37
+ * 外盘捆绑买卖(先买后卖)
38
+ */
39
+ bundleBuySell(params: DexBundleBuySellParams): Promise<BundleBuySellResult>;
40
+ }
41
+ export declare function createDexBundleExecutor(config?: DexBundleConfig): DexBundleExecutor;
42
+ export declare function dexBundleBuySell(params: DexBundleBuySellParams): Promise<BundleBuySellResult>;
@@ -0,0 +1,378 @@
1
+ /**
2
+ * XLayer 外盘(PotatoSwap/DEX)捆绑交易 SDK(AA / ERC-4337)
3
+ *
4
+ * 目标:对齐内盘 `bundle.ts` 的能力,但 Router/报价/授权逻辑走 DEX。
5
+ * - 捆绑买卖(买入 ->(可选授权)-> 卖出 ->(可选归集))
6
+ * - 自动归集 OKB 到 owner
7
+ * - ✅ 支持“刮取利润”:在归集时从可转出金额中按 bps 拆分转到 PROFIT_CONFIG.RECIPIENT
8
+ */
9
+ import { Wallet, Contract, Interface, ethers } from 'ethers';
10
+ import { ENTRYPOINT_ABI, DEFAULT_CALL_GAS_LIMIT_SELL, DEFAULT_WITHDRAW_RESERVE, POTATOSWAP_V2_ROUTER, WOKB, } from './constants.js';
11
+ import { AAAccountManager, encodeExecute, encodeExecuteBatch } from './aa-account.js';
12
+ import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee } from './dex.js';
13
+ import { PortalQuery, encodeApproveCall, parseOkb, formatOkb } from './portal-ops.js';
14
+ import { mapWithConcurrency } from '../utils/concurrency.js';
15
+ import { PROFIT_CONFIG } from '../utils/constants.js';
16
+ import { AANonceMap } from './types.js';
17
+ function getDexDeadline(minutes = 20) {
18
+ return Math.floor(Date.now() / 1000) + minutes * 60;
19
+ }
20
+ function resolveProfitSettings(config) {
21
+ const extractProfit = config?.extractProfit !== false; // 默认 true
22
+ const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
23
+ const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
24
+ const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
25
+ return { extractProfit, profitBps, profitRecipient };
26
+ }
27
+ function calculateProfitWei(amountWei, profitBps) {
28
+ if (amountWei <= 0n)
29
+ return 0n;
30
+ if (!Number.isFinite(profitBps) || profitBps <= 0)
31
+ return 0n;
32
+ return (amountWei * BigInt(profitBps)) / 10000n;
33
+ }
34
+ // 固定 gas(用于大规模减少 RPC);具体值允许通过 config.fixedGas 覆盖
35
+ const DEFAULT_CALL_GAS_LIMIT_BUY = DEFAULT_CALL_GAS_LIMIT_SELL;
36
+ const DEFAULT_CALL_GAS_LIMIT_APPROVE = 200000n;
37
+ const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
38
+ /**
39
+ * 外盘捆绑交易执行器
40
+ */
41
+ export class DexBundleExecutor {
42
+ constructor(config = {}) {
43
+ this.config = config;
44
+ this.aaManager = new AAAccountManager(config);
45
+ this.portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
46
+ this.dexQuery = new DexQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId, routerAddress: config.routerAddress });
47
+ }
48
+ getEffectiveRouter(params) {
49
+ if (params.routerAddress)
50
+ return params.routerAddress;
51
+ if (params.dexKey === 'DYORSWAP') {
52
+ return '0xfb001fbbace32f09cb6d3c449b935183de53ee96';
53
+ }
54
+ return POTATOSWAP_V2_ROUTER;
55
+ }
56
+ getDeadlineMinutes(params) {
57
+ return params.deadlineMinutes ?? this.config.deadlineMinutes ?? 20;
58
+ }
59
+ /**
60
+ * 执行 handleOps 并解析结果
61
+ */
62
+ async runHandleOps(label, ops, bundlerSigner, beneficiary) {
63
+ if (ops.length === 0) {
64
+ console.log(`\n[${label}] 没有 ops,跳过`);
65
+ return null;
66
+ }
67
+ const provider = this.aaManager.getProvider();
68
+ const entryPointAddress = this.aaManager.getEntryPointAddress();
69
+ const feeData = await provider.getFeeData();
70
+ console.log(`\n[${label}] 发送 handleOps,ops=${ops.length} ...`);
71
+ const entryPointWithSigner = new Contract(entryPointAddress, ENTRYPOINT_ABI, bundlerSigner);
72
+ const tx = await entryPointWithSigner.handleOps(ops, beneficiary, { gasPrice: feeData.gasPrice ?? 100000000n });
73
+ console.log(`[${label}] txHash:`, tx.hash);
74
+ const receipt = await tx.wait();
75
+ console.log(`[${label}] mined block=${receipt.blockNumber} status=${receipt.status}`);
76
+ const epIface = new Interface(ENTRYPOINT_ABI);
77
+ const userOpEvents = [];
78
+ for (const log of receipt.logs) {
79
+ try {
80
+ const parsed = epIface.parseLog(log);
81
+ if (parsed?.name === 'UserOperationEvent') {
82
+ const e = parsed.args;
83
+ userOpEvents.push({
84
+ userOpHash: e.userOpHash,
85
+ sender: e.sender,
86
+ paymaster: e.paymaster,
87
+ success: e.success,
88
+ actualGasCost: e.actualGasCost,
89
+ actualGasUsed: e.actualGasUsed,
90
+ });
91
+ }
92
+ }
93
+ catch { }
94
+ }
95
+ return {
96
+ txHash: tx.hash,
97
+ blockNumber: receipt.blockNumber,
98
+ status: receipt.status,
99
+ userOpEvents,
100
+ };
101
+ }
102
+ async buildWithdrawUserOpWithState(params) {
103
+ const senderBalance = params.senderBalance;
104
+ if (senderBalance <= params.reserveWei) {
105
+ console.log(`\n[${params.ownerName ?? 'owner'}] sender OKB 太少,跳过归集:${formatOkb(senderBalance)} OKB`);
106
+ return null;
107
+ }
108
+ // 先估算 prefund(使用空调用)
109
+ const tempCallData = encodeExecute(params.ownerWallet.address, 0n, '0x');
110
+ if (!params.signOnly) {
111
+ await this.aaManager.ensureSenderBalance(params.ownerWallet, params.sender, parseOkb('0.0002'), `${params.ownerName ?? 'owner'}/withdraw-prefund`);
112
+ }
113
+ const effConfig = { ...(this.config ?? {}), ...(params.configOverride ?? {}) };
114
+ const gasPolicyRaw = effConfig.gasPolicy ?? 'bundlerEstimate';
115
+ const gasPolicy = params.signOnly && gasPolicyRaw === 'bundlerEstimate' ? 'fixed' : gasPolicyRaw;
116
+ const { prefundWei } = gasPolicy === 'fixed'
117
+ ? await this.aaManager.buildUserOpWithFixedGas({
118
+ ownerWallet: params.ownerWallet,
119
+ sender: params.sender,
120
+ callData: tempCallData,
121
+ nonce: params.nonce,
122
+ initCode: params.initCode,
123
+ deployed: params.initCode === '0x',
124
+ fixedGas: {
125
+ ...(effConfig.fixedGas ?? {}),
126
+ callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
127
+ },
128
+ })
129
+ : gasPolicy === 'localEstimate'
130
+ ? await this.aaManager.buildUserOpWithLocalEstimate({
131
+ ownerWallet: params.ownerWallet,
132
+ sender: params.sender,
133
+ callData: tempCallData,
134
+ nonce: params.nonce,
135
+ initCode: params.initCode,
136
+ })
137
+ : await this.aaManager.buildUserOpWithBundlerEstimate({
138
+ ownerWallet: params.ownerWallet,
139
+ sender: params.sender,
140
+ callData: tempCallData,
141
+ nonce: params.nonce,
142
+ initCode: params.initCode,
143
+ });
144
+ const withdrawAmount = senderBalance > prefundWei + params.reserveWei
145
+ ? senderBalance - prefundWei - params.reserveWei
146
+ : 0n;
147
+ if (withdrawAmount <= 0n) {
148
+ console.log(`\n[${params.ownerName ?? 'owner'}] 归集后可转出=0(余额不足以覆盖 prefund+reserve)`);
149
+ return null;
150
+ }
151
+ const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effConfig);
152
+ const profitWei = extractProfit ? calculateProfitWei(withdrawAmount, profitBps) : 0n;
153
+ const toOwnerWei = withdrawAmount - profitWei;
154
+ const callData = extractProfit && profitWei > 0n
155
+ ? encodeExecuteBatch([profitRecipient, params.ownerWallet.address], [profitWei, toOwnerWei], ['0x', '0x'])
156
+ : encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
157
+ const { userOp } = gasPolicy === 'fixed'
158
+ ? await this.aaManager.buildUserOpWithFixedGas({
159
+ ownerWallet: params.ownerWallet,
160
+ sender: params.sender,
161
+ callData,
162
+ nonce: params.nonce,
163
+ initCode: params.initCode,
164
+ deployed: params.initCode === '0x',
165
+ fixedGas: {
166
+ ...(effConfig.fixedGas ?? {}),
167
+ callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
168
+ },
169
+ })
170
+ : gasPolicy === 'localEstimate'
171
+ ? await this.aaManager.buildUserOpWithLocalEstimate({
172
+ ownerWallet: params.ownerWallet,
173
+ sender: params.sender,
174
+ callData,
175
+ nonce: params.nonce,
176
+ initCode: params.initCode,
177
+ })
178
+ : await this.aaManager.buildUserOpWithBundlerEstimate({
179
+ ownerWallet: params.ownerWallet,
180
+ sender: params.sender,
181
+ callData,
182
+ nonce: params.nonce,
183
+ initCode: params.initCode,
184
+ });
185
+ if (extractProfit && profitWei > 0n) {
186
+ console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(toOwnerWei)} OKB (profit: ${formatOkb(profitWei)} OKB -> ${profitRecipient})`);
187
+ }
188
+ else {
189
+ console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
190
+ }
191
+ return { userOp, prefundWei };
192
+ }
193
+ /**
194
+ * 外盘捆绑买卖(先买后卖)
195
+ */
196
+ async bundleBuySell(params) {
197
+ const { tokenAddress, privateKeys, buyAmounts, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, dexKey, routerAddress, deadlineMinutes, } = params;
198
+ if (privateKeys.length !== buyAmounts.length) {
199
+ throw new Error('私钥数量和购买金额数量必须一致');
200
+ }
201
+ const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress });
202
+ const deadline = getDexDeadline(this.getDeadlineMinutes({ deadlineMinutes }));
203
+ const sharedProvider = this.aaManager.getProvider();
204
+ const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
205
+ const bundlerSigner = wallets[0];
206
+ const beneficiary = bundlerSigner.address;
207
+ const reserveWei = parseOkb(withdrawReserve);
208
+ console.log('=== XLayer DEX Bundle Buy -> Sell ===');
209
+ console.log('router:', effectiveRouter);
210
+ console.log('token:', tokenAddress);
211
+ console.log('owners:', wallets.length);
212
+ console.log('sellPercent:', sellPercent);
213
+ const effCfg = { ...(this.config ?? {}), ...(config ?? {}) };
214
+ const profitSettings = resolveProfitSettings(effCfg);
215
+ // 1) accountInfos / nonceMap
216
+ const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
217
+ const senders = accountInfos.map((ai) => ai.sender);
218
+ const nonceMap = new AANonceMap();
219
+ for (const ai of accountInfos)
220
+ nonceMap.init(ai.sender, ai.nonce);
221
+ // 2) 买入(批量估算 + 补余额 + 并发签名)
222
+ const buyWeis = buyAmounts.map((a) => parseOkb(a));
223
+ await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
224
+ const buyWei = buyWeis[i] ?? 0n;
225
+ if (buyWei <= 0n)
226
+ return;
227
+ await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWei + parseOkb('0.0003'), `owner${i + 1}/dex-buy-prefund-before-estimate`);
228
+ });
229
+ const buyCallDatas = buyWeis.map((buyWei, i) => {
230
+ const sender = senders[i];
231
+ const swapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], sender, deadline);
232
+ return encodeExecute(effectiveRouter, buyWei, swapData);
233
+ });
234
+ const initCodes = accountInfos.map((ai, i) => (ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address)));
235
+ const { userOps: buyUserOps, prefundWeis } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
236
+ ops: accountInfos.map((ai, i) => ({
237
+ sender: ai.sender,
238
+ nonce: nonceMap.next(ai.sender),
239
+ callData: buyCallDatas[i],
240
+ initCode: initCodes[i],
241
+ })),
242
+ });
243
+ await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
244
+ const buyWei = buyWeis[i] ?? 0n;
245
+ await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWei + (prefundWeis[i] ?? 0n) + parseOkb('0.0002'), `owner${i + 1}/dex-buy-fund`);
246
+ });
247
+ const signedBuy = await mapWithConcurrency(buyUserOps, 10, async (op, i) => this.aaManager.signUserOp(op, wallets[i]));
248
+ const buyOps = signedBuy.map((s) => s.userOp);
249
+ const buyResult = await this.runHandleOps('dex-buyBundle', buyOps, bundlerSigner, beneficiary);
250
+ if (!buyResult) {
251
+ throw new Error('买入交易失败');
252
+ }
253
+ // 3) 授权 + 卖出(同一笔 handleOps)
254
+ const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
255
+ const allowances = await this.portalQuery.getMultipleAllowances(tokenAddress, senders, effectiveRouter);
256
+ const sellOps = [];
257
+ const sellPerWallet = await mapWithConcurrency(wallets, 4, async (w, i) => {
258
+ const sender = senders[i];
259
+ const balance = tokenBalances.get(sender) ?? 0n;
260
+ if (balance === 0n) {
261
+ console.log(`[owner${i + 1}] 没买到代币,跳过卖出`);
262
+ return [];
263
+ }
264
+ const allowance = allowances.get(sender) ?? 0n;
265
+ const needApprove = allowance < balance;
266
+ const out = [];
267
+ // buy 已经用过一次,因此 initCode = 0x(且 nonce 已更新过)
268
+ const initCode = '0x';
269
+ if (needApprove) {
270
+ const approveNonce = nonceMap.next(sender);
271
+ const approveCallData = encodeExecute(tokenAddress, 0n, encodeApproveCall(effectiveRouter, ethers.MaxUint256));
272
+ const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
273
+ ownerWallet: w,
274
+ sender,
275
+ nonce: approveNonce,
276
+ initCode,
277
+ callData: approveCallData,
278
+ deployed: true,
279
+ fixedGas: {
280
+ ...(effCfg.fixedGas ?? {}),
281
+ callGasLimit: effCfg.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_APPROVE,
282
+ },
283
+ });
284
+ const signed = await this.aaManager.signUserOp(userOp, w);
285
+ out.push(signed.userOp);
286
+ }
287
+ const sellAmount = (balance * BigInt(sellPercent)) / 100n;
288
+ if (sellAmount === 0n)
289
+ return out;
290
+ const sellNonce = nonceMap.next(sender);
291
+ const sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellAmount, 0n, [tokenAddress, WOKB], sender, deadline);
292
+ const sellCallData = encodeExecute(effectiveRouter, 0n, sellSwapData);
293
+ const signedSell = await this.aaManager.buildUserOpWithState({
294
+ ownerWallet: w,
295
+ sender,
296
+ nonce: sellNonce,
297
+ initCode,
298
+ callData: sellCallData,
299
+ signOnly: false,
300
+ });
301
+ out.push(signedSell.userOp);
302
+ return out;
303
+ });
304
+ for (const ops of sellPerWallet)
305
+ sellOps.push(...ops);
306
+ const sellResult = await this.runHandleOps('dex-sellBundle', sellOps, bundlerSigner, beneficiary);
307
+ if (!sellResult) {
308
+ throw new Error('卖出交易失败');
309
+ }
310
+ // 4) 归集 OKB(sell 后状态)
311
+ let withdrawResult;
312
+ let totalProfitWei = 0n;
313
+ if (withdrawToOwner) {
314
+ const withdrawOps = [];
315
+ const okbBalances = await this.portalQuery.getMultipleOkbBalances(senders);
316
+ const withdrawItems = wallets.map((w, i) => ({
317
+ i,
318
+ ownerWallet: w,
319
+ sender: senders[i],
320
+ senderBalance: okbBalances.get(senders[i]) ?? 0n,
321
+ nonce: nonceMap.peek(senders[i]),
322
+ initCode: '0x',
323
+ }));
324
+ const signedWithdraws = await mapWithConcurrency(withdrawItems, 3, async (it) => {
325
+ const built = await this.buildWithdrawUserOpWithState({
326
+ ownerWallet: it.ownerWallet,
327
+ sender: it.sender,
328
+ nonce: it.nonce,
329
+ initCode: it.initCode,
330
+ senderBalance: it.senderBalance,
331
+ reserveWei,
332
+ ownerName: `owner${it.i + 1}`,
333
+ configOverride: config,
334
+ });
335
+ if (built) {
336
+ nonceMap.commit(it.sender, it.nonce);
337
+ withdrawOps.push(built.userOp);
338
+ if (profitSettings.extractProfit) {
339
+ const withdrawAmount = it.senderBalance > built.prefundWei + reserveWei
340
+ ? it.senderBalance - built.prefundWei - reserveWei
341
+ : 0n;
342
+ totalProfitWei += calculateProfitWei(withdrawAmount, profitSettings.profitBps);
343
+ }
344
+ }
345
+ });
346
+ void signedWithdraws; // silence unused (build is applied in loop)
347
+ if (withdrawOps.length > 0) {
348
+ withdrawResult = await this.runHandleOps('dex-withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
349
+ }
350
+ }
351
+ const finalBalances = await this.portalQuery.getMultipleOkbBalances(senders);
352
+ return {
353
+ buyResult,
354
+ sellResult,
355
+ withdrawResult,
356
+ finalBalances,
357
+ profit: withdrawToOwner
358
+ ? {
359
+ extractProfit: profitSettings.extractProfit,
360
+ profitBps: profitSettings.profitBps,
361
+ profitRecipient: profitSettings.profitRecipient,
362
+ totalProfitWei: totalProfitWei.toString(),
363
+ }
364
+ : undefined,
365
+ // approveResult: 这里我们把 approve 合并进 sellBundle 了,不单独返回
366
+ };
367
+ }
368
+ }
369
+ // ============================================================================
370
+ // 便捷函数
371
+ // ============================================================================
372
+ export function createDexBundleExecutor(config) {
373
+ return new DexBundleExecutor(config);
374
+ }
375
+ export async function dexBundleBuySell(params) {
376
+ const executor = createDexBundleExecutor(params.config);
377
+ return executor.bundleBuySell(params);
378
+ }