four-flap-meme-sdk 1.5.32 → 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.
@@ -75,7 +75,8 @@ const DEFAULT_CALL_GAS_LIMIT_TRANSFER = 150000n;
75
75
  const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
76
76
  function resolveProfitSettings(config) {
77
77
  const extractProfit = config?.extractProfit !== false; // 默认 true
78
- const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS;
78
+ // 对齐 bundle/刷量模式默认费率(与 BSC bundle-buy-first 语义一致)
79
+ const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
79
80
  const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
80
81
  const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
81
82
  return { extractProfit, profitBps, profitRecipient };
@@ -706,6 +707,9 @@ export class BundleExecutor {
706
707
  }
707
708
  // 3. 可选:归集 OKB
708
709
  let withdrawResult;
710
+ let totalProfitWei = 0n;
711
+ const effProfitCfg = { ...(this.config ?? {}), ...(config ?? {}) };
712
+ const profitSettings = resolveProfitSettings(effProfitCfg);
709
713
  if (withdrawToOwner) {
710
714
  const withdrawOps = [];
711
715
  // 批量获取 sender OKB 余额
@@ -731,6 +735,12 @@ export class BundleExecutor {
731
735
  ownerName: `owner${it.i + 1}`,
732
736
  configOverride: config,
733
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
+ }
734
744
  if (signed?.userOp)
735
745
  nonceMap.commit(it.sender, it.nonce);
736
746
  return signed?.userOp ?? null;
@@ -743,7 +753,19 @@ export class BundleExecutor {
743
753
  withdrawResult = await this.runHandleOps('withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
744
754
  }
745
755
  }
746
- 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
+ };
747
769
  }
