four-flap-meme-sdk 1.6.1 → 1.6.4

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.
@@ -1193,26 +1193,7 @@ export class BundleExecutor {
1193
1193
  gasLimit: effConfig.gasLimit,
1194
1194
  gasPrice: effConfig.minGasPriceGwei ? ethers.parseUnits(effConfig.minGasPriceGwei.toString(), 'gwei') : undefined
1195
1195
  }) ?? undefined;
1196
- if (profitSettingsBuySell.extractProfit) {
1197
- const totalProfitForTail = nativeBuyProfitAmount + totalSellWithdrawProfitWei;
1198
- if (totalProfitForTail > 0n) {
1199
- const provider = this.aaManager.getProvider();
1200
- const feeData = await provider.getFeeData();
1201
- const nonce = await provider.getTransactionCount(bundlerSigner.address, 'pending');
1202
- const tailTx = await bundlerSigner.signTransaction({
1203
- to: profitSettingsBuySell.profitRecipient,
1204
- value: totalProfitForTail,
1205
- data: '0x',
1206
- nonce,
1207
- gasLimit: this.config.profitTailGasLimit ?? 21000n,
1208
- gasPrice: feeData.gasPrice ?? 100000000n,
1209
- chainId: this.config.chainId ?? 196,
1210
- type: 0,
1211
- });
1212
- const tx = await provider.broadcastTransaction(tailTx);
1213
- await tx.wait();
1214
- }
1215
- }
1196
+ // 利润已在 withdraw UserOp 内部通过 executeBatch 刮取,无需单独广播 tail tx
1216
1197
  }
1217
1198
  }
1218
1199
  // 最终余额
@@ -1721,7 +1702,7 @@ export class BundleExecutor {
1721
1702
  signedTransactions.push(signedDexTx);
1722
1703
  currentNonce++;
1723
1704
  }
1724
- // --- 5. 利润提取 (Direct EOA transfer) ---
1705
+ // --- 5. 利润提取 (AA 内部 UserOp,不再使用 Tail Transaction) ---
1725
1706
  let totalProfitWei = 0n;
