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.
@@ -0,0 +1,415 @@
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 { Contract, Interface, Wallet, ethers } from 'ethers';
14
+ import { AANonceMap } from './types.js';
15
+ import { ENTRYPOINT_ABI, DEFAULT_WITHDRAW_RESERVE, FLAP_PORTAL, } from './constants.js';
16
+ import { AAAccountManager, encodeExecute, encodeExecuteBatch } from './aa-account.js';
17
+ import { PortalQuery, encodeApproveCall, encodeBuyCall, encodeSellCall, parseOkb } from './portal-ops.js';
18
+ import { mapWithConcurrency } from '../utils/concurrency.js';
19
+ import { PROFIT_CONFIG } from '../utils/constants.js';
20
+ // 固定 gas(用于减少 RPC 与提高可控性)
21
+ const DEFAULT_CALL_GAS_LIMIT_BUY = 450000n;
22
+ const DEFAULT_CALL_GAS_LIMIT_SELL = 450000n;
23
+ const DEFAULT_CALL_GAS_LIMIT_APPROVE = 200000n;
24
+ const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
25
+ function resolveProfitSettings(config) {
26
+ const extractProfit = config?.extractProfit !== false; // 默认 true
27
+ const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
28
+ const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
29
+ const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
30
+ return { extractProfit, profitBps, profitRecipient };
31
+ }
32
+ function calculateProfitWei(amountWei, profitBps) {
33
+ if (amountWei <= 0n)
34
+ return 0n;
35
+ if (!Number.isFinite(profitBps) || profitBps <= 0)
36
+ return 0n;
37
+ return (amountWei * BigInt(profitBps)) / 10000n;
38
+ }
39
+ function shuffle(arr) {
40
+ const a = arr.slice();
41
+ for (let i = a.length - 1; i > 0; i--) {
42
+ const j = Math.floor(Math.random() * (i + 1));
43
+ [a[i], a[j]] = [a[j], a[i]];
44
+ }
45
+ return a;
46
+ }
47
+ /**
48
+ * ✅ 将金额随机拆分成多份(BigInt 安全)
49
+ * 每份权重在 0.5 - 1.5 之间随机浮动(用整数权重近似)
50
+ */
51
+ function splitAmount(totalAmount, count) {
52
+ if (count <= 0)
53
+ throw new Error('拆分份数必须大于 0');
54
+ if (count === 1)
55
+ return [totalAmount];
56
+ if (totalAmount <= 0n)
57
+ return new Array(count).fill(0n);
58
+ const SCALE = 1000000n;
59
+ const weights = [];
60
+ for (let i = 0; i < count; i++) {
61
+ // [0.5, 1.5) * SCALE
62
+ const w = BigInt(500000 + Math.floor(Math.random() * 1000000));
63
+ weights.push(w);
64
+ }
65
+ const totalWeight = weights.reduce((a, b) => a + b, 0n);
66
+ const amounts = [];
67
+ let allocated = 0n;
68
+ for (let i = 0; i < count - 1; i++) {
69
+ const amt = (totalAmount * weights[i]) / totalWeight;
70
+ amounts.push(amt);
71
+ allocated += amt;
72
+ }
73
+ amounts.push(totalAmount - allocated);
74
+ return shuffle(amounts);
75
+ }
76
+ export class AAPortalBuyFirstExecutor {
77
+ constructor(config = {}) {
78
+ this.config = config;
79
+ this.aaManager = new AAAccountManager(config);
80
+ this.portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
81
+ }
82
+ async runHandleOps(label, ops, bundlerSigner, beneficiary) {
83
+ const provider = this.aaManager.getProvider();
84
+ const entryPointAddress = this.aaManager.getEntryPointAddress();
85
+ const feeData = await provider.getFeeData();
86
+ console.log(`\n[${label}] 发送 handleOps,ops=${ops.length} ...`);
87
+ const entryPointWithSigner = new Contract(entryPointAddress, ENTRYPOINT_ABI, bundlerSigner);
88
+ const tx = await entryPointWithSigner.handleOps(ops, beneficiary, { gasPrice: feeData.gasPrice ?? 100000000n });
89
+ console.log(`[${label}] txHash:`, tx.hash);
90
+ const receipt = await tx.wait();
91
+ console.log(`[${label}] mined block=${receipt.blockNumber} status=${receipt.status}`);
92
+ const epIface = new Interface(ENTRYPOINT_ABI);
93
+ const userOpEvents = [];
94
+ for (const log of receipt.logs) {
95
+ try {
96
+ const parsed = epIface.parseLog(log);
97
+ if (parsed?.name === 'UserOperationEvent') {
98
+ const e = parsed.args;
99
+ userOpEvents.push({
100
+ userOpHash: e.userOpHash,
101
+ sender: e.sender,
102
+ paymaster: e.paymaster,
103
+ success: e.success,
104
+ actualGasCost: e.actualGasCost,
105
+ actualGasUsed: e.actualGasUsed,
106
+ });
107
+ }
108
+ }
109
+ catch { }
110
+ }
111
+ return { txHash: tx.hash, blockNumber: receipt.blockNumber, status: receipt.status, userOpEvents };
112
+ }
113
+ pickWallets(privateKeys, n) {
114
+ const provider = this.aaManager.getProvider();
115
+ const wallets = privateKeys.map((pk) => new Wallet(pk, provider));
116
+ if (!n || n <= 0 || n >= wallets.length)
117
+ return wallets;
118
+ return wallets.slice(0, n);
119
+ }
120
+ async getAccountInfos(wallets) {
121
+ return await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
122
+ }
123
+ async safePreviewBuy(tokenAddress, okbAmount) {
124
+ if (okbAmount <= 0n)
125
+ return 0n;
126
+ try {
127
+ return await this.portalQuery.previewBuy(tokenAddress, okbAmount);
128
+ }
129
+ catch {
130
+ // fallback:quoteExactInput(native->token)
131
+ return await this.portalQuery.quoteExactInput('0x0000000000000000000000000000000000000000', tokenAddress, okbAmount);
132
+ }
133
+ }
134
+ async buildWithdrawUserOp(params) {
135
+ const senderBalance = params.senderBalance;
136
+ if (senderBalance <= params.reserveWei)
137
+ return null;
138
+ const effConfig = { ...(this.config ?? {}), ...(params.configOverride ?? {}) };
139
+ const gasPolicyRaw = effConfig.gasPolicy ?? 'bundlerEstimate';
140
+ const gasPolicy = gasPolicyRaw === 'bundlerEstimate' ? 'fixed' : gasPolicyRaw;
141
+ // 先估 prefund(空调用)
142
+ const tempCallData = encodeExecute(params.ownerWallet.address, 0n, '0x');
143
+ const { prefundWei } = gasPolicy === 'fixed'
144
+ ? await this.aaManager.buildUserOpWithFixedGas({
145
+ ownerWallet: params.ownerWallet,
146
+ sender: params.sender,
147
+ callData: tempCallData,
148
+ nonce: params.nonce,
149
+ initCode: params.initCode,
150
+ deployed: params.initCode === '0x',
151
+ fixedGas: {
152
+ ...(effConfig.fixedGas ?? {}),
153
+ callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
154
+ },
155
+ })
156
+ : await this.aaManager.buildUserOpWithLocalEstimate({
157
+ ownerWallet: params.ownerWallet,
158
+ sender: params.sender,
159
+ callData: tempCallData,
160
+ nonce: params.nonce,
161
+ initCode: params.initCode,
162
+ });
163
+ const withdrawAmount = senderBalance > prefundWei + params.reserveWei
164
+ ? senderBalance - prefundWei - params.reserveWei
165
+ : 0n;
166
+ if (withdrawAmount <= 0n)
167
+ return null;
168
+ const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effConfig);
169
+ const profitWei = extractProfit ? calculateProfitWei(withdrawAmount, profitBps) : 0n;
170
+ const toOwnerWei = withdrawAmount - profitWei;
171
+ const callData = extractProfit && profitWei > 0n
172
+ ? encodeExecuteBatch([profitRecipient, params.ownerWallet.address], [profitWei, toOwnerWei], ['0x', '0x'])
173
+ : encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
174
+ const { userOp } = gasPolicy === 'fixed'
175
+ ? await this.aaManager.buildUserOpWithFixedGas({
176
+ ownerWallet: params.ownerWallet,
177
+ sender: params.sender,
178
+ callData,
179
+ nonce: params.nonce,
180
+ initCode: params.initCode,
181
+ deployed: params.initCode === '0x',
182
+ fixedGas: {
183
+ ...(effConfig.fixedGas ?? {}),
184
+ callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
185
+ },
186
+ })
187
+ : await this.aaManager.buildUserOpWithLocalEstimate({
188
+ ownerWallet: params.ownerWallet,
189
+ sender: params.sender,
190
+ callData,
191
+ nonce: params.nonce,
192
+ initCode: params.initCode,
193
+ });
194
+ return { userOp, prefundWei, profitWei };
195
+ }
196
+ /**
197
+ * 执行一轮 buy-first(内盘)
198
+ */
199
+ async execute(params) {
200
+ const { tradeType = 'FLAP', tokenAddress, buyerPrivateKeys, sellerPrivateKeys, buyerFunds, buyCount, sellCount, slippageBps = 0, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, } = params;
201
+ if (tradeType !== 'FLAP') {
202
+ throw new Error('AAPortalBuyFirstExecutor 仅支持 tradeType=FLAP(内盘)');
203
+ }
204
+ const buyers = this.pickWallets(buyerPrivateKeys, buyCount);
205
+ const sellers = this.pickWallets(sellerPrivateKeys, sellCount);
206
+ if (buyers.length === 0)
207
+ throw new Error('buyCount=0 或 buyerPrivateKeys 为空');
208
+ if (sellers.length === 0)
209
+ throw new Error('sellCount=0 或 sellerPrivateKeys 为空');
210
+ const effConfig = { ...(this.config ?? {}), ...(config ?? {}) };
211
+ const profitSettings = resolveProfitSettings(effConfig);
212
+ const totalBuyWei = parseOkb(String(buyerFunds));
213
+ if (totalBuyWei <= 0n)
214
+ throw new Error('buyerFunds 需要 > 0');
215
+ const buyAmountsWei = splitAmount(totalBuyWei, buyers.length);
216
+ // 估算买入 tokenOut,并按 slippageBps 做折减(只用于估算 sellAmount)
217
+ const quotedPerBuy = await mapWithConcurrency(buyAmountsWei, 6, async (amt) => this.safePreviewBuy(tokenAddress, amt));
218
+ const quotedTotalTokenOut = quotedPerBuy.reduce((a, b) => a + (b ?? 0n), 0n);
219
+ const estimatedTokenOutWei = (quotedTotalTokenOut * BigInt(10000 - Math.max(0, Math.min(10000, slippageBps)))) / 10000n;
220
+ if (estimatedTokenOutWei <= 0n) {
221
+ throw new Error('买入报价为 0,无法规划 sellAmount(可能无流动性或报价失败)');
222
+ }
223
+ const sellAmountsWei = splitAmount(estimatedTokenOutWei, sellers.length);
224
+ // 获取 AA 账户信息
225
+ const buyerAis = await this.getAccountInfos(buyers);
226
+ const sellerAis = await this.getAccountInfos(sellers);
227
+ // nonceMap
228
+ const nonceMap = new AANonceMap();
229
+ for (const ai of buyerAis)
230
+ nonceMap.init(ai.sender, ai.nonce);
231
+ for (const ai of sellerAis)
232
+ nonceMap.init(ai.sender, ai.nonce);
233
+ // initCode:确保同一 sender 只在第一笔 op 携带
234
+ const initCodeBySenderLower = new Map();
235
+ const initIfNeeded = (sender, deployed, ownerAddress) => {
236
+ const k = sender.toLowerCase();
237
+ if (initCodeBySenderLower.has(k))
238
+ return;
239
+ initCodeBySenderLower.set(k, deployed ? '0x' : this.aaManager.generateInitCode(ownerAddress));
240
+ };
241
+ const consumeInitCode = (sender) => {
242
+ const k = sender.toLowerCase();
243
+ const cur = initCodeBySenderLower.get(k);
244
+ if (cur === undefined)
245
+ throw new Error(`initCode not initialized for sender: ${sender}`);
246
+ if (cur !== '0x')
247
+ initCodeBySenderLower.set(k, '0x');
248
+ return cur;
249
+ };
250
+ for (let i = 0; i < buyers.length; i++)
251
+ initIfNeeded(buyerAis[i].sender, buyerAis[i].deployed, buyers[i].address);
252
+ for (let i = 0; i < sellers.length; i++)
253
+ initIfNeeded(sellerAis[i].sender, sellerAis[i].deployed, sellers[i].address);
254
+ // 卖方余额检查(需要预持仓)
255
+ const sellerSenders = sellerAis.map((ai) => ai.sender);
256
+ const sellerTokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, sellerSenders);
257
+ for (let i = 0; i < sellers.length; i++) {
258
+ const sender = sellerSenders[i];
259
+ const needSell = sellAmountsWei[i] ?? 0n;
260
+ const bal = sellerTokenBalances.get(sender) ?? 0n;
261
+ if (needSell > bal) {
262
+ throw new Error(`卖方预持仓不足: sender=${sender} need=${needSell.toString()} bal=${bal.toString()}`);
263
+ }
264
+ }
265
+ // 卖方 allowance 检查(spender=FLAP_PORTAL)
266
+ const sellerAllowances = await this.portalQuery.getMultipleAllowances(tokenAddress, sellerSenders, FLAP_PORTAL);
267
+ // 构建 ops(buy-first:buy -> (approve) -> sell)
268
+ const ops = [];
269
+ // 1) buy ops(多买方)
270
+ for (let i = 0; i < buyers.length; i++) {
271
+ const w = buyers[i];
272
+ const ai = buyerAis[i];
273
+ const sender = ai.sender;
274
+ const buyWei = buyAmountsWei[i] ?? 0n;
275
+ if (buyWei <= 0n)
276
+ continue;
277
+ const swapData = encodeBuyCall(tokenAddress, buyWei, 0n);
278
+ const callData = encodeExecute(FLAP_PORTAL, buyWei, swapData);
279
+ const { userOp, prefundWei } = await this.aaManager.buildUserOpWithFixedGas({
280
+ ownerWallet: w,
281
+ sender,
282
+ nonce: nonceMap.next(sender),
283
+ initCode: consumeInitCode(sender),
284
+ callData,
285
+ deployed: ai.deployed,
286
+ fixedGas: {
287
+ ...(effConfig.fixedGas ?? {}),
288
+ callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_BUY,
289
+ },
290
+ });
291
+ // 确保 sender 有足够余额:买入金额 + prefund(paymaster 下 prefund=0)
292
+ await this.aaManager.ensureSenderBalance(w, sender, buyWei + prefundWei + parseOkb('0.0001'), `buyFirst/buyer${i + 1}/fund`);
293
+ const signed = await this.aaManager.signUserOp(userOp, w);
294
+ ops.push(signed.userOp);
295
+ }
296
+ // 2) seller approve + sell ops
297
+ for (let i = 0; i < sellers.length; i++) {
298
+ const w = sellers[i];
299
+ const ai = sellerAis[i];
300
+ const sender = ai.sender;
301
+ const sellWei = sellAmountsWei[i] ?? 0n;
302
+ if (sellWei <= 0n)
303
+ continue;
304
+ const allowance = sellerAllowances.get(sender) ?? 0n;
305
+ if (allowance < sellWei) {
306
+ const approveCallData = encodeExecute(tokenAddress, 0n, encodeApproveCall(FLAP_PORTAL, ethers.MaxUint256));
307
+ const { userOp: approveOp, prefundWei } = await this.aaManager.buildUserOpWithFixedGas({
308
+ ownerWallet: w,
309
+ sender,
310
+ nonce: nonceMap.next(sender),
311
+ initCode: consumeInitCode(sender),
312
+ callData: approveCallData,
313
+ deployed: ai.deployed,
314
+ fixedGas: {
315
+ ...(effConfig.fixedGas ?? {}),
316
+ callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_APPROVE,
317
+ },
318
+ });
319
+ await this.aaManager.ensureSenderBalance(w, sender, prefundWei + parseOkb('0.0001'), `buyFirst/seller${i + 1}/approve-fund`);
320
+ const signedApprove = await this.aaManager.signUserOp(approveOp, w);
321
+ ops.push(signedApprove.userOp);
322
+ }
323
+ const sellSwapData = encodeSellCall(tokenAddress, sellWei, 0n);
324
+ const sellCallData = encodeExecute(FLAP_PORTAL, 0n, sellSwapData);
325
+ const { userOp: sellOp, prefundWei: sellPrefund } = await this.aaManager.buildUserOpWithFixedGas({
326
+ ownerWallet: w,
327
+ sender,
328
+ nonce: nonceMap.next(sender),
329
+ initCode: consumeInitCode(sender),
330
+ callData: sellCallData,
331
+ deployed: ai.deployed,
332
+ fixedGas: {
333
+ ...(effConfig.fixedGas ?? {}),
334
+ callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_SELL,
335
+ },
336
+ });
337
+ await this.aaManager.ensureSenderBalance(w, sender, sellPrefund + parseOkb('0.0001'), `buyFirst/seller${i + 1}/sell-fund`);
338
+ const signedSell = await this.aaManager.signUserOp(sellOp, w);
339
+ ops.push(signedSell.userOp);
340
+ }
341
+ if (ops.length === 0) {
342
+ throw new Error('本轮没有生成任何 UserOp(buyAmounts 或 sellAmounts 可能全为 0)');
343
+ }
344
+ // payer/beneficiary
345
+ const payerWallet = buyers[0];
346
+ const beneficiary = payerWallet.address;
347
+ // 第一步:buy-first 原子阶段
348
+ const buySellResult = await this.runHandleOps('aa-buy-first/portal', ops, payerWallet, beneficiary);
349
+ // 第二步:可选归集(卖方)
350
+ let withdrawResult;
351
+ let finalSellerBalances;
352
+ let totalProfitWei = 0n;
353
+ if (withdrawToOwner) {
354
+ const reserveWei = parseOkb(withdrawReserve);
355
+ const sellerOkbBalances = await this.portalQuery.getMultipleOkbBalances(sellerSenders);
356
+ const withdrawOps = [];
357
+ const withdrawOpsBuilt = await mapWithConcurrency(sellers, 3, async (w, i) => {
358
+ const sender = sellerAis[i].sender;
359
+ const senderBalance = sellerOkbBalances.get(sender) ?? 0n;
360
+ const usedNonce = nonceMap.peek(sender);
361
+ const built = await this.buildWithdrawUserOp({
362
+ ownerWallet: w,
363
+ sender,
364
+ nonce: usedNonce,
365
+ initCode: '0x',
366
+ senderBalance,
367
+ reserveWei,
368
+ configOverride: config,
369
+ });
370
+ if (!built)
371
+ return null;
372
+ nonceMap.commit(sender, usedNonce);
373
+ totalProfitWei += built.profitWei;
374
+ const signed = await this.aaManager.signUserOp(built.userOp, w);
375
+ return signed.userOp;
376
+ });
377
+ for (const op of withdrawOpsBuilt)
378
+ if (op)
379
+ withdrawOps.push(op);
380
+ if (withdrawOps.length > 0) {
381
+ withdrawResult = await this.runHandleOps('aa-buy-first/portal-withdraw', withdrawOps, payerWallet, beneficiary);
382
+ }
383
+ finalSellerBalances = await this.portalQuery.getMultipleOkbBalances(sellerSenders);
384
+ }
385
+ return {
386
+ buySellResult,
387
+ withdrawResult,
388
+ finalSellerBalances,
389
+ profit: withdrawToOwner
390
+ ? {
391
+ extractProfit: profitSettings.extractProfit,
392
+ profitBps: profitSettings.profitBps,
393
+ profitRecipient: profitSettings.profitRecipient,
394
+ totalProfitWei: totalProfitWei.toString(),
395
+ }
396
+ : undefined,
397
+ metadata: {
398
+ tradeType: 'FLAP',
399
+ tokenAddress,
400
+ buyerOwners: buyers.map((w) => w.address),
401
+ buyerSenders: buyerAis.map((ai) => ai.sender),
402
+ sellerOwners: sellers.map((w) => w.address),
403
+ sellerSenders,
404
+ buyAmountsWei: buyAmountsWei.map((x) => x.toString()),
405
+ sellAmountsWei: sellAmountsWei.map((x) => x.toString()),
406
+ totalBuyWei: totalBuyWei.toString(),
407
+ estimatedTokenOutWei: estimatedTokenOutWei.toString(),
408
+ slippageBps,
409
+ },
410
+ };
411
+ }
412
+ }
413
+ export function createAAPortalBuyFirstExecutor(config) {
414
+ return new AAPortalBuyFirstExecutor(config);
415
+ }
@@ -161,8 +161,12 @@ export interface BundleBuyParams {
161
161
  tokenAddress: string;
162
162
  /** Owner 私钥列表 */
163
163
  privateKeys: string[];
164
- /** 每个 owner 的买入金额(OKB */
164
+ /** 每个 owner 的买入金额(OKB 或 quoteToken,取决于 quoteToken 参数) */
165
165
  buyAmounts: string[];
166
+ /** 买入使用的代币地址(零地址表示使用原生代币 OKB,非零地址表示使用 ERC20 代币如 USDC) */
167
+ quoteToken?: string;
168
+ /** quoteToken 的精度(默认 18,如果 quoteToken 不是 18 位精度需要指定) */
169
+ quoteTokenDecimals?: number;
166
170
  /** 是否将代币转回 owner EOA(默认 false) */
167
171
  transferBackToOwner?: boolean;
168
172
  /** 配置覆盖 */
@@ -193,8 +197,12 @@ export interface BundleBuySellParams {
193
197
  tokenAddress: string;
194
198
  /** Owner 私钥列表 */
195
199
  privateKeys: string[];
196
- /** 每个 owner 的买入金额(OKB */
200
+ /** 每个 owner 的买入金额(OKB 或 quoteToken,取决于 quoteToken 参数) */
197
201
  buyAmounts: string[];
202
+ /** 买入使用的代币地址(零地址表示使用原生代币 OKB,非零地址表示使用 ERC20 代币如 USDC) */
203
+ quoteToken?: string;
204
+ /** quoteToken 的精度(默认 18,如果 quoteToken 不是 18 位精度需要指定) */
205
+ quoteTokenDecimals?: number;
198
206
  /** 卖出比例(0-100,默认 100 全部卖出) */
199
207
  sellPercent?: number;
200
208
  /** 是否将 OKB 归集回 owner EOA(默认 true) */
@@ -234,6 +242,14 @@ export interface BundleSwapParams {
234
242
  buyAmountOkb?: string;
235
243
  /** 预估卖出滑点(基点),默认 100 = 1%(用于 buyAmountOkb 未传入时) */
236
244
  slippageBps?: number;
245
+ /**
246
+ * ✅ 转账多跳数(AA 专用,且与 BSC 的“多跳”逻辑相互独立)
247
+ *
248
+ * 用途:当卖出与买入不是同一个 AA(Sender) 时,需要把卖出得到的 OKB 先转到买方 AA(Sender) 才能执行 buy。
249
+ * - 0:卖方 AA(Sender) 直接转给买方 AA(Sender)
250
+ * - >0:按“多跳”方式转账(实现位于 xlayer/*,不会影响 BSC bundle/merkle)
251
+ */
252
+ disperseHopCount?: number;
237
253
  /** 配置覆盖 */
238
254
  config?: Partial<XLayerConfig>;
239
255
  }
@@ -263,6 +279,8 @@ export interface BundleSwapResult {
263
279
  profitWei?: string;
264
280
  /** 预估卖出输出(OKB wei) */
265
281
  quotedSellOutWei?: string;
282
+ /** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
283
+ disperseHopCount?: string;
266
284
  };
267
285
  }
268
286
  /**
@@ -312,6 +330,8 @@ export interface BundleSwapSignResult {
312
330
  profitWei?: string;
313
331
  /** 预估卖出输出(OKB wei) */
314
332
  quotedSellOutWei?: string;
333
+ /** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
334
+ disperseHopCount?: string;
315
335
  };
316
336
  }
317
337
  /**
@@ -336,6 +356,14 @@ export interface BundleBatchSwapParams {
336
356
  sellAmount?: string;
337
357
  /** 卖出比例(0-100,默认 100) */
338
358
  sellPercent?: number;
359
+ /**
360
+ * ✅ 转账多跳数(AA 专用)
361
+ *
362
+ * 用途:卖出所得 OKB 需要在同一笔 handleOps 内分发给多个买方 AA(Sender)。
363
+ * - 0:卖方 AA(Sender) 直接分发给所有买方 AA(Sender)
364
+ * - >0:按“多跳”方式分发(实现位于 xlayer/*,不会影响 BSC bundle/merkle)
365
+ */
366
+ disperseHopCount?: number;
339
367
  /** 配置覆盖 */
340
368
  config?: Partial<XLayerConfig>;
341
369
  }
@@ -360,6 +388,8 @@ export interface BundleBatchSwapResult {
360
388
  profitWei?: string;
361
389
  /** 预估卖出输出(OKB wei) */
362
390
  quotedSellOutWei?: string;
391
+ /** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
392
+ disperseHopCount?: string;
363
393
  };
364
394
  }
365
395
  export interface BundleBatchSwapSignParams extends BundleBatchSwapParams {
@@ -392,6 +422,8 @@ export interface BundleBatchSwapSignResult {
392
422
  profitWei?: string;
393
423
  /** 预估卖出输出(OKB wei) */
394
424
  quotedSellOutWei?: string;
425
+ /** ✅ AA:转账多跳数(仅 xlayer AA 使用;不影响 BSC) */
426
+ disperseHopCount?: string;
395
427
  };
396
428
  }
397
429
  /**
@@ -458,6 +490,16 @@ export interface BundleSellResult {
458
490
  sellResult: HandleOpsResult;
459
491
  /** 归集交易结果(如果 withdrawToOwner) */
460
492
  withdrawResult?: HandleOpsResult;
493
+ /**
494
+ * 利润汇总(如果 withdrawToOwner=true 且实际归集金额>0)
495
+ * - 注意:AA 的 handleOps tx.value 永远是 0;利润是“从 AA Sender 余额里拆分转走”的金额
496
+ */
497
+ profit?: {
498
+ extractProfit: boolean;
499
+ profitBps: number;
500
+ profitRecipient: string;
501
+ totalProfitWei: string;
502
+ };
461
503
  }