748
770
  /**
749
771
  * 捆绑买卖(先买后卖)
@@ -838,6 +860,9 @@ export class BundleExecutor {
838
860
  }
839
861
  // 3. 可选:归集 OKB
840
862
  let withdrawResult;
863
+ let totalProfitWei = 0n;
864
+ const effProfitCfg = { ...(this.config ?? {}), ...(config ?? {}) };
865
+ const profitSettings = resolveProfitSettings(effProfitCfg);
841
866
  if (withdrawToOwner) {
842
867
  const withdrawOps = [];
843
868
  // 批量获取 OKB 余额(sell 后状态)
@@ -861,6 +886,12 @@ export class BundleExecutor {
861
886
  ownerName: `owner${it.i + 1}`,
862
887
  configOverride: config,
863
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
+ }
864
895
  if (signed?.userOp)
865
896
  nonceMap.commit(it.sender, it.nonce);
866
897
  return signed?.userOp ?? null;
@@ -875,7 +906,20 @@ export class BundleExecutor {
875
906
  }
876
907
  // 最终余额
877
908
  const finalBalances = await this.portalQuery.getMultipleOkbBalances(senders);
878
- 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
+ };
879
923
  }
880
924
  }
881
925
  // ============================================================================
@@ -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
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * XLayer 外盘刷量/做市 SDK(AA / ERC-4337)
3
+ *
4
+ * - 与 `src/xlayer/volume.ts`(内盘)分离
5
+ * - 底层使用 `DexBundleExecutor.bundleBuySell`(买入 -> 卖出 -> 归集 -> 刮利润)
6
+ */
7
+ import type { XLayerConfig, VolumeResult } from './types.js';
8
+ export interface DexVolumeParams {
9
+ tokenAddress: string;
10
+ privateKeys: string[];
11
+ buyAmountPerRound: string;
12
+ rounds: number;
13
+ intervalMs?: number;
14
+ sellImmediately?: boolean;
15
+ sellPercent?: number;
16
+ withdrawToOwner?: boolean;
17
+ withdrawReserve?: string;
18
+ dexKey?: string;
19
+ routerAddress?: string;
20
+ deadlineMinutes?: number;
21
+ config?: Partial<XLayerConfig>;
22
+ }
23
+ export declare class DexVolumeExecutor {
24
+ private dexBundleExecutor;
25
+ private config;
26
+ constructor(config?: XLayerConfig);
27
+ execute(params: DexVolumeParams): Promise<VolumeResult>;
28
+ }
29
+ export declare function createDexVolumeExecutor(config?: XLayerConfig): DexVolumeExecutor;
30
+ export declare function makeDexVolume(params: DexVolumeParams): Promise<VolumeResult>;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * XLayer 外盘刷量/做市 SDK(AA / ERC-4337)
3
+ *
4
+ * - 与 `src/xlayer/volume.ts`(内盘)分离
5
+ * - 底层使用 `DexBundleExecutor.bundleBuySell`(买入 -> 卖出 -> 归集 -> 刮利润)
6
+ */
7
+ import { parseOkb, formatOkb } from './portal-ops.js';
8
+ import { DexBundleExecutor } from './dex-bundle.js';
9
+ function sleep(ms) {
10
+ return new Promise((resolve) => setTimeout(resolve, ms));
11
+ }
12
+ export class DexVolumeExecutor {
13
+ constructor(config = {}) {
14
+ this.config = config;
15
+ this.dexBundleExecutor = new DexBundleExecutor(config);
16
+ }
17
+ async execute(params) {
18
+ const { tokenAddress, privateKeys, buyAmountPerRound, rounds, intervalMs = 3000, sellImmediately = true, sellPercent, withdrawToOwner = true, withdrawReserve, dexKey, routerAddress, deadlineMinutes, config, } = params;
19
+ console.log('=== XLayer DEX Volume Maker ===');
20
+ console.log('token:', tokenAddress);
21
+ console.log('owners:', privateKeys.length);
22
+ console.log('rounds:', rounds);
23
+ console.log('buyAmountPerRound:', buyAmountPerRound, 'OKB');
24
+ console.log('intervalMs:', intervalMs);
25
+ console.log('sellImmediately:', sellImmediately);
26
+ const roundResults = [];
27
+ let successRounds = 0;
28
+ let failedRounds = 0;
29
+ let totalVolume = 0n;
30
+ const buyWei = parseOkb(buyAmountPerRound);
31
+ for (let round = 0; round < rounds; round++) {
32
+ console.log(`\n========== DEX Round ${round + 1}/${rounds} ==========`);
33
+ try {
34
+ const buyAmounts = privateKeys.map(() => buyAmountPerRound);
35
+ const effSellPercent = typeof sellPercent === 'number' ? sellPercent : (sellImmediately ? 100 : 0);
36
+ const result = await this.dexBundleExecutor.bundleBuySell({
37
+ tokenAddress,
38
+ privateKeys,
39
+ buyAmounts,
40
+ sellPercent: effSellPercent,
41
+ withdrawToOwner,
42
+ withdrawReserve,
43
+ dexKey,
44
+ routerAddress,
45
+ deadlineMinutes,
46
+ config: { ...(this.config ?? {}), ...(config ?? {}) },
47
+ });
48
+ roundResults.push(result);
49
+ successRounds++;
50
+ totalVolume += buyWei * BigInt(privateKeys.length);
51
+ console.log(`DEX Round ${round + 1} 完成`);
52
+ }
53
+ catch (error) {
54
+ console.error(`DEX Round ${round + 1} 失败:`, error);
55
+ failedRounds++;
56
+ }
57
+ if (round < rounds - 1 && intervalMs > 0) {
58
+ console.log(`等待 ${intervalMs}ms...`);
59
+ await sleep(intervalMs);
60
+ }
61
+ }
62
+ console.log('\n=== DEX Volume Summary ===');
63
+ console.log('成功轮数:', successRounds);
64
+ console.log('失败轮数:', failedRounds);
65
+ console.log('总交易量:', formatOkb(totalVolume), 'OKB');
66
+ return {
67
+ successRounds,
68
+ failedRounds,
69
+ roundResults,
70
+ totalVolume,
71
+ };
72
+ }
73
+ }
74
+ export function createDexVolumeExecutor(config) {
75
+ return new DexVolumeExecutor(config);
76
+ }
77
+ export async function makeDexVolume(params) {
78
+ const executor = createDexVolumeExecutor(params.config);
79
+ return executor.execute(params);
80
+ }
@@ -63,6 +63,7 @@ export { BundlerClient, createBundlerClient, type BundlerConfig, type BundlerRec
63
63
  export { AAAccountManager, createAAAccountManager, predictSender, createWallet, encodeExecute, encodeExecuteBatch, generateAAWallets, generateAAWalletsFromMnemonic, predictSendersFromPrivateKeys, type GeneratedAAWallet, type GenerateAAWalletsParams, type GenerateAAWalletsResult, } from './aa-account.js';
64
64
  export { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, PortalQuery, createPortalQuery, applySlippage, formatOkb, parseOkb, formatTokenAmount, parseTokenAmount, type PortalQueryConfig, } from './portal-ops.js';
65
65
  export { BundleExecutor, createBundleExecutor, bundleBuy, bundleSell, bundleBuySell, } from './bundle.js';
66
+ export { DexBundleExecutor, createDexBundleExecutor, dexBundleBuySell, type DexBundleConfig, type DexBundleBuySellParams, } from './dex-bundle.js';
66
67
  export { AAPortalSwapExecutor, } from './portal-bundle-swap.js';
67
68
  export { AADexSwapExecutor, } from './dex-bundle-swap.js';
68
69
  /**
@@ -78,6 +79,7 @@ export declare function bundleBatchSwapSign(params: BundleBatchSwapSignParams &
78
79
  skipApprovalCheck?: boolean;
79
80
  }): Promise<BundleBatchSwapSignResult>;
80
81
  export { VolumeExecutor, createVolumeExecutor, makeVolume, singleRoundVolume, } from './volume.js';
82
+ export { DexVolumeExecutor, createDexVolumeExecutor, makeDexVolume, type DexVolumeParams, } from './dex-volume.js';
81
83
  export { DexQuery, DexExecutor, createDexQuery, createDexExecutor, quoteOkbToToken, quoteTokenToOkb, encodeSwapExactETHForTokens, encodeSwapExactTokensForETH, encodeSwapExactTokensForTokens, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactTokensForTokensSupportingFee, type DexConfig, } from './dex.js';
82
84
  export declare const xlayer: {
83
85
  bundleBuy: (params: import("./types.js").BundleBuyParams) => Promise<import("./types.js").BundleBuyResult>;
@@ -83,6 +83,10 @@ export { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, P
83
83
  // ============================================================================
84
84
  export { BundleExecutor, createBundleExecutor, bundleBuy, bundleSell, bundleBuySell, } from './bundle.js';
85
85
  // ============================================================================
86
+ // 外盘捆绑交易(DEX Bundle)
87
+ // ============================================================================
88
+ export { DexBundleExecutor, createDexBundleExecutor, dexBundleBuySell, } from './dex-bundle.js';
89
+ // ============================================================================
86
90
  // 捆绑换手 (AASwap)
87
91
  // ============================================================================
88
92
  export { AAPortalSwapExecutor, } from './portal-bundle-swap.js';
@@ -116,6 +120,10 @@ export async function bundleBatchSwapSign(params) {
116
120
  // ============================================================================
117
121
  export { VolumeExecutor, createVolumeExecutor, makeVolume, singleRoundVolume, } from './volume.js';
118
122
  // ============================================================================
123
+ // 外盘刷量/做市(DEX Volume)
124
+ // ============================================================================
125
+ export { DexVolumeExecutor, createDexVolumeExecutor, makeDexVolume, } from './dex-volume.js';
126
+ // ============================================================================
119
127
  // 外盘交易 (DEX)
120
128
  // ============================================================================
121
129
  export { DexQuery, DexExecutor, createDexQuery, createDexExecutor, quoteOkbToToken, quoteTokenToOkb,
@@ -3,14 +3,36 @@
3
3
  */
4
4
  import { Wallet, ethers } from 'ethers';
5
5
  import { AANonceMap, } from './types.js';
6
- import { FLAP_PORTAL, ZERO_ADDRESS, } from './constants.js';
6
+ import { FLAP_PORTAL, ZERO_ADDRESS, MULTICALL3, } from './constants.js';
7
7
  import { AAAccountManager, encodeExecute } from './aa-account.js';
8
8
  import { encodeBuyCall, encodeSellCall, encodeApproveCall, PortalQuery, parseOkb, } from './portal-ops.js';
9
9
  import { BundleExecutor } from './bundle.js';
10
10
  import { PROFIT_CONFIG } from '../utils/constants.js';
11
+ const multicallIface = new ethers.Interface([
12
+ 'function aggregate3Value((address target,bool allowFailure,uint256 value,bytes callData)[] calls) payable returns ((bool success,bytes returnData)[] returnData)',
13
+ ]);
14
+ function chunkArray(arr, size) {
15
+ const out = [];
16
+ const n = Math.max(1, Math.floor(size));
17
+ for (let i = 0; i < arr.length; i += n)
18
+ out.push(arr.slice(i, i + n));
19
+ return out;
20
+ }
21
+ function encodeNativeDisperseViaMulticall3(params) {
22
+ const calls = params.to.map((target, idx) => ({
23
+ target,
24
+ allowFailure: false,
25
+ value: params.values[idx] ?? 0n,
26
+ callData: '0x',
27
+ }));
28
+ const totalValue = params.values.reduce((a, b) => a + (b ?? 0n), 0n);
29
+ const data = multicallIface.encodeFunctionData('aggregate3Value', [calls]);
30
+ return { totalValue, data };
31
+ }
11
32
  function resolveProfitSettings(config) {
12
33
  const extractProfit = config?.extractProfit !== false; // 默认 true(对齐 BSC)
13
- const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS;
34
+ // 对齐 BSC “捆绑换手模式”默认费率
35
+ const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
14
36
  const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
15
37
  const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
16
38
  return { extractProfit, profitBps, profitRecipient };
@@ -39,7 +61,7 @@ export class AAPortalSwapExecutor {
39
61
  * AA 内盘单钱包换手签名
40
62
  */
41
63
  async bundleSwapSign(params) {
42
- const { tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
64
+ const { tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
43
65
  const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
44
66
  const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
45
67
  const provider = this.aaManager.getProvider();
@@ -100,11 +122,18 @@ export class AAPortalSwapExecutor {
100
122
  }
101
123
  })();
102
124
  const quotedSellOutWei = quoted;
103
- const finalBuyAmountWei = buyAmountOkb
125
+ // 3.1 利润提取(从卖出输出 OKB 里按比例刮取;但必须保证买入资金充足)
126
+ const requestedBuyWei = buyAmountOkb
104
127
  ? parseOkb(String(buyAmountOkb))
105
- : (quoted * BigInt(10000 - slippageBps)) / 10000n;
106
- // 3.1 利润提取(从卖出输出 OKB 里按比例刮取)
107
- const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
128
+ : (quotedSellOutWei * BigInt(10000 - slippageBps)) / 10000n;
129
+ if (requestedBuyWei > quotedSellOutWei) {
130
+ throw new Error('AA 捆绑换手:buyAmountOkb 超过预估卖出输出(quotedSellOut)');
131
+ }
132
+ const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
133
+ const profitCap = quotedSellOutWei - requestedBuyWei;
134
+ const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
135
+ const finalBuyAmountWei = requestedBuyWei;
136
+ const hopCount = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
108
137
  // 4. 构建 Ops
109
138
  const outOps = [];
110
139
  if (needApprove) {
@@ -144,6 +173,19 @@ export class AAPortalSwapExecutor {
144
173
  });
145
174
  outOps.push(signedProfit.userOp);
146
175
  }
176
+ // ✅ 卖出所得 OKB 转给买方 AA(Sender),否则买方 UserOp 无法携带 value 执行 buy
177
+ if (sellerSender.toLowerCase() !== buyerSender.toLowerCase() && finalBuyAmountWei > 0n) {
178
+ const transferCallData = encodeExecute(buyerSender, finalBuyAmountWei, '0x');
179
+ const signedTransfer = await this.aaManager.buildUserOpWithState({
180
+ ownerWallet: sellerOwner,
181
+ sender: sellerSender,
182
+ nonce: nonceMap.next(sellerSender),
183
+ initCode: consumeInitCode(sellerSender),
184
+ callData: transferCallData,
185
+ signOnly: true,
186
+ });
187
+ outOps.push(signedTransfer.userOp);
188
+ }
147
189
  // Buy op
148
190
  const buySwapData = encodeBuyCall(tokenAddress, finalBuyAmountWei, 0n);
149
191
  const buyCallData = encodeExecute(FLAP_PORTAL, finalBuyAmountWei, buySwapData);
@@ -195,6 +237,7 @@ export class AAPortalSwapExecutor {
195
237
  profitRecipient,
196
238
  profitWei: profitWei.toString(),
197
239
  quotedSellOutWei: quotedSellOutWei.toString(),
240
+ disperseHopCount: String(hopCount),
198
241
  },
199
242
  };
200
243
  }
@@ -202,7 +245,7 @@ export class AAPortalSwapExecutor {
202
245
  * AA 内盘批量换手签名 (一卖多买)
203
246
  */
204
247
  async bundleBatchSwapSign(params) {
205
- const { tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
248
+ const { tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
206
249
  const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
207
250
  const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
208
251
  const provider = this.aaManager.getProvider();
@@ -272,7 +315,10 @@ export class AAPortalSwapExecutor {
272
315
  signOnly: true,
273
316
  });
274
317
  outOps.push(signedSell.userOp);
275
- // Profit op:用 previewSell/quoteExactInput 估算卖出输出,再按比例刮取
318
+ // 先计算 buyAmountsWei(用于后续分发与校验)
319
+ const buyAmountsWei = buyAmountsOkb.map(a => parseOkb(a));
320
+ const totalBuyWei = buyAmountsWei.reduce((a, b) => a + (b ?? 0n), 0n);
321
+ // Profit op:用 previewSell/quoteExactInput 估算卖出输出,再按比例刮取(但必须保证分发/买入资金充足)
276
322
  let quotedSellOutWei = 0n;
277
323
  try {
278
324
  quotedSellOutWei = await this.portalQuery.previewSell(tokenAddress, sellAmountWei);
@@ -280,7 +326,12 @@ export class AAPortalSwapExecutor {
280
326
  catch {
281
327
  quotedSellOutWei = await this.portalQuery.quoteExactInput(tokenAddress, ZERO_ADDRESS, sellAmountWei);
282
328
  }
283
- const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
329
+ if (totalBuyWei > quotedSellOutWei) {
330
+ throw new Error('AA 批量换手:buyAmountsOkb 总和超过预估卖出输出(quotedSellOut)');
331
+ }
332
+ const profitWeiRaw = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
333
+ const profitCap = quotedSellOutWei - totalBuyWei;
334
+ const profitWei = profitWeiRaw > profitCap ? profitCap : profitWeiRaw;
284
335
  if (extractProfit && profitWei > 0n) {
285
336
  const profitCallData = encodeExecute(profitRecipient, profitWei, '0x');
286
337
  const signedProfit = await this.aaManager.buildUserOpWithState({
@@ -293,8 +344,97 @@ export class AAPortalSwapExecutor {
293
344
  });
294
345
  outOps.push(signedProfit.userOp);
295
346
  }
296
- // Batch Buy ops
297
- const buyAmountsWei = buyAmountsOkb.map(a => parseOkb(a));
347
+ // 卖出所得 OKB 分发给多个买方 AA(Sender)(支持多跳)
348
+ const buyerSenders = buyerAis.map(ai => ai.sender);
349
+ const hopCountRaw = Math.max(0, Math.floor(Number(disperseHopCountIn ?? 0)));
350
+ const hopCount = Math.min(hopCountRaw, buyerSenders.length); // 允许等于 buyerCount(最后一跳不再分发)
351
+ const maxPerOp = Math.max(1, Math.floor(Number(effectiveConfig.maxTransfersPerUserOpNative ?? 30)));
352
+ if (buyerSenders.length > 0 && totalBuyWei > 0n) {
353
+ if (hopCount <= 0) {
354
+ // 0 跳:卖方 AA(Sender) 直接用 multicall3 分发给所有买方 AA(Sender)
355
+ const items = buyerSenders.map((to, i) => ({ to, value: buyAmountsWei[i] ?? 0n })).filter(x => x.value > 0n);
356
+ const chunks = chunkArray(items, maxPerOp);
357
+ for (const ch of chunks) {
358
+ const { totalValue, data } = encodeNativeDisperseViaMulticall3({
359
+ to: ch.map(x => x.to),
360
+ values: ch.map(x => x.value),
361
+ });
362
+ const callData = encodeExecute(MULTICALL3, totalValue, data);
363
+ const signedDisperse = await this.aaManager.buildUserOpWithState({
364
+ ownerWallet: sellerOwner,
365
+ sender: sellerAi.sender,
366
+ nonce: nonceMap.next(sellerAi.sender),
367
+ initCode: consumeInitCode(sellerAi.sender),
368
+ callData,
369
+ signOnly: true,
370
+ });
371
+ outOps.push(signedDisperse.userOp);
372
+ }
373
+ }
374
+ else {
375
+ // 多跳:使用前 hopCount 个买方作为中转链(与 BSC 多跳语义独立,仅作用于 xlayer AA)
376
+ const hopSenders = buyerSenders.slice(0, hopCount);
377
+ const hopOwners = buyerOwners.slice(0, hopCount);
378
+ // 1) seller -> hop0(总额)
379
+ const hop0 = hopSenders[0];
380
+ const callData0 = encodeExecute(hop0, totalBuyWei, '0x');
381
+ const signedToHop0 = await this.aaManager.buildUserOpWithState({
382
+ ownerWallet: sellerOwner,
383
+ sender: sellerAi.sender,
384
+ nonce: nonceMap.next(sellerAi.sender),
385
+ initCode: consumeInitCode(sellerAi.sender),
386
+ callData: callData0,
387
+ signOnly: true,
388
+ });
389
+ outOps.push(signedToHop0.userOp);
390
+ // 2) hop j -> hop j+1(扣掉自己那份)
391
+ let prefixKept = 0n;
392
+ for (let j = 0; j < hopSenders.length - 1; j++) {
393
+ const sender = hopSenders[j];
394
+ const next = hopSenders[j + 1];
395
+ const keep = buyAmountsWei[j] ?? 0n;
396
+ prefixKept += keep;
397
+ const remaining = totalBuyWei - prefixKept;
398
+ if (remaining <= 0n)
399
+ break;
400
+ const callData = encodeExecute(next, remaining, '0x');
401
+ const signedHop = await this.aaManager.buildUserOpWithState({
402
+ ownerWallet: hopOwners[j],
403
+ sender,
404
+ nonce: nonceMap.next(sender),
405
+ initCode: consumeInitCode(sender),
406
+ callData,
407
+ signOnly: true,
408
+ });
409
+ outOps.push(signedHop.userOp);
410
+ }
411
+ // 3) lastHop 分发给剩余买方(不含 hop 自己)
412
+ const lastHopIdx = hopSenders.length - 1;
413
+ const lastHopSender = hopSenders[lastHopIdx];
414
+ const lastHopOwner = hopOwners[lastHopIdx];
415
+ const rest = buyerSenders.slice(hopSenders.length);
416
+ const restAmounts = buyAmountsWei.slice(hopSenders.length);
417
+ const restItems = rest.map((to, i) => ({ to, value: restAmounts[i] ?? 0n })).filter(x => x.value > 0n);
418
+ const restChunks = chunkArray(restItems, maxPerOp);
419
+ for (const ch of restChunks) {
420
+ const { totalValue, data } = encodeNativeDisperseViaMulticall3({
421
+ to: ch.map(x => x.to),
422
+ values: ch.map(x => x.value),
423
+ });
424
+ const callData = encodeExecute(MULTICALL3, totalValue, data);
425
+ const signedDisperse = await this.aaManager.buildUserOpWithState({
426
+ ownerWallet: lastHopOwner,
427
+ sender: lastHopSender,
428
+ nonce: nonceMap.next(lastHopSender),
429
+ initCode: consumeInitCode(lastHopSender),
430
+ callData,
431
+ signOnly: true,
432
+ });
433
+ outOps.push(signedDisperse.userOp);
434
+ }
435
+ }
436
+ }
437
+ // Batch Buy ops(分发后再执行,确保每个买方 sender 先有 OKB)
298
438
  for (let i = 0; i < buyerOwners.length; i++) {
299
439
  const ai = buyerAis[i];
300
440
  const buyWei = buyAmountsWei[i];
@@ -348,6 +488,7 @@ export class AAPortalSwapExecutor {
348
488
  profitRecipient,
349
489
  profitWei: profitWei.toString(),
350
490
  quotedSellOutWei: quotedSellOutWei.toString(),
491
+ disperseHopCount: String(hopCount),
351
492
  },
352
493
  };
353
494
  }
@@ -234,6 +234,14 @@ export interface BundleSwapParams {
234
234
  buyAmountOkb?: string;
235
235
  /** 预估卖出滑点(基点),默认 100 = 1%(用于 buyAmountOkb 未传入时) */
236
236
  slippageBps?: number;
237
+ /**
238
+ * ✅ 转账多跳数(AA 专用,且与 BSC 的“多跳”逻辑相互独立)
239
+ *
240
+ * 用途:当卖出与买入不是同一个 AA(Sender) 时,需要把卖出得到的 OKB 先转到买方 AA(Sender) 才能执行 buy。
241
+ * - 0:卖方 AA(Sender) 直接转给买方 AA(Sender)
242
+ * - >0:按“多跳”方式转账(实现位于 xlayer/*,不会影响 BSC bundle/merkle)
243
+ */
244
+ disperseHopCount?: number;
237
245
  /** 配置覆盖 */
238
246
  config?: Partial<XLayerConfig>;
239
247
  }
@@ -263,6 +271,8 @@ export interface BundleSwapResult {
263
271
  profitWei?: string;
264
272
  /** 预估卖出输出(OKB wei) */
265
273
  quotedSellOutWei?: string;
274
+ /** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
275
+ disperseHopCount?: string;
266
276
  };
267
277
  }
268
278
  /**
@@ -312,6 +322,8 @@ export interface BundleSwapSignResult {
312
322
  profitWei?: string;
313
323
  /** 预估卖出输出(OKB wei) */
314
324
  quotedSellOutWei?: string;
325
+ /** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
326
+ disperseHopCount?: string;
315
327
  };
316
328
  }
317
329
  /**
@@ -336,6 +348,14 @@ export interface BundleBatchSwapParams {
336
348
  sellAmount?: string;
337
349
  /** 卖出比例(0-100,默认 100) */
338
350
  sellPercent?: number;
351
+ /**
352
+ * ✅ 转账多跳数(AA 专用)
353
+ *
354
+ * 用途:卖出所得 OKB 需要在同一笔 handleOps 内分发给多个买方 AA(Sender)。
355
+ * - 0:卖方 AA(Sender) 直接分发给所有买方 AA(Sender)
356
+ * - >0:按“多跳”方式分发(实现位于 xlayer/*,不会影响 BSC bundle/merkle)
357
+ */
358
+ disperseHopCount?: number;
339
359
  /** 配置覆盖 */
340
360
  config?: Partial<XLayerConfig>;
341
361
  }
@@ -360,6 +380,8 @@ export interface BundleBatchSwapResult {
360
380
  profitWei?: string;
361
381
  /** 预估卖出输出(OKB wei) */
362
382
  quotedSellOutWei?: string;
383
+ /** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
384
+ disperseHopCount?: string;
363
385
  };
364
386
  }
365
387
  export interface BundleBatchSwapSignParams extends BundleBatchSwapParams {
@@ -392,6 +414,8 @@ export interface BundleBatchSwapSignResult {
392
414
  profitWei?: string;
393
415
  /** 预估卖出输出(OKB wei) */
394
416
  quotedSellOutWei?: string;
417
+ /** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
418
+ disperseHopCount?: string;
395
419
  };
396
420
  }
397
421
  /**
@@ -458,6 +482,16 @@ export interface BundleSellResult {
458
482
  sellResult: HandleOpsResult;
459
483
  /** 归集交易结果(如果 withdrawToOwner) */
460
484
  withdrawResult?: HandleOpsResult;
485
+ /**
486
+ * 利润汇总(如果 withdrawToOwner=true 且实际归集金额>0)
487
+ * - 注意:AA 的 handleOps tx.value 永远是 0;利润是“从 AA Sender 余额里拆分转走”的金额
488
+ */
489
+ profit?: {
490
+ extractProfit: boolean;
491
+ profitBps: number;
492
+ profitRecipient: string;
493
+ totalProfitWei: string;
494
+ };
461
495
  }
462
496
  /**
463
497
  * 捆绑买卖结果
@@ -473,6 +507,13 @@ export interface BundleBuySellResult {
473
507
  withdrawResult?: HandleOpsResult;
474
508
  /** 各 sender 的最终 OKB 余额 */
475
509
  finalBalances: Map<string, bigint>;
510
+ /** 利润汇总(如果 withdrawToOwner=true 且实际归集金额>0) */
511
+ profit?: {
512
+ extractProfit: boolean;
513
+ profitBps: number;
514
+ profitRecipient: string;
515
+ totalProfitWei: string;
516
+ };
476
517
  }
477
518
  /**
478
519
  * 刷量结果
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.5.32",
3
+ "version": "1.5.33",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",