four-flap-meme-sdk 1.5.32 → 1.5.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,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
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * XLayer AA Buy-First(内盘 / Flap Portal)
3
+ *
4
+ * 对齐 BSC buy-first 思路:
5
+ * - 买方先买入(多钱包随机拆分资金)
6
+ * - 卖方再卖出“等值 token”(卖方需要预持仓)
7
+ * - ✅ 利润:在“卖出后归集 OKB”阶段刮取(executeBatch 拆分给 profitRecipient + owner)
8
+ *
9
+ * 说明:
10
+ * - buy + sell 会放在同一笔 handleOps 内,保持“buy-first 原子”语义
11
+ * - withdraw 需要知道 sell 后的余额,因此单独再发一笔 handleOps
12
+ */
13
+ import type { XLayerConfig } from './types.js';
14
+ import type { BuyFirstParams, BuyFirstResult } from './types.js';
15
+ export declare class AAPortalBuyFirstExecutor {
16
+ private aaManager;
17
+ private portalQuery;
18
+ private config;
19
+ constructor(config?: XLayerConfig);
20
+ private runHandleOps;
21
+ private pickWallets;
22
+ private getAccountInfos;
23
+ private safePreviewBuy;
24
+ private buildWithdrawUserOp;
25
+ /**
26
+ * 执行一轮 buy-first(内盘)
27
+ */
28
+ execute(params: BuyFirstParams): Promise<BuyFirstResult>;
29
+ }
30
+ export declare function createAAPortalBuyFirstExecutor(config?: XLayerConfig): AAPortalBuyFirstExecutor;