four-flap-meme-sdk 1.5.31 → 1.5.33

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.
@@ -9,9 +9,10 @@
9
9
  */
10
10
  import { Wallet, Interface, Contract } from 'ethers';
11
11
  import { FLAP_PORTAL, ENTRYPOINT_ABI, DEFAULT_CALL_GAS_LIMIT_SELL, DEFAULT_WITHDRAW_RESERVE, } from './constants.js';
12
- import { AAAccountManager, encodeExecute } from './aa-account.js';
12
+ import { AAAccountManager, encodeExecute, encodeExecuteBatch } from './aa-account.js';
13
13
  import { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, PortalQuery, parseOkb, formatOkb, } from './portal-ops.js';
14
14
  import { mapWithConcurrency } from '../utils/concurrency.js';
15
+ import { PROFIT_CONFIG } from '../utils/constants.js';
15
16
  // ============================================================================
16
17
  // AA Nonce(EntryPoint nonce)本地分配器
17
18
  // ============================================================================
@@ -72,6 +73,21 @@ const DEFAULT_CALL_GAS_LIMIT_BUY = DEFAULT_CALL_GAS_LIMIT_SELL; // buy 与 sell
72
73
  const DEFAULT_CALL_GAS_LIMIT_APPROVE = 200000n;
73
74
  const DEFAULT_CALL_GAS_LIMIT_TRANSFER = 150000n;
74
75
  const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
76
+ function resolveProfitSettings(config) {
77
+ const extractProfit = config?.extractProfit !== false; // 默认 true
78
+ // ✅ 对齐 bundle/刷量模式默认费率(与 BSC bundle-buy-first 语义一致)
79
+ const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
80
+ const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
81
+ const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
82
+ return { extractProfit, profitBps, profitRecipient };
83
+ }
84
+ function calculateProfitWei(amountWei, profitBps) {
85
+ if (amountWei <= 0n)
86
+ return 0n;
87
+ if (!Number.isFinite(profitBps) || profitBps <= 0)
88
+ return 0n;
89
+ return (amountWei * BigInt(profitBps)) / 10000n;
90
+ }
75
91
  /**
76
92
  * XLayer 捆绑交易执行器
77
93
  *
@@ -418,7 +434,14 @@ export class BundleExecutor {
418
434
  console.log(`\n[${params.ownerName ?? 'owner'}] 归集后可转出=0(余额不足以覆盖 prefund+reserve)`);
419
435
  return null;
420
436
  }
421
- const callData = encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
437
+ // 利润提取:从“归集金额”里按 bps 刮取一部分转到 profitRecipient
438
+ const effConfig = { ...(this.config ?? {}), ...(params.configOverride ?? {}) };
439
+ const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effConfig);
440
+ const profitWei = extractProfit ? calculateProfitWei(withdrawAmount, profitBps) : 0n;
441
+ const toOwnerWei = withdrawAmount - profitWei;
442
+ const callData = extractProfit && profitWei > 0n
443
+ ? encodeExecuteBatch([profitRecipient, params.ownerWallet.address], [profitWei, toOwnerWei], ['0x', '0x'])
444
+ : encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
422
445
  const { userOp } = gasPolicy === 'fixed'
423
446
  ? await this.aaManager.buildUserOpWithFixedGas({
424
447
  ownerWallet: params.ownerWallet,
@@ -447,7 +470,12 @@ export class BundleExecutor {
447
470
  nonce: params.nonce,
448
471
  initCode: params.initCode,
449
472
  });
450
- console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
473
+ if (extractProfit && profitWei > 0n) {
474
+ console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(toOwnerWei)} OKB (profit: ${formatOkb(profitWei)} OKB -> ${profitRecipient})`);
475
+ }
476
+ else {
477
+ console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
478
+ }
451
479
  const signed = await this.aaManager.signUserOp(userOp, params.ownerWallet);
452
480
  return { ...signed, prefundWei, ownerName: params.ownerName };
453
481
  }
@@ -596,7 +624,7 @@ export class BundleExecutor {
596
624
  * 多个地址同一笔 handleOps 卖出代币
597
625
  */