1726
1707
  if (profitSettings.extractProfit) {
1727
1708
  totalProfitWei += calculateProfitWei(totalCurveBuyWei, profitSettings.profitBps);
@@ -1729,13 +1710,34 @@ export class BundleExecutor {
1729
1710
  totalProfitWei += calculateProfitWei(totalDexBuyWei, profitSettings.profitBps);
1730
1711
  }
1731
1712
  if (totalProfitWei > 0n) {
1732
- const signedProfitTx = await this.signProfitTransaction({
1733
- payerWallet,
1734
- profitWei: totalProfitWei,
1735
- recipient: profitSettings.profitRecipient,
1736
- nonce: currentNonce
1713
+ // 使用 Payer AA 账户发起利润转账 UserOp(不再使用 Tail Transaction)
1714
+ const profitCallData = encodeExecute(profitSettings.profitRecipient, totalProfitWei, '0x');
1715
+ const payerInitCode = payerAccount.deployed ? '0x' : aaManager.generateInitCode(payerWallet.address);
1716
+ const { userOp: profitOp, prefundWei: profitPrefund } = await aaManager.buildUserOpWithFixedGas({
1717
+ ownerWallet: payerWallet,
1718
+ sender: payerAccount.sender,
1719
+ nonce: nonceMap.next(payerAccount.sender),
1720
+ initCode: payerInitCode,
1721
+ callData: profitCallData,
1722
+ deployed: payerAccount.deployed,
1723
+ fixedGas: {
1724
+ ...(effConfig.fixedGas ?? {}),
1725
+ callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
1726
+ },
1727
+ });
1728
+ // 确保 Payer AA 账户有足够余额支付利润转账
1729
+ await aaManager.ensureSenderBalance(payerWallet, payerAccount.sender, totalProfitWei + profitPrefund + parseOkb('0.0001'), 'profit-transfer-fund');
1730
+ const signedProfitOp = await aaManager.signUserOp(profitOp, payerWallet);
1731
+ // 签名利润 handleOps 交易
1732
+ const signedProfitHandleOpsTx = await this.signHandleOpsTx({
1733
+ ops: [signedProfitOp.userOp],
1734
+ payerWallet: payerWallet,
1735
+ beneficiary: params.beneficiary ?? payerWallet.address,
1736
+ nonce: currentNonce,
1737
+ gasLimit: effConfig.gasLimit,
1738
+ gasPrice: effConfig.minGasPriceGwei ? ethers.parseUnits(effConfig.minGasPriceGwei.toString(), 'gwei') : undefined
1737
1739
  });
1738
- signedTransactions.push(signedProfitTx);
1740
+ signedTransactions.push(signedProfitHandleOpsTx);
1739
1741
  }
1740
1742
  }
1741
1743
  return {
@@ -5,6 +5,7 @@ import { XLayerConfig, BundleSwapSignParams, BundleSwapSignResult, BundleBatchSw
5
5
  /**
6
6
  * XLayer AA 外盘换手执行器
7
7
  * ✅ 支持 V2 和 V3 交易
8
+ * ✅ 支持 ERC20 稳定币(USDT/USDC/USDT0)
8
9
  */
9
10
  export declare class AADexSwapExecutor {
10
11
  private aaManager;
@@ -35,6 +36,7 @@ export declare class AADexSwapExecutor {
35
36
  /**
36
37
  * AA 外盘批量换手签名
37
38
  * ✅ 支持 V2 和 V3 交易
39
+ * ✅ 支持 ERC20 稳定币(USDT/USDC/USDT0)
38
40
  */
39
41
  bundleBatchSwapSign(params: BundleBatchSwapSignParams & {
40
42
  skipApprovalCheck?: boolean;
@@ -4,11 +4,11 @@
4
4
  import { Wallet, ethers } from 'ethers';
5
5
  import { AANonceMap, } from './types.js';
6
6
  import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, POTATOSWAP_V3_FACTORY, WOKB, MULTICALL3, } from './constants.js';
7
- import { AAAccountManager, encodeExecute } from './aa-account.js';
7
+ import { AAAccountManager, encodeExecute, encodeExecuteBatch } from './aa-account.js';
8
8
  import { encodeApproveCall, lpFeeProfileToV3Fee, } from './portal-ops.js';
9
- import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, } from './dex.js';
9
+ import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, encodeSwapExactTokensForTokensSupportingFee, } from './dex.js';
10
10
  import { BundleExecutor } from './bundle.js';
11
- import { PROFIT_CONFIG } from '../utils/constants.js';
11
+ import { PROFIT_CONFIG, ZERO_ADDRESS } from '../utils/constants.js';
12
12
  const multicallIface = new ethers.Interface([
13
13
  'function aggregate3Value((address target,bool allowFailure,uint256 value,bytes callData)[] calls) payable returns ((bool success,bytes returnData)[] returnData)',
14
14
  ]);
@@ -24,7 +24,7 @@ const V3_POOL_ABI = [
24
24
  'function token1() view returns (address)',
25
25
  ];
26
26
  const V3_FEE_DENOMINATOR = 1000000n;
27
- const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
27
+ // ZERO_ADDRESS 已从 ../utils/constants.js 导入
28
28
  /**
29
29
  * 使用 V3 Pool 的 slot0 获取 Token → WOKB 的报价
30
30
  * 这是一个现货价计算,比 V3 Quoter 更简单但精度稍低
@@ -103,9 +103,16 @@ function calculateProfitWei(amountWei, profitBps) {
103
103
  return 0n;
104
104
  return (amountWei * BigInt(profitBps)) / 10000n;
105
105
  }
106
+ /**
107
+ * ✅ 判断是否使用原生代币(OKB)
108
+ */
109
+ function isNativeToken(quoteToken) {
110
+ return !quoteToken || quoteToken === ZERO_ADDRESS || quoteToken.toLowerCase() === WOKB.toLowerCase();
111
+ }
106
112
  /**
107
113
  * XLayer AA 外盘换手执行器
108
114
  * ✅ 支持 V2 和 V3 交易
115
+ * ✅ 支持 ERC20 稳定币(USDT/USDC/USDT0)
109
116
  */
110
117
  export class AADexSwapExecutor {
111
118
  constructor(config = {}) {
@@ -149,9 +156,12 @@ export class AADexSwapExecutor {
149
156
  * ✅ 支持 V2 和 V3 交易
150
157
  */
151
158
  async bundleSwapSign(params) {
152
- const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, tradeType, lpFeeProfile = 0, } = params;
159
+ const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, tradeType, lpFeeProfile = 0, quoteToken, quoteTokenDecimals = 6, // XLayer USDT/USDC/USDT0 都是 6 位精度
160
+ } = params;
153
161
  const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
154
162
  const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
163
+ // ✅ ERC20 稳定币支持:判断是否使用原生代币
164
+ const useNativeToken = isNativeToken(quoteToken);
155
165
  const isV3Trade = this.isV3(tradeType);
156
166
  const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn, tradeType });
157
167
  const provider = this.aaManager.getProvider();
@@ -253,27 +263,34 @@ export class AADexSwapExecutor {
253
263
  const signedApprove = await this.aaManager.signUserOp(userOp, sellerOwner);
254
264
  outOps.push(signedApprove.userOp);
255
265
  }
256
- // Sell op - ✅ 支持 V2 和 V3
266
+ // Sell op - ✅ 支持 V2 和 V3,支持 ERC20 稳定币
257
267
  const deadline = this.getDexDeadline();
258
268
  let sellSwapData;
259
- if (isV3Trade) {
260
- // V3: 使用 multicall(exactInputSingle + unwrapWETH9)
261
- // 注意:V3 卖出得到的是 WOKB,需要 unwrap 成 OKB
262
- sellSwapData = encodeSwapExactTokensForETHV3({
263
- tokenIn: tokenAddress,
264
- tokenOut: WOKB,
265
- fee: lpFeeProfileToV3Fee(lpFeeProfile),
266
- recipient: sellerSender, // 会被函数内部替换为 ADDRESS_THIS
267
- deadline,
268
- amountIn: sellAmountWei,
269
- amountOutMinimum: 0n,
270
- sqrtPriceLimitX96: 0n,
271
- unwrapRecipient: sellerSender, // ✅ unwrap 后发送到 sellerSender
272
- });
269
+ if (useNativeToken) {
270
+ // 原生代币模式:Token OKB
271
+ if (isV3Trade) {
272
+ // V3: 使用 multicall(exactInputSingle + unwrapWETH9)
273
+ sellSwapData = encodeSwapExactTokensForETHV3({
274
+ tokenIn: tokenAddress,
275
+ tokenOut: WOKB,
276
+ fee: lpFeeProfileToV3Fee(lpFeeProfile),
277
+ recipient: sellerSender,
278
+ deadline,
279
+ amountIn: sellAmountWei,
280
+ amountOutMinimum: 0n,
281
+ sqrtPriceLimitX96: 0n,
282
+ unwrapRecipient: sellerSender,
283
+ });
284
+ }
285
+ else {
286
+ // V2: Token → OKB
287
+ sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellAmountWei, 0n, [tokenAddress, WOKB], sellerSender, deadline);
288
+ }
273
289
  }
274
290
  else {
275
- // V2: 使用 swapExactTokensForETHSupportingFeeOnTransferTokens
276
- sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellAmountWei, 0n, [tokenAddress, WOKB], sellerSender, deadline);
291
+ // ERC20 模式:Token → quoteToken (USDT/USDC)
292
+ // V2/V3 都使用 Token Token 路径
293
+ sellSwapData = encodeSwapExactTokensForTokensSupportingFee(sellAmountWei, 0n, [tokenAddress, quoteToken], sellerSender, deadline);
277
294
  }
278
295
  const sellCallData = encodeExecute(effectiveRouter, 0n, sellSwapData);
279
296
  const signedSell = await this.aaManager.buildUserOpWithState({
@@ -285,10 +302,32 @@ export class AADexSwapExecutor {
285
302
  signOnly: true,
286
303
  });
287
304
  outOps.push(signedSell.userOp);
288
- // Profit 由尾部交易处理(不再在 handleOps 内部追加 profit UserOp)
289
- // 卖出所得 OKB 转给买方 AA(Sender)
305
+ // 卖出所得资金转给买方 AA(Sender) + 利润刮取(AA 内部)
306
+ // 原生代币模式:直接转 OKB;ERC20 模式:调用 ERC20 transfer
290
307
  if (sellerSender.toLowerCase() !== buyerSender.toLowerCase() && finalBuyAmountWei > 0n) {
291
- const transferCallData = encodeExecute(buyerSender, finalBuyAmountWei, '0x');
308
+ let transferCallData;
309
+ if (useNativeToken) {
310
+ // ✅ 原生代币模式:使用 executeBatch 同时转账给买方和利润接收者
311
+ if (extractProfit && profitWei > 0n) {
312
+ transferCallData = encodeExecuteBatch([buyerSender, profitRecipient], [finalBuyAmountWei, profitWei], ['0x', '0x']);
313
+ }
314
+ else {
315
+ transferCallData = encodeExecute(buyerSender, finalBuyAmountWei, '0x');
316
+ }
317
+ }
318
+ else {
319
+ // ✅ ERC20 模式:使用 ERC20 transfer 批量转账
320
+ const erc20Iface = new ethers.Interface(['function transfer(address to, uint256 amount) returns (bool)']);
321
+ if (extractProfit && profitWei > 0n) {
322
+ const transferToBuyer = erc20Iface.encodeFunctionData('transfer', [buyerSender, finalBuyAmountWei]);
323
+ const transferToProfit = erc20Iface.encodeFunctionData('transfer', [profitRecipient, profitWei]);
324
+ transferCallData = encodeExecuteBatch([quoteToken, quoteToken], [0n, 0n], [transferToBuyer, transferToProfit]);
325
+ }
326
+ else {
327
+ const transferData = erc20Iface.encodeFunctionData('transfer', [buyerSender, finalBuyAmountWei]);
328
+ transferCallData = encodeExecute(quoteToken, 0n, transferData);
329
+ }
330
+ }
292
331
  const signedTransfer = await this.aaManager.buildUserOpWithState({
293
332
  ownerWallet: sellerOwner,
294
333
  sender: sellerSender,
@@ -299,26 +338,34 @@ export class AADexSwapExecutor {
299
338
  });
300
339
  outOps.push(signedTransfer.userOp);
301
340
  }
302
- // Buy op - ✅ 支持 V2 V3
303
- let buySwapData;
304
- if (isV3Trade) {
305
- // V3: 使用 exactInputSingle(注意:V3 买入需要用 WOKB 作为输入)
306
- buySwapData = encodeSwapExactETHForTokensV3({
307
- tokenIn: WOKB,
308
- tokenOut: tokenAddress,
309
- fee: lpFeeProfileToV3Fee(lpFeeProfile),
310
- recipient: buyerSender,
311
- deadline,
312
- amountIn: finalBuyAmountWei,
313
- amountOutMinimum: 0n,
314
- sqrtPriceLimitX96: 0n,
315
- });
341
+ // Buy op - ✅ 支持 V2/V3,支持 ERC20 稳定币
342
+ let buyCallData;
343
+ if (useNativeToken) {
344
+ // 原生代币模式:OKB Token
345
+ let buySwapData;
346
+ if (isV3Trade) {
347
+ buySwapData = encodeSwapExactETHForTokensV3({
348
+ tokenIn: WOKB,
349
+ tokenOut: tokenAddress,
350
+ fee: lpFeeProfileToV3Fee(lpFeeProfile),
351
+ recipient: buyerSender,
352
+ deadline,
353
+ amountIn: finalBuyAmountWei,
354
+ amountOutMinimum: 0n,
355
+ sqrtPriceLimitX96: 0n,
356
+ });
357
+ }
358
+ else {
359
+ buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], buyerSender, deadline);
360
+ }
361
+ buyCallData = encodeExecute(effectiveRouter, finalBuyAmountWei, buySwapData);
316
362
  }
317
363
  else {
318
- // V2: 使用 swapExactETHForTokensSupportingFeeOnTransferTokens
319
- buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], buyerSender, deadline);
364
+ // ERC20 模式:quoteToken → Token(需要先 approve)
365
+ const approveData = encodeApproveCall(effectiveRouter, finalBuyAmountWei);
366
+ const swapData = encodeSwapExactTokensForTokensSupportingFee(finalBuyAmountWei, 0n, [quoteToken, tokenAddress], buyerSender, deadline);
367
+ buyCallData = encodeExecuteBatch([quoteToken, effectiveRouter], [0n, 0n], [approveData, swapData]);
320
368
  }
321
- const buyCallData = encodeExecute(effectiveRouter, finalBuyAmountWei, buySwapData);
322
369
  const signedBuy = await this.aaManager.buildUserOpWithState({
323
370
  ownerWallet: buyerOwner,
324
371
  sender: buyerSender,
@@ -334,31 +381,8 @@ export class AADexSwapExecutor {
334
381
  beneficiary,
335
382
  nonce: payerStartNonce,
336
383
  });
384
+ // ✅ 利润已在 AA 内部通过 executeBatch 刮取,不需要 Tail Tx
337
385
  const signedTransactions = [signedHandleOps];
338
- let currentNonce = Number(ethers.Transaction.from(signedHandleOps).nonce) + 1;
339
- // 1. 处理 Route Tail Tx (如果存在)
340
- if (routeAddress) {
341
- const tailTx = await payerWallet.signTransaction({
342
- to: routeAddress,
343
- value: 0n,
344
- nonce: currentNonce++,
345
- gasLimit: 21000n,
346
- gasPrice: ethers.Transaction.from(signedHandleOps).gasPrice,
347
- chainId: this.config.chainId ?? 196,
348
- type: 0,
349
- });
350
- signedTransactions.push(tailTx);
351
- }
352
- // 2. ✅ 处理 Profit Tail Tx (修复:补全缺失的利润签名)
353
- if (extractProfit && profitWei > 0n) {
354
- const tailTx = await this.bundleExecutor['signProfitTransaction']({
355
- payerWallet,
356
- profitWei,
357
- recipient: profitRecipient,
358
- nonce: currentNonce++,
359
- });
360
- signedTransactions.push(tailTx);
361
- }
362
386
  return {
363
387
  signedTransactions,
364
388
  metadata: {
@@ -384,11 +408,15 @@ export class AADexSwapExecutor {
384
408
  /**
385
409
  * AA 外盘批量换手签名
386
410
  * ✅ 支持 V2 和 V3 交易
411
+ * ✅ 支持 ERC20 稳定币(USDT/USDC/USDT0)
387
412
  */
388
413
  async bundleBatchSwapSign(params) {
389
- const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, tradeType, lpFeeProfile = 0, } = params;
414
+ const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, tradeType, lpFeeProfile = 0, quoteToken, quoteTokenDecimals = 6, // XLayer USDT/USDC/USDT0 都是 6 位精度
415
+ } = params;
390
416
  const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
391
417
  const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
418
+ // ✅ ERC20 稳定币支持:判断是否使用原生代币
419
+ const useNativeToken = isNativeToken(quoteToken);
392
420
  const isV3Trade = this.isV3(tradeType);
393
421
  const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn, tradeType });
394
422
  const provider = this.aaManager.getProvider();
@@ -446,27 +474,31 @@ export class AADexSwapExecutor {
446
474
  const signedApprove = await this.aaManager.signUserOp(userOp, sellerOwner);
447
475
  outOps.push(signedApprove.userOp);
448
476
  }
449
- // Sell op - ✅ 支持 V2 V3
477
+ // Sell op - ✅ 支持 V2/V3,支持 ERC20 稳定币
450
478
  const deadline = this.getDexDeadline();
451
479
  let sellSwapData;
452
- if (isV3Trade) {
453
- // V3: 使用 multicall(exactInputSingle + unwrapWETH9)
454
- // 注意:V3 卖出得到的是 WOKB,需要 unwrap 成 OKB
455
- sellSwapData = encodeSwapExactTokensForETHV3({
456
- tokenIn: tokenAddress,
457
- tokenOut: WOKB,
458
- fee: lpFeeProfileToV3Fee(lpFeeProfile),
459
- recipient: sellerAi.sender, // 会被函数内部替换为 ADDRESS_THIS
460
- deadline,
461
- amountIn: sellAmountWei,
462
- amountOutMinimum: 0n,
463
- sqrtPriceLimitX96: 0n,
464
- unwrapRecipient: sellerAi.sender, // ✅ unwrap 后发送到 sellerAi.sender
465
- });
480
+ if (useNativeToken) {
481
+ // 原生代币模式:Token OKB
482
+ if (isV3Trade) {
483
+ sellSwapData = encodeSwapExactTokensForETHV3({
484
+ tokenIn: tokenAddress,
485
+ tokenOut: WOKB,
486
+ fee: lpFeeProfileToV3Fee(lpFeeProfile),
487
+ recipient: sellerAi.sender,
488
+ deadline,
489
+ amountIn: sellAmountWei,
490
+ amountOutMinimum: 0n,
491
+ sqrtPriceLimitX96: 0n,
492
+ unwrapRecipient: sellerAi.sender,
493
+ });
494
+ }
495
+ else {
496
+ sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellAmountWei, 0n, [tokenAddress, WOKB], sellerAi.sender, deadline);
497
+ }
466
498
  }
467
499
  else {
468
- // V2: 使用 swapExactTokensForETHSupportingFeeOnTransferTokens
469
- sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellAmountWei, 0n, [tokenAddress, WOKB], sellerAi.sender, deadline);
500
+ // ERC20 模式:Token → quoteToken (USDT/USDC)
501
+ sellSwapData = encodeSwapExactTokensForTokensSupportingFee(sellAmountWei, 0n, [tokenAddress, quoteToken], sellerAi.sender, deadline);
470
502
  }
471
503
  const sellCallData = encodeExecute(effectiveRouter, 0n, sellSwapData);
472
504
  const signedSell = await this.aaManager.buildUserOpWithState({
@@ -479,7 +511,10 @@ export class AADexSwapExecutor {
479
511
  });
480
512
  outOps.push(signedSell.userOp);
481
513
  // 先计算 buyAmountsWei(用于后续分发与校验)
482
- const buyAmountsWei = buyAmountsOkb.map(a => ethers.parseEther(a));
514
+ // 原生代币模式:使用 parseEther;ERC20 模式:使用 parseUnits
515
+ const buyAmountsWei = useNativeToken
516
+ ? buyAmountsOkb.map(a => ethers.parseEther(a))
517
+ : buyAmountsOkb.map(a => ethers.parseUnits(a, quoteTokenDecimals));
483
518
  const totalBuyWei = buyAmountsWei.reduce((a, b) => a + (b ?? 0n), 0n);
484
519
  // Profit op:估算卖出输出,按比例刮取(但必须保证分发/买入资金充足)
485
520
  // ✅ V3 模式:优先使用 slot0 报价
@@ -511,22 +546,41 @@ export class AADexSwapExecutor {
511
546
  const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
512
547
  const profitCap = quotedSellOutWei > totalBuyWei ? (quotedSellOutWei - totalBuyWei) : 0n;
513
548
  const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
514
- // Profit 由尾部交易处理(不再在 handleOps 内部追加 profit UserOp)
515
- // ✅ 卖出所得 OKB 分发给多个买方 AA(Sender)(支持多跳)
549
+ // 利润在分发阶段通过 AA 内部刮取
550
+ // ✅ 卖出所得资金分发给多个买方 AA(Sender)(支持多跳)+ 利润刮取
551
+ // 原生代币模式:使用 Multicall3 批量转账
552
+ // ERC20 模式:使用 ERC20 transfer 批量转账
516
553
  const buyerSenders = buyerAis.map(ai => ai.sender);
517
554
  const hopCountRaw = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
518
555
  const hopCount = Math.min(hopCountRaw, buyerSenders.length);
519
556
  const maxPerOp = Math.max(1, Math.floor(Number(effectiveConfig.maxTransfersPerUserOpNative ?? 30)));
557
+ const erc20Iface = new ethers.Interface(['function transfer(address to, uint256 amount) returns (bool)']);
520
558
  if (buyerSenders.length > 0 && totalBuyWei > 0n) {
521
559
  if (hopCount <= 0) {
560
+ // ✅ 直接分发模式:将利润接收者加入分发列表(AA 内部刮取)
522
561
  const items = buyerSenders.map((to, i) => ({ to, value: buyAmountsWei[i] ?? 0n })).filter(x => x.value > 0n);
562
+ // 添加利润接收者到分发列表
563
+ if (extractProfit && profitWei > 0n) {
564
+ items.push({ to: profitRecipient, value: profitWei });
565
+ }
523
566
  const chunks = chunkArray(items, maxPerOp);
524
567
  for (const ch of chunks) {
525
- const { totalValue, data } = encodeNativeDisperseViaMulticall3({
526
- to: ch.map(x => x.to),
527
- values: ch.map(x => x.value),
528
- });
529
- const callData = encodeExecute(MULTICALL3, totalValue, data);
568
+ let callData;
569
+ if (useNativeToken) {
570
+ // 原生代币模式:使用 Multicall3 批量转账
571
+ const { totalValue, data } = encodeNativeDisperseViaMulticall3({
572
+ to: ch.map(x => x.to),
573
+ values: ch.map(x => x.value),
574
+ });
575
+ callData = encodeExecute(MULTICALL3, totalValue, data);
576
+ }
577
+ else {
578
+ // ✅ ERC20 模式:使用 executeBatch 批量调用 ERC20 transfer
579
+ const targets = ch.map(() => quoteToken);
580
+ const values = ch.map(() => 0n);
581
+ const datas = ch.map(x => erc20Iface.encodeFunctionData('transfer', [x.to, x.value]));
582
+ callData = encodeExecuteBatch(targets, values, datas);
583
+ }
530
584
  const signedDisperse = await this.aaManager.buildUserOpWithState({
531
585
  ownerWallet: sellerOwner,
532
586
  sender: sellerAi.sender,
@@ -539,10 +593,32 @@ export class AADexSwapExecutor {
539
593
  }
540
594
  }
541
595
  else {
596
+ // ✅ 多跳模式:在第一跳时同时将利润刮取到 profitRecipient
542
597
  const hopSenders = buyerSenders.slice(0, hopCount);
543
598
  const hopOwners = buyerOwners.slice(0, hopCount);
544
599
  const hop0 = hopSenders[0];
545
- const callData0 = encodeExecute(hop0, totalBuyWei, '0x');
600
+ let callData0;
601
+ if (useNativeToken) {
602
+ // ✅ 原生代币模式
603
+ if (extractProfit && profitWei > 0n) {
604
+ callData0 = encodeExecuteBatch([hop0, profitRecipient], [totalBuyWei, profitWei], ['0x', '0x']);
605
+ }
606
+ else {
607
+ callData0 = encodeExecute(hop0, totalBuyWei, '0x');
608
+ }
609
+ }
610
+ else {
611
+ // ✅ ERC20 模式
612
+ if (extractProfit && profitWei > 0n) {
613
+ const transferToHop0 = erc20Iface.encodeFunctionData('transfer', [hop0, totalBuyWei]);
614
+ const transferToProfit = erc20Iface.encodeFunctionData('transfer', [profitRecipient, profitWei]);
615
+ callData0 = encodeExecuteBatch([quoteToken, quoteToken], [0n, 0n], [transferToHop0, transferToProfit]);
616
+ }
617
+ else {
618
+ const transferData = erc20Iface.encodeFunctionData('transfer', [hop0, totalBuyWei]);
619
+ callData0 = encodeExecute(quoteToken, 0n, transferData);
620
+ }
621
+ }
546
622
  const signedToHop0 = await this.aaManager.buildUserOpWithState({
547
623
  ownerWallet: sellerOwner,
548
624
  sender: sellerAi.sender,
@@ -561,7 +637,14 @@ export class AADexSwapExecutor {
561
637
  const remaining = totalBuyWei - prefixKept;
562
638
  if (remaining <= 0n)
563
639
  break;
564
- const callData = encodeExecute(next, remaining, '0x');
640
+ let callData;
641
+ if (useNativeToken) {
642
+ callData = encodeExecute(next, remaining, '0x');
643
+ }
644
+ else {
645
+ const transferData = erc20Iface.encodeFunctionData('transfer', [next, remaining]);
646
+ callData = encodeExecute(quoteToken, 0n, transferData);
647
+ }
565
648
  const signedHop = await this.aaManager.buildUserOpWithState({
566
649
  ownerWallet: hopOwners[j],
567
650
  sender,
@@ -580,11 +663,20 @@ export class AADexSwapExecutor {
580
663
  const restItems = rest.map((to, i) => ({ to, value: restAmounts[i] ?? 0n })).filter(x => x.value > 0n);
581
664
  const restChunks = chunkArray(restItems, maxPerOp);
582
665
  for (const ch of restChunks) {
583
- const { totalValue, data } = encodeNativeDisperseViaMulticall3({
584
- to: ch.map(x => x.to),
585
- values: ch.map(x => x.value),
586
- });
587
- const callData = encodeExecute(MULTICALL3, totalValue, data);
666
+ let callData;
667
+ if (useNativeToken) {
668
+ const { totalValue, data } = encodeNativeDisperseViaMulticall3({
669
+ to: ch.map(x => x.to),
670
+ values: ch.map(x => x.value),
671
+ });
672
+ callData = encodeExecute(MULTICALL3, totalValue, data);
673
+ }
674
+ else {
675
+ const targets = ch.map(() => quoteToken);
676
+ const values = ch.map(() => 0n);
677
+ const datas = ch.map(x => erc20Iface.encodeFunctionData('transfer', [x.to, x.value]));
678
+ callData = encodeExecuteBatch(targets, values, datas);
679
+ }
588
680
  const signedDisperse = await this.aaManager.buildUserOpWithState({
589
681
  ownerWallet: lastHopOwner,
590
682
  sender: lastHopSender,
@@ -597,29 +689,37 @@ export class AADexSwapExecutor {
597
689
  }
598
690
  }
599
691
  }
600
- // Batch Buy ops(分发后再执行)- ✅ 支持 V2 V3
692
+ // Batch Buy ops(分发后再执行)- ✅ 支持 V2/V3,支持 ERC20 稳定币
601
693
  for (let i = 0; i < buyerOwners.length; i++) {
602
694
  const ai = buyerAis[i];
603
695
  const buyWei = buyAmountsWei[i];
604
- let buySwapData;
605
- if (isV3Trade) {
606
- // V3: 使用 exactInputSingle
607
- buySwapData = encodeSwapExactETHForTokensV3({
608
- tokenIn: WOKB,
609
- tokenOut: tokenAddress,
610
- fee: lpFeeProfileToV3Fee(lpFeeProfile),
611
- recipient: ai.sender,
612
- deadline,
613
- amountIn: buyWei,
614
- amountOutMinimum: 0n,
615
- sqrtPriceLimitX96: 0n,
616
- });
696
+ let buyCallData;
697
+ if (useNativeToken) {
698
+ // 原生代币模式:OKB → Token
699
+ let buySwapData;
700
+ if (isV3Trade) {
701
+ buySwapData = encodeSwapExactETHForTokensV3({
702
+ tokenIn: WOKB,
703
+ tokenOut: tokenAddress,
704
+ fee: lpFeeProfileToV3Fee(lpFeeProfile),
705
+ recipient: ai.sender,
706
+ deadline,
707
+ amountIn: buyWei,
708
+ amountOutMinimum: 0n,
709
+ sqrtPriceLimitX96: 0n,
710
+ });
711
+ }
712
+ else {
713
+ buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], ai.sender, deadline);
714
+ }
715
+ buyCallData = encodeExecute(effectiveRouter, buyWei, buySwapData);
617
716
  }
618
717
  else {
619
- // V2: 使用 swapExactETHForTokensSupportingFeeOnTransferTokens
620
- buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], ai.sender, deadline);
718
+ // ERC20 模式:quoteToken → Token(需要先 approve)
719
+ const approveData = encodeApproveCall(effectiveRouter, buyWei);
720
+ const swapData = encodeSwapExactTokensForTokensSupportingFee(buyWei, 0n, [quoteToken, tokenAddress], ai.sender, deadline);
721
+ buyCallData = encodeExecuteBatch([quoteToken, effectiveRouter], [0n, 0n], [approveData, swapData]);
621
722
  }
622
- const buyCallData = encodeExecute(effectiveRouter, buyWei, buySwapData);
623
723
  const signedBuy = await this.aaManager.buildUserOpWithState({
624
724
  ownerWallet: buyerOwners[i],
625
725
  sender: ai.sender,
@@ -636,17 +736,8 @@ export class AADexSwapExecutor {
636
736
  beneficiary,
637
737
  nonce: payerStartNonce,
638
738
  });
739
+ // ✅ 利润已在 AA 内部通过分发阶段刮取,不需要 Tail Tx
639
740
  const signedTransactions = [signedHandleOps];
640
- if (extractProfit && profitWei > 0n) {
641
- const tx = ethers.Transaction.from(signedHandleOps);
642
- const tailTx = await this.bundleExecutor['signProfitTransaction']({
643
- payerWallet,
644
- profitWei,
645
- recipient: profitRecipient,
646
- nonce: Number(tx.nonce) + 1,
647
- });
648
- signedTransactions.push(tailTx);
649
- }
650
741
  return {
651
742
  signedTransactions,
652
743
  metadata: {
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { Wallet, Contract, Interface, ethers } from 'ethers';
10
10
  import { ENTRYPOINT_ABI, DEFAULT_CALL_GAS_LIMIT_SELL, DEFAULT_WITHDRAW_RESERVE, POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, WOKB, } from './constants.js';
11
- import { AAAccountManager, encodeExecute } from './aa-account.js';
11
+ import { AAAccountManager, encodeExecute, encodeExecuteBatch } from './aa-account.js';
12
12
  import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, } from './dex.js';
13
13
  import { PortalQuery, encodeApproveCall, parseOkb, formatOkb, lpFeeProfileToV3Fee } from './portal-ops.js';
14
14
  import { mapWithConcurrency } from '../utils/concurrency.js';
@@ -153,7 +153,10 @@ export class DexBundleExecutor {
153
153
  const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effConfig);
154
154
  const profitWei = extractProfit ? calculateProfitWei(withdrawAmount, profitBps) : 0n;
155
155
  const toOwnerWei = withdrawAmount - profitWei;
156
- const callData = encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
156
+ // AA 内部刮取利润:使用 executeBatch 同时转账给利润接收者和 owner
157
+ const callData = extractProfit && profitWei > 0n
158
+ ? encodeExecuteBatch([profitRecipient, params.ownerWallet.address], [profitWei, toOwnerWei], ['0x', '0x'])
159
+ : encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
157
160
  const { userOp } = gasPolicy === 'fixed'
158
161
  ? await this.aaManager.buildUserOpWithFixedGas({
159
162
  ownerWallet: params.ownerWallet,
@@ -380,23 +383,7 @@ export class DexBundleExecutor {
380
383
  if (withdrawOps.length > 0) {
381
384
  withdrawResult = await this.runHandleOps('dex-withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
382
385
  }
383
- if (profitSettings.extractProfit && totalProfitWei > 0n) {
384
- const provider = this.aaManager.getProvider();
385
- const feeData = await provider.getFeeData();
386
- const nonce = await provider.getTransactionCount(bundlerSigner.address, 'pending');
387
- const tailTx = await bundlerSigner.signTransaction({
388
- to: profitSettings.profitRecipient,
389
- value: totalProfitWei,
390
- data: '0x',
391
- nonce,
392
- gasLimit: this.config.profitTailGasLimit ?? 21000n,
393
- gasPrice: feeData.gasPrice ?? 100000000n,
394
- chainId: this.config.chainId ?? 196,
395
- type: 0,
396
- });
397
- const tx = await provider.broadcastTransaction(tailTx);
398
- await tx.wait();
399
- }
386
+ // 利润已在 withdraw UserOp 内部通过 executeBatch 刮取,无需单独广播 tail tx
400
387
  }
401
388
  const finalBalances = await this.portalQuery.getMultipleOkbBalances(senders);
402
389
  return {