462
504
  /**
463
505
  * 捆绑买卖结果
@@ -473,6 +515,13 @@ export interface BundleBuySellResult {
473
515
  withdrawResult?: HandleOpsResult;
474
516
  /** 各 sender 的最终 OKB 余额 */
475
517
  finalBalances: Map<string, bigint>;
518
+ /** 利润汇总(如果 withdrawToOwner=true 且实际归集金额>0) */
519
+ profit?: {
520
+ extractProfit: boolean;
521
+ profitBps: number;
522
+ profitRecipient: string;
523
+ totalProfitWei: string;
524
+ };
476
525
  }
477
526
  /**
478
527
  * 刷量结果
@@ -487,6 +536,64 @@ export interface VolumeResult {
487
536
  /** 总交易量(OKB) */
488
537
  totalVolume: bigint;
489
538
  }
539
+ export type BuyFirstTradeType = 'FLAP' | 'V2';
540
+ export interface BuyFirstParams {
541
+ tradeType?: BuyFirstTradeType;
542
+ /** DEX 标识(仅 V2 时使用,用于选择 Router) */
543
+ dexKey?: string;
544
+ /** 显式指定 Router 地址(仅 V2 时使用) */
545
+ routerAddress?: string;
546
+ /** DEX deadline 分钟数(仅 V2 时使用) */
547
+ deadlineMinutes?: number;
548
+ tokenAddress: string;
549
+ buyerPrivateKeys: string[];
550
+ sellerPrivateKeys: string[];
551
+ /** 本轮总买入资金(OKB) */
552
+ buyerFunds: string;
553
+ buyCount?: number;
554
+ sellCount?: number;
555
+ /** 报价滑点(仅用于估算 sellAmount),默认 0 */
556
+ slippageBps?: number;
557
+ withdrawToOwner?: boolean;
558
+ withdrawReserve?: string;
559
+ config?: Partial<XLayerConfig>;
560
+ }
561
+ export interface BuyFirstResult {
562
+ buySellResult: HandleOpsResult;
563
+ withdrawResult?: HandleOpsResult;
564
+ finalSellerBalances?: Map<string, bigint>;
565
+ profit?: {
566
+ extractProfit: boolean;
567
+ profitBps: number;
568
+ profitRecipient: string;
569
+ totalProfitWei: string;
570
+ };
571
+ metadata: {
572
+ tradeType: BuyFirstTradeType;
573
+ tokenAddress: string;
574
+ buyerOwners: string[];
575
+ buyerSenders: string[];
576
+ sellerOwners: string[];
577
+ sellerSenders: string[];
578
+ buyAmountsWei: string[];
579
+ sellAmountsWei: string[];
580
+ totalBuyWei: string;
581
+ estimatedTokenOutWei: string;
582
+ slippageBps: number;
583
+ };
584
+ }
585
+ export interface BuyFirstVolumeParams extends Omit<BuyFirstParams, 'buyerFunds'> {
586
+ buyerFundsPerRound: string;
587
+ rounds: number;
588
+ intervalMs?: number;
589
+ }
590
+ export interface BuyFirstVolumeResult {
591
+ successRounds: number;
592
+ failedRounds: number;
593
+ roundResults: BuyFirstResult[];
594
+ /** 总交易量(OKB wei,按“买入资金*2”统计) */
595
+ totalVolume: bigint;
596
+ }
490
597
  /**
491
598
  * 外盘(PotatoSwap)交易参数
492
599
  */
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.34",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",