598
626
  async bundleSell(params) {
599
- const { tokenAddress, privateKeys, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
627
+ const { tokenAddress, privateKeys, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, } = params;
600
628
  const sharedProvider = this.aaManager.getProvider();
601
629
  const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
602
630
  const bundlerSigner = wallets[0];
@@ -679,6 +707,9 @@ export class BundleExecutor {
679
707
  }
680
708
  // 3. 可选:归集 OKB
681
709
  let withdrawResult;
710
+ let totalProfitWei = 0n;
711
+ const effProfitCfg = { ...(this.config ?? {}), ...(config ?? {}) };
712
+ const profitSettings = resolveProfitSettings(effProfitCfg);
682
713
  if (withdrawToOwner) {
683
714
  const withdrawOps = [];
684
715
  // 批量获取 sender OKB 余额
@@ -702,7 +733,14 @@ export class BundleExecutor {
702
733
  senderBalance: it.senderBalance,
703
734
  reserveWei,
704
735
  ownerName: `owner${it.i + 1}`,
736
+ configOverride: config,
705
737
  });
738
+ if (signed && profitSettings.extractProfit) {
739
+ const withdrawAmount = it.senderBalance > signed.prefundWei + reserveWei
740
+ ? it.senderBalance - signed.prefundWei - reserveWei
741
+ : 0n;
742
+ totalProfitWei += calculateProfitWei(withdrawAmount, profitSettings.profitBps);
743
+ }
706
744
  if (signed?.userOp)
707
745
  nonceMap.commit(it.sender, it.nonce);
708
746
  return signed?.userOp ?? null;
@@ -715,7 +753,19 @@ export class BundleExecutor {
715
753
  withdrawResult = await this.runHandleOps('withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
716
754
  }
717
755
  }
718
- return { approveResult, sellResult, withdrawResult };
756
+ return {
757
+ approveResult,
758
+ sellResult,
759
+ withdrawResult,
760
+ profit: withdrawToOwner
761
+ ? {
762
+ extractProfit: profitSettings.extractProfit,
763
+ profitBps: profitSettings.profitBps,
764
+ profitRecipient: profitSettings.profitRecipient,
765
+ totalProfitWei: totalProfitWei.toString(),
766
+ }
767
+ : undefined,
768
+ };
719
769
  }
720
770
  /**
721
771
  * 捆绑买卖(先买后卖)
@@ -723,7 +773,7 @@ export class BundleExecutor {
723
773
  * 完整流程:买入 -> 授权 -> 卖出 -> 归集
724
774
  */
725
775
  async bundleBuySell(params) {
726
- const { tokenAddress, privateKeys, buyAmounts, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, } = params;
776
+ const { tokenAddress, privateKeys, buyAmounts, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, } = params;
727
777
  if (privateKeys.length !== buyAmounts.length) {
728
778
  throw new Error('私钥数量和购买金额数量必须一致');
729
779
  }
@@ -810,6 +860,9 @@ export class BundleExecutor {
810
860
  }
811
861
  // 3. 可选:归集 OKB
812
862
  let withdrawResult;
863
+ let totalProfitWei = 0n;
864
+ const effProfitCfg = { ...(this.config ?? {}), ...(config ?? {}) };
865
+ const profitSettings = resolveProfitSettings(effProfitCfg);
813
866
  if (withdrawToOwner) {
814
867
  const withdrawOps = [];
815
868
  // 批量获取 OKB 余额(sell 后状态)
@@ -831,7 +884,14 @@ export class BundleExecutor {
831
884
  senderBalance: it.senderBalance,
832
885
  reserveWei,
833
886
  ownerName: `owner${it.i + 1}`,
887
+ configOverride: config,
834
888
  });
889
+ if (signed && profitSettings.extractProfit) {
890
+ const withdrawAmount = it.senderBalance > signed.prefundWei + reserveWei
891
+ ? it.senderBalance - signed.prefundWei - reserveWei
892
+ : 0n;
893
+ totalProfitWei += calculateProfitWei(withdrawAmount, profitSettings.profitBps);
894
+ }
835
895
  if (signed?.userOp)
836
896
  nonceMap.commit(it.sender, it.nonce);
837
897
  return signed?.userOp ?? null;
@@ -846,7 +906,20 @@ export class BundleExecutor {
846
906
  }
847
907
  // 最终余额
848
908
  const finalBalances = await this.portalQuery.getMultipleOkbBalances(senders);
849
- return { buyResult, sellResult, withdrawResult, finalBalances };
909
+ return {
910
+ buyResult,
911
+ sellResult,
912
+ withdrawResult,
913
+ finalBalances,
914
+ profit: withdrawToOwner
915
+ ? {
916
+ extractProfit: profitSettings.extractProfit,
917
+ profitBps: profitSettings.profitBps,
918
+ profitRecipient: profitSettings.profitRecipient,
919
+ totalProfitWei: totalProfitWei.toString(),
920
+ }
921
+ : undefined,
922
+ };
850
923
  }
851
924
  }
852
925
  // ============================================================================
@@ -3,11 +3,48 @@
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
+ 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
+ }
33
+ function resolveProfitSettings(config) {
34
+ const extractProfit = config?.extractProfit !== false; // 默认 true(对齐 BSC)
35
+ // ✅ 对齐 BSC “捆绑换手模式”默认费率
36
+ const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
37
+ const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
38
+ const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
39
+ return { extractProfit, profitBps, profitRecipient };
40
+ }
41
+ function calculateProfitWei(amountWei, profitBps) {
42
+ if (amountWei <= 0n)
43
+ return 0n;
44
+ if (!Number.isFinite(profitBps) || profitBps <= 0)
45
+ return 0n;
46
+ return (amountWei * BigInt(profitBps)) / 10000n;
47
+ }
11
48
  function getDexDeadline(minutes = 20) {
12
49
  return Math.floor(Date.now() / 1000) + minutes * 60;
13
50
  }
@@ -39,7 +76,9 @@ export class AADexSwapExecutor {
39
76
  * AA 外盘单钱包换手签名
40
77
  */
41
78
  async bundleSwapSign(params) {
42
- 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;
80
+ const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
81
+ const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
43
82
  const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn });
44
83
  const provider = this.aaManager.getProvider();
45
84
  const sellerOwner = new Wallet(sellerPrivateKey, provider);
@@ -84,15 +123,27 @@ export class AADexSwapExecutor {
84
123
  const allowance = await this.aaManager.getErc20Allowance(tokenAddress, sellerSender, effectiveRouter);
85
124
  needApprove = allowance < sellAmountWei;
86
125
  }
87
- // 报价
88
- let finalBuyAmountWei;
89
- if (buyAmountOkb) {
90
- finalBuyAmountWei = ethers.parseEther(String(buyAmountOkb));
91
- }
92
- else {
93
- const quoted = await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
94
- finalBuyAmountWei = (quoted * BigInt(10000 - slippageBps)) / 10000n;
126
+ // 估算卖出输出(用于利润提取与 buy 预算)
127
+ const quotedSellOutWei = await (async () => {
128
+ try {
129
+ return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
130
+ }
131
+ catch {
132
+ return 0n;
133
+ }
134
+ })();
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)');
95
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)));
96
147
  const outOps = [];
97
148
  if (needApprove) {
98
149
  const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
@@ -118,6 +169,32 @@ export class AADexSwapExecutor {
118
169
  signOnly: true,
119
170
  });
120
171
  outOps.push(signedSell.userOp);
172
+ // Profit op(紧跟 sell 之后)
173
+ if (extractProfit && profitWei > 0n) {
174
+ const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
175
+ const signedProfit = await this.aaManager.buildUserOpWithState({
176
+ ownerWallet: sellerOwner,
177
+ sender: sellerSender,
178
+ nonce: nonceMap.next(sellerSender),
179
+ initCode: consumeInitCode(sellerSender),
180
+ callData: profitCallData,
181
+ signOnly: true,
182
+ });
183
+ outOps.push(signedProfit.userOp);
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
+ }
121
198
  // Buy op
122
199
  const buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], buyerSender, getDexDeadline());
123
200
  const buyCallData = encodeExecute(effectiveRouter, finalBuyAmountWei, buySwapData);
@@ -163,6 +240,12 @@ export class AADexSwapExecutor {
163
240
  buyAmountWei: finalBuyAmountWei.toString(),
164
241
  hasApprove: needApprove,
165
242
  routeAddress,
243
+ extractProfit,
244
+ profitBps,
245
+ profitRecipient,
246
+ profitWei: profitWei.toString(),
247
+ quotedSellOutWei: quotedSellOutWei.toString(),
248
+ disperseHopCount: String(hopCount),
166
249
  },
167
250
  };
168
251
  }
@@ -170,7 +253,9 @@ export class AADexSwapExecutor {
170
253
  * AA 外盘批量换手签名
171
254
  */
172
255
  async bundleBatchSwapSign(params) {
173
- 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;
257
+ const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
258
+ const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
174
259
  const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress: routerAddressIn });
175
260
  const provider = this.aaManager.getProvider();
176
261
  const sellerOwner = new Wallet(sellerPrivateKey, provider);
@@ -239,8 +324,122 @@ export class AADexSwapExecutor {
239
324
  signOnly: true,
240
325
  });
241
326
  outOps.push(signedSell.userOp);
242
- // Batch Buy ops
327
+ // 先计算 buyAmountsWei(用于后续分发与校验)
243
328
  const buyAmountsWei = buyAmountsOkb.map(a => ethers.parseEther(a));
329
+ const totalBuyWei = buyAmountsWei.reduce((a, b) => a + (b ?? 0n), 0n);
330
+ // Profit op:估算卖出输出,按比例刮取(但必须保证分发/买入资金充足)
331
+ const quotedSellOutWei = await (async () => {
332
+ try {
333
+ return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
334
+ }
335
+ catch {
336
+ return 0n;
337
+ }
338
+ })();
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;
345
+ if (extractProfit && profitWei > 0n) {
346
+ const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
347
+ const signedProfit = await this.aaManager.buildUserOpWithState({
348
+ ownerWallet: sellerOwner,
349
+ sender: sellerAi.sender,
350
+ nonce: nonceMap.next(sellerAi.sender),
351
+ initCode: consumeInitCode(sellerAi.sender),
352
+ callData: profitCallData,
353
+ signOnly: true,
354
+ });
355
+ outOps.push(signedProfit.userOp);
356
+ }
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(分发后再执行)
244
443
  for (let i = 0; i < buyerOwners.length; i++) {
245
444
  const ai = buyerAis[i];
246
445
  const buyWei = buyAmountsWei[i];
@@ -289,6 +488,12 @@ export class AADexSwapExecutor {
289
488
  buyAmountsWei: buyAmountsWei.map(w => w.toString()),
290
489
  hasApprove: needApprove,
291
490
  routeAddress,
491
+ extractProfit,
492
+ profitBps,
493
+ profitRecipient,
494
+ profitWei: profitWei.toString(),
495
+ quotedSellOutWei: quotedSellOutWei.toString(),
496
+ disperseHopCount: String(hopCount),
292
497
  },
293
498
  };
294
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>;