four-flap-meme-sdk 1.5.51 → 1.5.53

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,27 @@
1
+ /**
2
+ * XLayer AA 账户信息批量获取器
3
+ */
4
+ import type { Wallet } from 'ethers';
5
+ import type { AAAccountManager } from './aa-account.js';
6
+ import type { AAAccount } from './types.js';
7
+ export interface AccountInfoBatch {
8
+ buyerAis: AAAccount[];
9
+ sellerAis: AAAccount[];
10
+ sellerSenders: string[];
11
+ sellerTokenBalances: Map<string, bigint>;
12
+ sellerAllowances: Map<string, bigint>;
13
+ }
14
+ /**
15
+ * ✅ 优化 1: 并行获取所有账户信息
16
+ */
17
+ export declare function fetchAccountInfoBatch(params: {
18
+ buyers: Wallet[];
19
+ sellers: Wallet[];
20
+ tokenAddress: string;
21
+ effectiveRouter: string;
22
+ aaManager: AAAccountManager;
23
+ portalQuery: {
24
+ getMultipleTokenBalances: (token: string, addresses: string[]) => Promise<Map<string, bigint>>;
25
+ getMultipleAllowances: (token: string, owners: string[], spender: string) => Promise<Map<string, bigint>>;
26
+ };
27
+ }): Promise<AccountInfoBatch>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * XLayer AA 账户信息批量获取器
3
+ */
4
+ /**
5
+ * ✅ 优化 1: 并行获取所有账户信息
6
+ */
7
+ export async function fetchAccountInfoBatch(params) {
8
+ const { buyers, sellers, tokenAddress, effectiveRouter, aaManager, portalQuery } = params;
9
+ // ✅ 并行获取买方和卖方的账户信息
10
+ const [buyerAis, sellerAis] = await Promise.all([
11
+ aaManager.getMultipleAccountInfo(buyers.map((w) => w.address)),
12
+ aaManager.getMultipleAccountInfo(sellers.map((w) => w.address))
13
+ ]);
14
+ const sellerSenders = sellerAis.map((ai) => ai.sender);
15
+ // ✅ 并行获取卖方的代币余额和授权额度
16
+ const [sellerTokenBalances, sellerAllowances] = await Promise.all([
17
+ portalQuery.getMultipleTokenBalances(tokenAddress, sellerSenders),
18
+ portalQuery.getMultipleAllowances(tokenAddress, sellerSenders, effectiveRouter)
19
+ ]);
20
+ return {
21
+ buyerAis,
22
+ sellerAis,
23
+ sellerSenders,
24
+ sellerTokenBalances,
25
+ sellerAllowances
26
+ };
27
+ }
@@ -648,7 +648,7 @@ export class BundleExecutor {
648
648
  // TODO: 如果需要,可以在这里检查并转账 ERC20 代币到 sender
649
649
  }
650
650
  });
651
- // ✅ 构建买入 callData:如果使用 ERC20 代币,需要修改 encodeBuyCall 以支持 inputToken
651
+ // ✅ 构建买入 callData:如果使用 ERC20 代币,需要先 approve
652
652
  const buyCallDatas = buyWeis.map((buyWei) => {
653
653
  // 使用 portal 的 swapExactInput,支持 inputToken 参数
654
654
  const portalIface = new Interface([
@@ -663,8 +663,16 @@ export class BundleExecutor {
663
663
  permitData: '0x',
664
664
  },
665
665
  ]);
666
- // 如果使用原生代币,value 为买入金额;否则为 0
667
- return encodeExecute(this.portalAddress, useNativeToken ? buyWei : 0n, swapData);
666
+ // ERC20 代币需要先 approve,使用 executeBatch 将 approve + swap 合并为一个 UserOp
667
+ if (useNativeToken) {
668
+ // 原生代币:直接 swap
669
+ return encodeExecute(this.portalAddress, buyWei, swapData);
670
+ }
671
+ else {
672
+ // ERC20 代币:approve + swap 批量调用
673
+ const approveData = encodeApproveCall(this.portalAddress, buyWei);
674
+ return encodeExecuteBatch([inputToken, this.portalAddress], [0n, 0n], [approveData, swapData]);
675
+ }
668
676
  });
669
677
  const initCodes = accountInfos.map((ai, i) => ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address));
670
678
  const { userOps: buyUserOps, prefundWeis } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
@@ -977,7 +985,7 @@ export class BundleExecutor {
977
985
  // TODO: 如果需要,可以在这里检查并转账 ERC20 代币到 sender
978
986
  }
979
987
  });
980
- // ✅ 构建买入 callData:如果使用 ERC20 代币,需要修改 encodeBuyCall 以支持 inputToken
988
+ // ✅ 构建买入 callData:如果使用 ERC20 代币,需要先 approve
981
989
  const buyCallDatas = buyWeis.map((buyWei) => {
982
990
  // 使用 portal 的 swapExactInput,支持 inputToken 参数
983
991
  const portalIface = new Interface([
@@ -992,8 +1000,16 @@ export class BundleExecutor {
992
1000
  permitData: '0x',
993
1001
  },
994
1002
  ]);
995
- // 如果使用原生代币,value 为买入金额;否则为 0
996
- return encodeExecute(this.portalAddress, useNativeToken ? buyWei : 0n, swapData);
1003
+ // ERC20 代币需要先 approve,使用 executeBatch 将 approve + swap 合并为一个 UserOp
1004
+ if (useNativeToken) {
1005
+ // 原生代币:直接 swap
1006
+ return encodeExecute(this.portalAddress, buyWei, swapData);
1007
+ }
1008
+ else {
1009
+ // ERC20 代币:approve + swap 批量调用
1010
+ const approveData = encodeApproveCall(this.portalAddress, buyWei);
1011
+ return encodeExecuteBatch([inputToken, this.portalAddress], [0n, 0n], [approveData, swapData]);
1012
+ }
997
1013
  });
998
1014
  const initCodes = accountInfos.map((ai, i) => (ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address)));
999
1015
  const { userOps: buyUserOps, prefundWeis } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
@@ -1252,12 +1268,15 @@ export class BundleExecutor {
1252
1268
  const inputToken = params.quoteToken && params.quoteToken !== ZERO_ADDRESS ? params.quoteToken : ZERO_ADDRESS;
1253
1269
  const useNativeToken = inputToken === ZERO_ADDRESS;
1254
1270
  const quoteDecimals = 18;
1255
- let totalBuyWei = 0n;
1256
- let totalBuyProfitWei = 0n;
1257
- const buyerSenders = [];
1258
1271
  const portalIface = new Interface(PORTAL_ABI);
1259
- for (let i = 0; i < buyerWallets.length; i++) {
1260
- const buyer = buyerWallets[i];
1272
+ // 批量获取所有买家的账户信息(并行优化)
1273
+ const buyerAccountInfos = await this.aaManager.getMultipleAccountInfo(buyerWallets.map(w => w.address));
1274
+ const buyerSenders = buyerAccountInfos.map(ai => ai.sender);
1275
+ for (const ai of buyerAccountInfos) {
1276
+ nonceMap.init(ai.sender, ai.nonce);
1277
+ }
1278
+ // ✅ 预计算所有买家的金额和利润
1279
+ const buyerData = buyerWallets.map((buyer, i) => {
1261
1280
  const amountStr = buyAmounts[i] || '0';
1262
1281
  const originalWei = useNativeToken ? parseOkb(amountStr) : ethers.parseUnits(amountStr, quoteDecimals);
1263
1282
  let buyWei = originalWei;
@@ -1267,11 +1286,13 @@ export class BundleExecutor {
1267
1286
  buyWei = res.remaining;
1268
1287
  profitWei = res.profit;
1269
1288
  }
1270
- totalBuyWei += buyWei;
1271
- totalBuyProfitWei += profitWei;
1272
- const buyerAccount = await this.aaManager.getAccountInfo(buyer.address);
1273
- buyerSenders.push(buyerAccount.sender);
1274
- nonceMap.init(buyerAccount.sender, buyerAccount.nonce);
1289
+ return { buyer, buyWei, profitWei, accountInfo: buyerAccountInfos[i] };
1290
+ });
1291
+ const totalBuyWei = buyerData.reduce((sum, d) => sum + d.buyWei, 0n);
1292
+ const totalBuyProfitWei = buyerData.reduce((sum, d) => sum + d.profitWei, 0n);
1293
+ // ✅ 并行构建和签名所有买家的 UserOps
1294
+ const signedBuyOps = await mapWithConcurrency(buyerData, 5, async (data) => {
1295
+ const { buyer, buyWei, accountInfo } = data;
1275
1296
  const swapData = portalIface.encodeFunctionData('swapExactInput', [
1276
1297
  {
1277
1298
  inputToken,
@@ -1281,17 +1302,28 @@ export class BundleExecutor {
1281
1302
  permitData: '0x',
1282
1303
  },
1283
1304
  ]);
1305
+ // ✅ ERC20 代币需要先 approve,使用 executeBatch 将 approve + swap 合并为一个 UserOp
1306
+ let buyCallData;
1307
+ if (useNativeToken) {
1308
+ buyCallData = encodeExecute(this.portalAddress, buyWei, swapData);
1309
+ }
1310
+ else {
1311
+ const approveData = encodeApproveCall(this.portalAddress, buyWei);
1312
+ buyCallData = encodeExecuteBatch([inputToken, this.portalAddress], [0n, 0n], [approveData, swapData]);
1313
+ }
1284
1314
  const buyOp = await this.aaManager.buildUserOpWithFixedGas({
1285
1315
  ownerWallet: buyer,
1286
- sender: buyerAccount.sender,
1287
- callData: encodeExecute(this.portalAddress, useNativeToken ? buyWei : 0n, swapData),
1288
- nonce: nonceMap.next(buyerAccount.sender),
1289
- initCode: buyerAccount.deployed ? '0x' : this.aaManager.generateInitCode(buyer.address),
1290
- deployed: buyerAccount.deployed
1316
+ sender: accountInfo.sender,
1317
+ callData: buyCallData,
1318
+ nonce: nonceMap.next(accountInfo.sender),
1319
+ initCode: accountInfo.deployed ? '0x' : this.aaManager.generateInitCode(buyer.address),
1320
+ deployed: accountInfo.deployed
1291
1321
  });
1292
1322
  const signedBuyOp = await this.aaManager.signUserOp(buyOp.userOp, buyer);
1293
- ops.push(signedBuyOp.userOp);
1294
- }
1323
+ return signedBuyOp.userOp;
1324
+ });
1325
+ // 将所有签名的买入 UserOps 添加到操作列表
1326
+ ops.push(...signedBuyOps);
1295
1327
  // 利润由尾笔交易处理(不再在买入阶段追加利润转账 UserOp)
1296
1328
  // === 计算利润金额 ===
1297
1329
  let nativeProfitAmount = 0n;
@@ -1425,17 +1457,20 @@ export class BundleExecutor {
1425
1457
  });
1426
1458
  const signedCreateOp = await aaManager.signUserOp(createOpRes.userOp, payerWallet);
1427
1459
  ops1.push(signedCreateOp.userOp);
1428
- // --- 3. 构建内盘买入 Ops ---
1429
- for (let i = 0; i < curveBuyerWallets.length; i++) {
1430
- const wallet = curveBuyerWallets[i];
1431
- const info = curveBuyerInfos[i];
1460
+ // --- 3. 构建内盘买入 Ops(并行优化)---
1461
+ // 预计算所有买入金额
1462
+ const curveBuyData = curveBuyerWallets.map((wallet, i) => {
1432
1463
  const buyWei = parseOkb(curveBuyAmounts[i]);
1433
- totalCurveBuyWei += buyWei;
1464
+ const isLast = i === curveBuyerWallets.length - 1;
1465
+ const gasLimit = isLast ? 8000000n : 800000n; // 最后一笔触发毕业,给大一点
1466
+ return { wallet, info: curveBuyerInfos[i], buyWei, gasLimit };
1467
+ });
1468
+ totalCurveBuyWei = curveBuyData.reduce((sum, d) => sum + d.buyWei, 0n);
1469
+ // ✅ 并行构建和签名所有内盘买入 UserOps
1470
+ const signedCurveBuyOps = await mapWithConcurrency(curveBuyData, 5, async (data) => {
1471
+ const { wallet, info, buyWei, gasLimit } = data;
1434
1472
  const buyData = encodeBuyCall(tokenAddress, buyWei, 0n);
1435
1473
  const buyCallData = encodeExecute(FLAP_PORTAL, buyWei, buyData);
1436
- // 最后一笔买入通常触发毕业,GasLimit 给大一点 (8M)
1437
- const isLast = i === curveBuyerWallets.length - 1;
1438
- const gasLimit = isLast ? 8000000n : 800000n;
1439
1474
  const buyOpRes = await aaManager.buildUserOpWithFixedGas({
1440
1475
  ownerWallet: wallet,
1441
1476
  sender: info.sender,
@@ -1446,8 +1481,9 @@ export class BundleExecutor {
1446
1481
  fixedGas: { callGasLimit: gasLimit }
1447
1482
  });
1448
1483
  const signedBuyOp = await aaManager.signUserOp(buyOpRes.userOp, wallet);
1449
- ops1.push(signedBuyOp.userOp);
1450
- }
1484
+ return signedBuyOp.userOp;
1485
+ });
1486
+ ops1.push(...signedCurveBuyOps);
1451
1487
  // 签名第一个 handleOps
1452
1488
  const startNonce = params.payerStartNonce ?? (await provider.getTransactionCount(payerWallet.address, 'pending'));
1453
1489
  const signedMainTx = await this.signHandleOpsTx({
@@ -1467,40 +1503,32 @@ export class BundleExecutor {
1467
1503
  });
1468
1504
  const ops2 = [];
1469
1505
  const deadline = Math.floor(Date.now() / 1000) + 1200; // 20 min
1470
- for (let i = 0; i < dexBuyerWallets.length; i++) {
1471
- const wallet = dexBuyerWallets[i];
1472
- const info = dexBuyerInfos[i];
1506
+ // 预计算所有外盘买入金额
1507
+ const dexBuyData = dexBuyerWallets.map((wallet, i) => {
1473
1508
  const buyWei = parseOkb(dexBuyAmounts[i]);
1474
- totalDexBuyWei += buyWei;
1475
- // AA 模式外盘:Approve + Swap
1476
- // 1. Approve (PotatoSwap Router)
1509
+ return { wallet, info: dexBuyerInfos[i], buyWei };
1510
+ });
1511
+ totalDexBuyWei = dexBuyData.reduce((sum, d) => sum + d.buyWei, 0n);
1512
+ // ✅ 并行构建和签名所有外盘买入 UserOps
1513
+ const signedDexBuyOps = await mapWithConcurrency(dexBuyData, 5, async (data) => {
1514
+ const { wallet, info, buyWei } = data;
1515
+ // AA 模式外盘:使用 executeBatch 将 Approve + Swap 合并为一个 UserOp
1477
1516
  const approveData = encodeApproveCall(POTATOSWAP_V2_ROUTER);
1478
- const approveCallData = encodeExecute(tokenAddress, 0n, approveData);
1479
- const approveOpRes = await aaManager.buildUserOpWithFixedGas({
1480
- ownerWallet: wallet,
1481
- sender: info.sender,
1482
- callData: approveCallData,
1483
- nonce: nonceMap.next(info.sender),
1484
- initCode: '0x', // HandleOps1 已部署
1485
- deployed: true,
1486
- fixedGas: { callGasLimit: 100000n }
1487
- });
1488
- const signedApprove = await aaManager.signUserOp(approveOpRes.userOp, wallet);
1489
- ops2.push(signedApprove.userOp);
1490
- // 2. Swap (PotatoSwap V2)
1491
1517
  const swapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], info.sender, deadline);
1492
- const swapCallData = encodeExecute(POTATOSWAP_V2_ROUTER, buyWei, swapData);
1493
- const swapOpRes = await aaManager.buildUserOpWithFixedGas({
1518
+ const batchCallData = encodeExecuteBatch([tokenAddress, POTATOSWAP_V2_ROUTER], [0n, buyWei], [approveData, swapData]);
1519
+ const dexBuyOpRes = await aaManager.buildUserOpWithFixedGas({
1494
1520
  ownerWallet: wallet,
1495
1521
  sender: info.sender,
1496
- callData: swapCallData,
1522
+ callData: batchCallData,
1497
1523
  nonce: nonceMap.next(info.sender),
1524
+ initCode: '0x', // HandleOps1 已部署
1498
1525
  deployed: true,
1499
- fixedGas: { callGasLimit: 500000n }
1526
+ fixedGas: { callGasLimit: 600000n }
1500
1527
  });
1501
- const signedSwap = await aaManager.signUserOp(swapOpRes.userOp, wallet);
1502
- ops2.push(signedSwap.userOp);
1503
- }
1528
+ const signedDexBuyOp = await aaManager.signUserOp(dexBuyOpRes.userOp, wallet);
1529
+ return signedDexBuyOp.userOp;
1530
+ });
1531
+ ops2.push(...signedDexBuyOps);
1504
1532
  const signedDexTx = await this.signHandleOpsTx({
1505
1533
  ops: ops2,
1506
1534
  payerWallet: payerWallet,
@@ -24,6 +24,10 @@ export declare const FLAP_PORTAL = "0xb30D8c4216E1f21F27444D2FfAee3ad577808678";
24
24
  export declare const FLAP_TOKEN_IMPL = "0x12Dc83157Bf1cfCB8Db5952b3ba5bb56Cc38f8C9";
25
25
  /** WOKB 原生包装代币 */
26
26
  export declare const WOKB = "0xe538905cf8410324e03a5a23c1c177a474d59b2b";
27
+ /** ✅ USDT 代币地址(XLayer,6 位精度) */
28
+ export declare const USDT = "0x1e4a5963abfd975d8c9021ce480b42188849d41d";
29
+ /** ✅ 零地址(表示原生代币) */
30
+ export declare const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
27
31
  /** Multicall3 合约 */
28
32
  export declare const MULTICALL3 = "0xca11bde05977b3631167028862be2a173976ca11";
29
33
  /** PotatoSwap V2 Router */
@@ -34,14 +38,10 @@ export declare const POTATOSWAP_SWAP_ROUTER02 = "0xB45D0149249488333E3F3f9F35980
34
38
  export declare const POTATOSWAP_V3_ROUTER = "0xBB069e9465BcabC4F488d21e793BDEf0F2d41D41";
35
39
  /** PotatoSwap V3 Factory */
36
40
  export declare const POTATOSWAP_V3_FACTORY = "0xa1415fAe79c4B196d087F02b8aD5a622B8A827E5";
37
- /** USDT (6位精度) */
38
- export declare const USDT = "0x1e4a5963abfd975d8c9021ce480b42188849d41d";
39
41
  /** USDC (6位精度) */
40
42
  export declare const USDC = "0x74b7f16337b8972027f6196a17a631ac6de26d22";
41
43
  /** USD₮0 / USDT0 (6位精度) - Flap xLayer 支持的稳定币计价 */
42
44
  export declare const USDT0 = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
43
- /** 零地址(表示原生代币 OKB) */
44
- export declare const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
45
45
  /** 默认 Gas Price 兜底值(0.1 gwei) */
46
46
  export declare const DEFAULT_GAS_PRICE = 100000000n;
47
47
  /** 默认 Salt(用于 AA 账户派生) */
@@ -36,6 +36,10 @@ export const FLAP_TOKEN_IMPL = '0x12Dc83157Bf1cfCB8Db5952b3ba5bb56Cc38f8C9';
36
36
  // ============================================================================
37
37
  /** WOKB 原生包装代币 */
38
38
  export const WOKB = '0xe538905cf8410324e03a5a23c1c177a474d59b2b';
39
+ /** ✅ USDT 代币地址(XLayer,6 位精度) */
40
+ export const USDT = '0x1e4a5963abfd975d8c9021ce480b42188849d41d';
41
+ /** ✅ 零地址(表示原生代币) */
42
+ export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
39
43
  /** Multicall3 合约 */
40
44
  export const MULTICALL3 = '0xca11bde05977b3631167028862be2a173976ca11';
41
45
  /** PotatoSwap V2 Router */
@@ -46,21 +50,11 @@ export const POTATOSWAP_SWAP_ROUTER02 = '0xB45D0149249488333E3F3f9F359807F4b810C
46
50
  export const POTATOSWAP_V3_ROUTER = '0xBB069e9465BcabC4F488d21e793BDEf0F2d41D41';
47
51
  /** PotatoSwap V3 Factory */
48
52
  export const POTATOSWAP_V3_FACTORY = '0xa1415fAe79c4B196d087F02b8aD5a622B8A827E5';
49
- // ============================================================================
50
- // 稳定币地址
51
- // ============================================================================
52
- /** USDT (6位精度) */
53
- export const USDT = '0x1e4a5963abfd975d8c9021ce480b42188849d41d';
54
53
  /** USDC (6位精度) */
55
54
  export const USDC = '0x74b7f16337b8972027f6196a17a631ac6de26d22';
56
55
  /** USD₮0 / USDT0 (6位精度) - Flap xLayer 支持的稳定币计价 */
57
56
  export const USDT0 = '0x779ded0c9e1022225f8e0630b35a9b54be713736';
58
57
  // ============================================================================
59
- // 零地址
60
- // ============================================================================
61
- /** 零地址(表示原生代币 OKB) */
62
- export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
63
- // ============================================================================
64
58
  // Gas 配置
65
59
  // ============================================================================
66
60
  /** 默认 Gas Price 兜底值(0.1 gwei) */
@@ -1,10 +1,6 @@
1
1
  /**
2
2
  * XLayer AA Buy-First(外盘 / PotatoSwap V2)
3
- *
4
- * 对齐 BSC buy-first 思路:
5
- * - 买方先买入(OKB -> token)
6
- * - 卖方再卖出等值 token(token -> OKB,卖方需要预持仓)
7
- * - ✅ 利润:在卖出后归集阶段刮取
3
+ * ✅ 优化版本:并行化 RPC、批量签名、合并 approve+sell
8
4
  */
9
5
  import type { XLayerConfig } from './types.js';
10
6
  import type { BuyFirstParams, BuyFirstResult } from './types.js';
@@ -14,11 +10,11 @@ export declare class AADexBuyFirstExecutor {
14
10
  private dexQuery;
15
11
  private config;
16
12
  constructor(config?: XLayerConfig);
17
- private getEffectiveRouter;
18
13
  private runHandleOps;
19
14
  private pickWallets;
20
15
  private buildWithdrawUserOp;
21
16
  private safeQuoteBuy;
17
+ private initNonceAndInitCode;
22
18
  execute(params: BuyFirstParams): Promise<BuyFirstResult>;
23
19
  }
24
20
  export declare function createAADexBuyFirstExecutor(config?: XLayerConfig): AADexBuyFirstExecutor;
@@ -1,72 +1,21 @@
1
1
  /**
2
2
  * XLayer AA Buy-First(外盘 / PotatoSwap V2)
3
- *
4
- * 对齐 BSC buy-first 思路:
5
- * - 买方先买入(OKB -> token)
6
- * - 卖方再卖出等值 token(token -> OKB,卖方需要预持仓)
7
- * - ✅ 利润:在卖出后归集阶段刮取
3
+ * ✅ 优化版本:并行化 RPC、批量签名、合并 approve+sell
8
4
  */
9
5
  import { Contract, Interface, Wallet, ethers } from 'ethers';
10
6
  import { AANonceMap } from './types.js';
11
- import { ENTRYPOINT_ABI, DEFAULT_WITHDRAW_RESERVE, POTATOSWAP_V2_ROUTER, WOKB, } from './constants.js';
7
+ import { ENTRYPOINT_ABI, DEFAULT_WITHDRAW_RESERVE, POTATOSWAP_V2_ROUTER, WOKB, ZERO_ADDRESS, } from './constants.js';
12
8
  import { AAAccountManager, encodeExecute } from './aa-account.js';
13
- import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee } from './dex.js';
14
- import { PortalQuery, encodeApproveCall, parseOkb } from './portal-ops.js';
9
+ import { DexQuery } from './dex.js';
10
+ import { PortalQuery, parseOkb } from './portal-ops.js';
15
11
  import { mapWithConcurrency } from '../utils/concurrency.js';
16
- import { PROFIT_CONFIG } from '../utils/constants.js';
17
- // 固定 gas(用于减少 RPC 与提高可控性)
12
+ import { getDexDeadline, resolveProfitSettings, calculateProfitWei, splitAmount, getEffectiveRouter } from './dex-helpers.js';
13
+ import { fetchAccountInfoBatch } from './account-fetcher.js';
14
+ import { buildBuyUserOps, buildSellUserOps } from './userOp-builder.js';
18
15
  const DEFAULT_CALL_GAS_LIMIT_BUY = 450000n;
19
16
  const DEFAULT_CALL_GAS_LIMIT_SELL = 450000n;
20
17
  const DEFAULT_CALL_GAS_LIMIT_APPROVE = 200000n;
21
18
  const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
22
- function getDexDeadline(minutes = 20) {
23
- return Math.floor(Date.now() / 1000) + minutes * 60;
24
- }
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
- function splitAmount(totalAmount, count) {
48
- if (count <= 0)
49
- throw new Error('拆分份数必须大于 0');
50
- if (count === 1)
51
- return [totalAmount];
52
- if (totalAmount <= 0n)
53
- return new Array(count).fill(0n);
54
- const weights = [];
55
- for (let i = 0; i < count; i++) {
56
- const w = BigInt(500000 + Math.floor(Math.random() * 1000000)); // [0.5,1.5)
57
- weights.push(w);
58
- }
59
- const totalWeight = weights.reduce((a, b) => a + b, 0n);
60
- const amounts = [];
61
- let allocated = 0n;
62
- for (let i = 0; i < count - 1; i++) {
63
- const amt = (totalAmount * weights[i]) / totalWeight;
64
- amounts.push(amt);
65
- allocated += amt;
66
- }
67
- amounts.push(totalAmount - allocated);
68
- return shuffle(amounts);
69
- }
70
19
  export class AADexBuyFirstExecutor {
71
20
  constructor(config = {}) {
72
21
  this.config = config;
@@ -74,14 +23,6 @@ export class AADexBuyFirstExecutor {
74
23
  this.portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
75
24
  this.dexQuery = new DexQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
76
25
  }
77
- getEffectiveRouter(params) {
78
- if (params.routerAddress)
79
- return params.routerAddress;
80
- if (params.dexKey === 'DYORSWAP') {
81
- return '0xfb001fbbace32f09cb6d3c449b935183de53ee96';
82
- }
83
- return POTATOSWAP_V2_ROUTER;
84
- }
85
26
  async runHandleOps(label, ops, bundlerSigner, beneficiary) {
86
27
  const provider = this.aaManager.getProvider();
87
28
  const entryPointAddress = this.aaManager.getEntryPointAddress();
@@ -153,9 +94,8 @@ export class AADexBuyFirstExecutor {
153
94
  : 0n;
154
95
  if (withdrawAmount <= 0n)
155
96
  return null;
156
- const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effConfig);
97
+ const { extractProfit, profitBps } = resolveProfitSettings(effConfig);
157
98
  const profitWei = extractProfit ? calculateProfitWei(withdrawAmount, profitBps) : 0n;
158
- const toOwnerWei = withdrawAmount - profitWei;
159
99
  const callData = encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
160
100
  const { userOp } = gasPolicy === 'fixed'
161
101
  ? await this.aaManager.buildUserOpWithFixedGas({
@@ -189,12 +129,49 @@ export class AADexBuyFirstExecutor {
189
129
  return 0n;
190
130
  }
191
131
  }
132
+ initNonceAndInitCode(params) {
133
+ const { buyers, sellers, buyerAis, sellerAis } = params;
134
+ const nonceMap = new AANonceMap();
135
+ // ✅ 使用 Set 去重,避免重复初始化
136
+ const uniqueSenders = new Set();
137
+ for (const ai of [...buyerAis, ...sellerAis]) {
138
+ const senderLower = ai.sender.toLowerCase();
139
+ if (!uniqueSenders.has(senderLower)) {
140
+ nonceMap.init(ai.sender, ai.nonce);
141
+ uniqueSenders.add(senderLower);
142
+ }
143
+ }
144
+ const initCodeBySenderLower = new Map();
145
+ const initIfNeeded = (sender, deployed, ownerAddress) => {
146
+ const k = sender.toLowerCase();
147
+ if (initCodeBySenderLower.has(k))
148
+ return;
149
+ initCodeBySenderLower.set(k, deployed ? '0x' : this.aaManager.generateInitCode(ownerAddress));
150
+ };
151
+ const consumeInitCode = (sender) => {
152
+ const k = sender.toLowerCase();
153
+ const cur = initCodeBySenderLower.get(k);
154
+ if (cur === undefined)
155
+ throw new Error(`initCode not initialized for sender: ${sender}`);
156
+ if (cur !== '0x')
157
+ initCodeBySenderLower.set(k, '0x');
158
+ return cur;
159
+ };
160
+ for (let i = 0; i < buyers.length; i++)
161
+ initIfNeeded(buyerAis[i].sender, buyerAis[i].deployed, buyers[i].address);
162
+ for (let i = 0; i < sellers.length; i++)
163
+ initIfNeeded(sellerAis[i].sender, sellerAis[i].deployed, sellers[i].address);
164
+ return { nonceMap, consumeInitCode };
165
+ }
192
166
  async execute(params) {
193
- const { tradeType = 'V2', dexKey, routerAddress, deadlineMinutes = 20, tokenAddress, buyerPrivateKeys, sellerPrivateKeys, buyerFunds, buyCount, sellCount, slippageBps = 0, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, } = params;
167
+ const { tradeType = 'V2', routerVersion = 'V2', // 新增
168
+ dexKey, routerAddress, deadlineMinutes = 20, tokenAddress, buyerPrivateKeys, sellerPrivateKeys, buyerFunds, buyCount, sellCount, slippageBps = 0, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, quoteToken, // ✅ 新增
169
+ quoteTokenDecimals = 18, // ✅ 新增
170
+ } = params;
194
171
  if (tradeType !== 'V2') {
195
172
  throw new Error('AADexBuyFirstExecutor 仅支持 tradeType=V2(外盘)');
196
173
  }
197
- const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress });
174
+ const effectiveRouter = getEffectiveRouter({ dexKey, routerAddress, routerVersion }, POTATOSWAP_V2_ROUTER);
198
175
  const deadline = getDexDeadline(deadlineMinutes);
199
176
  const buyers = this.pickWallets(buyerPrivateKeys, buyCount);
200
177
  const sellers = this.pickWallets(sellerPrivateKeys, sellCount);
@@ -204,49 +181,60 @@ export class AADexBuyFirstExecutor {
204
181
  throw new Error('sellCount=0 或 sellerPrivateKeys 为空');
205
182
  const effConfig = { ...(this.config ?? {}), ...(config ?? {}) };
206
183
  const profitSettings = resolveProfitSettings(effConfig);
207
- const totalBuyWei = parseOkb(String(buyerFunds));
184
+ const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
185
+ // ✅ 解析买入金额(根据 quoteToken 精度)
186
+ const totalBuyWei = useNativeToken
187
+ ? parseOkb(String(buyerFunds))
188
+ : ethers.parseUnits(String(buyerFunds), quoteTokenDecimals);
208
189
  if (totalBuyWei <= 0n)
209
190
  throw new Error('buyerFunds 需要 > 0');
210
191
  const buyAmountsWei = splitAmount(totalBuyWei, buyers.length);
211
- const quotedPerBuy = await mapWithConcurrency(buyAmountsWei, 6, async (amt) => this.safeQuoteBuy(tokenAddress, amt));
192
+ // 并行报价(支持 OKB USDT)
193
+ const quotedPerBuy = await mapWithConcurrency(buyAmountsWei, 6, async (amt) => {
194
+ if (useNativeToken) {
195
+ return await this.safeQuoteBuy(tokenAddress, amt);
196
+ }
197
+ else {
198
+ // USDT → Token 报价
199
+ try {
200
+ return await this.dexQuery.quoteTokenToToken(quoteToken, tokenAddress, amt);
201
+ }
202
+ catch {
203
+ return 0n;
204
+ }
205
+ }
206
+ });
212
207
  const quotedTotalTokenOut = quotedPerBuy.reduce((a, b) => a + (b ?? 0n), 0n);
213
208
  const estimatedTokenOutWei = (quotedTotalTokenOut * BigInt(10000 - Math.max(0, Math.min(10000, slippageBps)))) / 10000n;
214
209
  if (estimatedTokenOutWei <= 0n) {
215
210
  throw new Error('买入报价为 0,无法规划 sellAmount(可能无流动性)');
216
211
  }
217
212
  const sellAmountsWei = splitAmount(estimatedTokenOutWei, sellers.length);
218
- // account infos
219
- const buyerAis = await this.aaManager.getMultipleAccountInfo(buyers.map((w) => w.address));
220
- const sellerAis = await this.aaManager.getMultipleAccountInfo(sellers.map((w) => w.address));
221
- const nonceMap = new AANonceMap();
222
- for (const ai of buyerAis)
223
- nonceMap.init(ai.sender, ai.nonce);
224
- for (const ai of sellerAis)
225
- nonceMap.init(ai.sender, ai.nonce);
226
- // initCode:确保同一 sender 只在第一笔 op 携带
227
- const initCodeBySenderLower = new Map();
228
- const initIfNeeded = (sender, deployed, ownerAddress) => {
229
- const k = sender.toLowerCase();
230
- if (initCodeBySenderLower.has(k))
231
- return;
232
- initCodeBySenderLower.set(k, deployed ? '0x' : this.aaManager.generateInitCode(ownerAddress));
233
- };
234
- const consumeInitCode = (sender) => {
235
- const k = sender.toLowerCase();
236
- const cur = initCodeBySenderLower.get(k);
237
- if (cur === undefined)
238
- throw new Error(`initCode not initialized for sender: ${sender}`);
239
- if (cur !== '0x')
240
- initCodeBySenderLower.set(k, '0x');
241
- return cur;
242
- };
243
- for (let i = 0; i < buyers.length; i++)
244
- initIfNeeded(buyerAis[i].sender, buyerAis[i].deployed, buyers[i].address);
245
- for (let i = 0; i < sellers.length; i++)
246
- initIfNeeded(sellerAis[i].sender, sellerAis[i].deployed, sellers[i].address);
247
- // 卖方预持仓检查
248
- const sellerSenders = sellerAis.map((ai) => ai.sender);
249
- const sellerTokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, sellerSenders);
213
+ // 优化 1: 并行获取所有账户信息
214
+ const accountInfo = await fetchAccountInfoBatch({
215
+ buyers,
216
+ sellers,
217
+ tokenAddress,
218
+ effectiveRouter,
219
+ aaManager: this.aaManager,
220
+ portalQuery: this.portalQuery
221
+ });
222
+ const { buyerAis, sellerAis, sellerSenders, sellerTokenBalances, sellerAllowances } = accountInfo;
223
+ // 验证买方余额(OKB USDT)
224
+ if (!useNativeToken) {
225
+ const buyerSenders = buyerAis.map(ai => ai.sender);
226
+ const buyerQuoteBalances = await this.portalQuery.getMultipleTokenBalances(quoteToken, buyerSenders);
227
+ for (let i = 0; i < buyers.length; i++) {
228
+ const sender = buyerSenders[i];
229
+ const needBuy = buyAmountsWei[i] ?? 0n;
230
+ const bal = buyerQuoteBalances.get(sender) ?? 0n;
231
+ if (needBuy > bal) {
232
+ const tokenName = quoteTokenDecimals === 6 ? 'USDT' : 'ERC20';
233
+ throw new Error(`买方 ${tokenName} 余额不足: sender=${sender} need=${needBuy.toString()} bal=${bal.toString()}`);
234
+ }
235
+ }
236
+ }
237
+ // 验证卖方余额
250
238
  for (let i = 0; i < sellers.length; i++) {
251
239
  const sender = sellerSenders[i];
252
240
  const needSell = sellAmountsWei[i] ?? 0n;
@@ -255,80 +243,41 @@ export class AADexBuyFirstExecutor {
255
243
  throw new Error(`卖方预持仓不足: sender=${sender} need=${needSell.toString()} bal=${bal.toString()}`);
256
244
  }
257
245
  }
258
- // allowance(spender=router)
259
- const sellerAllowances = await this.portalQuery.getMultipleAllowances(tokenAddress, sellerSenders, effectiveRouter);
260
- const ops = [];
261
- // buy ops
262
- for (let i = 0; i < buyers.length; i++) {
263
- const w = buyers[i];
264
- const ai = buyerAis[i];
265
- const sender = ai.sender;
266
- const buyWei = buyAmountsWei[i] ?? 0n;
267
- if (buyWei <= 0n)
268
- continue;
269
- const swapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], sender, deadline);
270
- const callData = encodeExecute(effectiveRouter, buyWei, swapData);
271
- const { userOp, prefundWei } = await this.aaManager.buildUserOpWithFixedGas({
272
- ownerWallet: w,
273
- sender,
274
- nonce: nonceMap.next(sender),
275
- initCode: consumeInitCode(sender),
276
- callData,
277
- deployed: ai.deployed,
278
- fixedGas: {
279
- ...(effConfig.fixedGas ?? {}),
280
- callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_BUY,
281
- },
282
- });
283
- await this.aaManager.ensureSenderBalance(w, sender, buyWei + prefundWei + parseOkb('0.0001'), `buyFirstDex/buyer${i + 1}/fund`);
284
- const signed = await this.aaManager.signUserOp(userOp, w);
285
- ops.push(signed.userOp);
286
- }
287
- // seller approve + sell ops
288
- for (let i = 0; i < sellers.length; i++) {
289
- const w = sellers[i];
290
- const ai = sellerAis[i];
291
- const sender = ai.sender;
292
- const sellWei = sellAmountsWei[i] ?? 0n;
293
- if (sellWei <= 0n)
294
- continue;
295
- const allowance = sellerAllowances.get(sender) ?? 0n;
296
- if (allowance < sellWei) {
297
- const approveCallData = encodeExecute(tokenAddress, 0n, encodeApproveCall(effectiveRouter, ethers.MaxUint256));
298
- const { userOp: approveOp, prefundWei } = await this.aaManager.buildUserOpWithFixedGas({
299
- ownerWallet: w,
300
- sender,
301
- nonce: nonceMap.next(sender),
302
- initCode: consumeInitCode(sender),
303
- callData: approveCallData,
304
- deployed: ai.deployed,
305
- fixedGas: {
306
- ...(effConfig.fixedGas ?? {}),
307
- callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_APPROVE,
308
- },
309
- });
310
- await this.aaManager.ensureSenderBalance(w, sender, prefundWei + parseOkb('0.0001'), `buyFirstDex/seller${i + 1}/approve-fund`);
311
- const signedApprove = await this.aaManager.signUserOp(approveOp, w);
312
- ops.push(signedApprove.userOp);
313
- }
314
- const sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellWei, 0n, [tokenAddress, WOKB], sender, deadline);
315
- const sellCallData = encodeExecute(effectiveRouter, 0n, sellSwapData);
316
- const { userOp: sellOp, prefundWei: sellPrefund } = await this.aaManager.buildUserOpWithFixedGas({
317
- ownerWallet: w,
318
- sender,
319
- nonce: nonceMap.next(sender),
320
- initCode: consumeInitCode(sender),
321
- callData: sellCallData,
322
- deployed: ai.deployed,
323
- fixedGas: {
324
- ...(effConfig.fixedGas ?? {}),
325
- callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_SELL,
326
- },
327
- });
328
- await this.aaManager.ensureSenderBalance(w, sender, sellPrefund + parseOkb('0.0001'), `buyFirstDex/seller${i + 1}/sell-fund`);
329
- const signedSell = await this.aaManager.signUserOp(sellOp, w);
330
- ops.push(signedSell.userOp);
331
- }
246
+ const { nonceMap, consumeInitCode } = this.initNonceAndInitCode({ buyers, sellers, buyerAis, sellerAis });
247
+ // 优化 2 + 3: 并行构建买入和卖出 UserOp(卖出合并 approve+sell)
248
+ const [buyOps, sellOps] = await Promise.all([
249
+ buildBuyUserOps({
250
+ buyers,
251
+ buyerAis,
252
+ buyAmountsWei,
253
+ tokenAddress,
254
+ effectiveRouter,
255
+ deadline,
256
+ wokbAddress: WOKB,
257
+ nonceMap,
258
+ consumeInitCode,
259
+ aaManager: this.aaManager,
260
+ config: effConfig,
261
+ defaultCallGasLimit: DEFAULT_CALL_GAS_LIMIT_BUY
262
+ }),
263
+ buildSellUserOps({
264
+ sellers,
265
+ sellerAis,
266
+ sellAmountsWei,
267
+ tokenAddress,
268
+ effectiveRouter,
269
+ deadline,
270
+ wokbAddress: WOKB,
271
+ sellerAllowances,
272
+ nonceMap,
273
+ consumeInitCode,
274
+ aaManager: this.aaManager,
275
+ config: effConfig,
276
+ defaultApproveGasLimit: DEFAULT_CALL_GAS_LIMIT_APPROVE,
277
+ defaultSellGasLimit: DEFAULT_CALL_GAS_LIMIT_SELL
278
+ })
279
+ ]);
280
+ const ops = [...buyOps, ...sellOps];
332
281
  if (ops.length === 0)
333
282
  throw new Error('本轮没有生成任何 UserOp');
334
283
  const payerWallet = buyers[0];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * XLayer AA DEX 工具函数
3
+ */
4
+ export declare function getDexDeadline(minutes?: number): number;
5
+ export declare function calculateProfitWei(amountWei: bigint, profitBps: number): bigint;
6
+ export declare function resolveProfitSettings(config?: {
7
+ extractProfit?: boolean;
8
+ profitBps?: number;
9
+ profitRecipient?: string;
10
+ }): {
11
+ extractProfit: boolean;
12
+ profitBps: number;
13
+ profitRecipient: string;
14
+ };
15
+ export declare function splitAmount(totalAmount: bigint, count: number): bigint[];
16
+ export declare function getEffectiveRouter(params: {
17
+ dexKey?: string;
18
+ routerAddress?: string;
19
+ routerVersion?: 'V2' | 'V3';
20
+ }, defaultRouter: string): string;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * XLayer AA DEX 工具函数
3
+ */
4
+ import { PROFIT_CONFIG } from '../utils/constants.js';
5
+ export function getDexDeadline(minutes = 20) {
6
+ return Math.floor(Date.now() / 1000) + minutes * 60;
7
+ }
8
+ export function calculateProfitWei(amountWei, profitBps) {
9
+ if (amountWei <= 0n)
10
+ return 0n;
11
+ if (!Number.isFinite(profitBps) || profitBps <= 0)
12
+ return 0n;
13
+ return (amountWei * BigInt(profitBps)) / 10000n;
14
+ }
15
+ export function resolveProfitSettings(config) {
16
+ const extractProfit = config?.extractProfit !== false;
17
+ const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
18
+ const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
19
+ const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
20
+ return { extractProfit, profitBps, profitRecipient };
21
+ }
22
+ function shuffle(arr) {
23
+ const a = arr.slice();
24
+ for (let i = a.length - 1; i > 0; i--) {
25
+ const j = Math.floor(Math.random() * (i + 1));
26
+ [a[i], a[j]] = [a[j], a[i]];
27
+ }
28
+ return a;
29
+ }
30
+ export function splitAmount(totalAmount, count) {
31
+ if (count <= 0)
32
+ throw new Error('拆分份数必须大于 0');
33
+ if (count === 1)
34
+ return [totalAmount];
35
+ if (totalAmount <= 0n)
36
+ return new Array(count).fill(0n);
37
+ const weights = [];
38
+ for (let i = 0; i < count; i++) {
39
+ const w = BigInt(500000 + Math.floor(Math.random() * 1000000));
40
+ weights.push(w);
41
+ }
42
+ const totalWeight = weights.reduce((a, b) => a + b, 0n);
43
+ const amounts = [];
44
+ let allocated = 0n;
45
+ for (let i = 0; i < count - 1; i++) {
46
+ const amt = (totalAmount * weights[i]) / totalWeight;
47
+ amounts.push(amt);
48
+ allocated += amt;
49
+ }
50
+ amounts.push(totalAmount - allocated);
51
+ return shuffle(amounts);
52
+ }
53
+ export function getEffectiveRouter(params, defaultRouter) {
54
+ // 优先使用显式指定的地址
55
+ if (params.routerAddress)
56
+ return params.routerAddress;
57
+ // 根据 dexKey 选择
58
+ if (params.dexKey === 'DYORSWAP') {
59
+ return '0xfb001fbbace32f09cb6d3c449b935183de53ee96';
60
+ }
61
+ // 根据 routerVersion 选择
62
+ if (params.routerVersion === 'V3') {
63
+ return '0xBB069e9465BcabC4F488d21e793BDEf0F2d41D41'; // POTATOSWAP_V3_ROUTER
64
+ }
65
+ // 默认使用传入的 defaultRouter(通常是 V2)
66
+ return defaultRouter;
67
+ }
@@ -64,6 +64,10 @@ export declare class DexQuery {
64
64
  * 获取 Token -> OKB 的预期输出
65
65
  */
66
66
  quoteTokenToOkb(tokenAmount: bigint, tokenAddress: string): Promise<bigint>;
67
+ /**
68
+ * ✅ 获取 Token → Token 的预期输出(支持 USDT → Token)
69
+ */
70
+ quoteTokenToToken(inputToken: string, outputToken: string, inputAmount: bigint): Promise<bigint>;
67
71
  /**
68
72
  * 获取 WOKB 地址
69
73
  */
@@ -129,6 +129,14 @@ export class DexQuery {
129
129
  const amounts = await this.getAmountsOut(tokenAmount, path);
130
130
  return amounts[amounts.length - 1];
131
131
  }
132
+ /**
133
+ * ✅ 获取 Token → Token 的预期输出(支持 USDT → Token)
134
+ */
135
+ async quoteTokenToToken(inputToken, outputToken, inputAmount) {
136
+ const path = [inputToken, outputToken];
137
+ const amounts = await this.getAmountsOut(inputAmount, path);
138
+ return amounts[amounts.length - 1];
139
+ }
132
140
  /**
133
141
  * 获取 WOKB 地址
134
142
  */
@@ -0,0 +1,38 @@
1
+ /**
2
+ * XLayer Router 管理器
3
+ * 统一管理 V2/V3 Router 地址和授权目标选择逻辑
4
+ */
5
+ import type { RouterVersion, ApprovalTarget } from './types.js';
6
+ /**
7
+ * ✅ 获取 Router 地址
8
+ * @param params.routerVersion - Router 版本(V2 或 V3)
9
+ * @param params.dexKey - DEX 标识(如 DYORSWAP)
10
+ * @param params.routerAddress - 显式指定的 Router 地址(优先级最高)
11
+ * @returns Router 地址
12
+ */
13
+ export declare function getRouterAddress(params: {
14
+ routerVersion?: RouterVersion;
15
+ dexKey?: string;
16
+ routerAddress?: string;
17
+ }): string;
18
+ /**
19
+ * ✅ 获取授权目标地址
20
+ * @param target - 授权目标类型
21
+ * @returns 目标合约地址
22
+ */
23
+ export declare function getApprovalTargetAddress(target: ApprovalTarget): string;
24
+ /**
25
+ * ✅ 获取所有授权目标地址
26
+ * @param targets - 授权目标类型数组
27
+ * @returns 目标信息数组
28
+ */
29
+ export declare function getAllApprovalTargets(targets: ApprovalTarget[]): Array<{
30
+ target: ApprovalTarget;
31
+ address: string;
32
+ }>;
33
+ /**
34
+ * ✅ 根据交易类型推断授权目标
35
+ * @param tradeType - 交易类型(FLAP/V2/V3)
36
+ * @returns 授权目标类型
37
+ */
38
+ export declare function inferApprovalTarget(tradeType: string): ApprovalTarget;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * XLayer Router 管理器
3
+ * 统一管理 V2/V3 Router 地址和授权目标选择逻辑
4
+ */
5
+ import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, FLAP_PORTAL, } from './constants.js';
6
+ /**
7
+ * ✅ 获取 Router 地址
8
+ * @param params.routerVersion - Router 版本(V2 或 V3)
9
+ * @param params.dexKey - DEX 标识(如 DYORSWAP)
10
+ * @param params.routerAddress - 显式指定的 Router 地址(优先级最高)
11
+ * @returns Router 地址
12
+ */
13
+ export function getRouterAddress(params) {
14
+ // 1. 优先使用显式指定的地址
15
+ if (params.routerAddress)
16
+ return params.routerAddress;
17
+ // 2. 根据 dexKey 选择
18
+ if (params.dexKey === 'DYORSWAP') {
19
+ return '0xfb001fbbace32f09cb6d3c449b935183de53ee96';
20
+ }
21
+ // 3. 根据 routerVersion 选择
22
+ const version = params.routerVersion ?? 'V2';
23
+ if (version === 'V3') {
24
+ return POTATOSWAP_V3_ROUTER;
25
+ }
26
+ // 4. 默认 V2
27
+ return POTATOSWAP_V2_ROUTER;
28
+ }
29
+ /**
30
+ * ✅ 获取授权目标地址
31
+ * @param target - 授权目标类型
32
+ * @returns 目标合约地址
33
+ */
34
+ export function getApprovalTargetAddress(target) {
35
+ switch (target) {
36
+ case 'FLAP_PORTAL':
37
+ return FLAP_PORTAL;
38
+ case 'V2_ROUTER':
39
+ return POTATOSWAP_V2_ROUTER;
40
+ case 'V3_ROUTER':
41
+ return POTATOSWAP_V3_ROUTER;
42
+ default:
43
+ throw new Error(`Unknown approval target: ${target}`);
44
+ }
45
+ }
46
+ /**
47
+ * ✅ 获取所有授权目标地址
48
+ * @param targets - 授权目标类型数组
49
+ * @returns 目标信息数组
50
+ */
51
+ export function getAllApprovalTargets(targets) {
52
+ return targets.map(target => ({
53
+ target,
54
+ address: getApprovalTargetAddress(target)
55
+ }));
56
+ }
57
+ /**
58
+ * ✅ 根据交易类型推断授权目标
59
+ * @param tradeType - 交易类型(FLAP/V2/V3)
60
+ * @returns 授权目标类型
61
+ */
62
+ export function inferApprovalTarget(tradeType) {
63
+ const type = String(tradeType || '').toUpperCase();
64
+ if (type === 'V2')
65
+ return 'V2_ROUTER';
66
+ if (type === 'V3')
67
+ return 'V3_ROUTER';
68
+ return 'FLAP_PORTAL'; // 默认内盘
69
+ }
@@ -661,8 +661,14 @@ export interface VolumeResult {
661
661
  totalVolume: bigint;
662
662
  }
663
663
  export type BuyFirstTradeType = 'FLAP' | 'V2';
664
+ /** ✅ Router 版本类型 */
665
+ export type RouterVersion = 'V2' | 'V3';
666
+ /** ✅ 授权目标类型 */
667
+ export type ApprovalTarget = 'FLAP_PORTAL' | 'V2_ROUTER' | 'V3_ROUTER';
664
668
  export interface BuyFirstParams {
665
669
  tradeType?: BuyFirstTradeType;
670
+ /** ✅ 新增:Router 版本(V2 或 V3,默认 V2) */
671
+ routerVersion?: RouterVersion;
666
672
  /** DEX 标识(仅 V2 时使用,用于选择 Router) */
667
673
  dexKey?: string;
668
674
  /** 显式指定 Router 地址(仅 V2 时使用) */
@@ -672,10 +678,14 @@ export interface BuyFirstParams {
672
678
  tokenAddress: string;
673
679
  buyerPrivateKeys: string[];
674
680
  sellerPrivateKeys: string[];
675
- /** 本轮总买入资金(OKB) */
681
+ /** 本轮总买入资金(OKB 或 quoteToken) */
676
682
  buyerFunds: string;
677
683
  buyCount?: number;
678
684
  sellCount?: number;
685
+ /** ✅ 新增:买入使用的代币地址(零地址或不传表示使用 OKB,非零地址表示使用 ERC20 如 USDT) */
686
+ quoteToken?: string;
687
+ /** ✅ 新增:quoteToken 的精度(默认 18,USDT 在 XLayer 上是 6 位) */
688
+ quoteTokenDecimals?: number;
679
689
  /** 报价滑点(仅用于估算 sellAmount),默认 0 */
680
690
  slippageBps?: number;
681
691
  withdrawToOwner?: boolean;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * XLayer AA UserOp 批量构建器
3
+ */
4
+ import type { Wallet } from 'ethers';
5
+ import type { AAAccountManager } from './aa-account.js';
6
+ import type { AANonceMap } from './types.js';
7
+ import type { UserOperation, XLayerConfig } from './types.js';
8
+ export interface BuildBuyOpsParams {
9
+ buyers: Wallet[];
10
+ buyerAis: Array<{
11
+ sender: string;
12
+ deployed: boolean;
13
+ }>;
14
+ buyAmountsWei: bigint[];
15
+ tokenAddress: string;
16
+ effectiveRouter: string;
17
+ deadline: number;
18
+ wokbAddress: string;
19
+ nonceMap: AANonceMap;
20
+ consumeInitCode: (sender: string) => string;
21
+ aaManager: AAAccountManager;
22
+ config: XLayerConfig;
23
+ defaultCallGasLimit: bigint;
24
+ }
25
+ export interface BuildSellOpsParams {
26
+ sellers: Wallet[];
27
+ sellerAis: Array<{
28
+ sender: string;
29
+ deployed: boolean;
30
+ }>;
31
+ sellAmountsWei: bigint[];
32
+ tokenAddress: string;
33
+ effectiveRouter: string;
34
+ deadline: number;
35
+ wokbAddress: string;
36
+ sellerAllowances: Map<string, bigint>;
37
+ nonceMap: AANonceMap;
38
+ consumeInitCode: (sender: string) => string;
39
+ aaManager: AAAccountManager;
40
+ config: XLayerConfig;
41
+ defaultApproveGasLimit: bigint;
42
+ defaultSellGasLimit: bigint;
43
+ }
44
+ /**
45
+ * ✅ 优化 1: 并行构建所有买入 UserOp
46
+ */
47
+ export declare function buildBuyUserOps(params: BuildBuyOpsParams): Promise<UserOperation[]>;
48
+ /**
49
+ * ✅ 优化 3: 合并 Approve 和 Sell
50
+ */
51
+ export declare function buildSellUserOps(params: BuildSellOpsParams): Promise<UserOperation[]>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * XLayer AA UserOp 批量构建器
3
+ */
4
+ import { ethers } from 'ethers';
5
+ import { encodeExecute, encodeExecuteBatch } from './aa-account.js';
6
+ import { encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee } from './dex.js';
7
+ import { encodeApproveCall, parseOkb } from './portal-ops.js';
8
+ /**
9
+ * ✅ 优化 1: 并行构建所有买入 UserOp
10
+ */
11
+ export async function buildBuyUserOps(params) {
12
+ const { buyers, buyerAis, buyAmountsWei, tokenAddress, effectiveRouter, deadline, wokbAddress, nonceMap, consumeInitCode, aaManager, config, defaultCallGasLimit } = params;
13
+ const buyOpPromises = buyers.map(async (w, i) => {
14
+ const ai = buyerAis[i];
15
+ const sender = ai.sender;
16
+ const buyWei = buyAmountsWei[i] ?? 0n;
17
+ if (buyWei <= 0n)
18
+ return null;
19
+ const swapData = encodeSwapExactETHForTokensSupportingFee(0n, [wokbAddress, tokenAddress], sender, deadline);
20
+ const callData = encodeExecute(effectiveRouter, buyWei, swapData);
21
+ const { userOp, prefundWei } = await aaManager.buildUserOpWithFixedGas({
22
+ ownerWallet: w,
23
+ sender,
24
+ nonce: nonceMap.next(sender),
25
+ initCode: consumeInitCode(sender),
26
+ callData,
27
+ deployed: ai.deployed,
28
+ fixedGas: {
29
+ ...(config.fixedGas ?? {}),
30
+ callGasLimit: config.fixedGas?.callGasLimit ?? defaultCallGasLimit,
31
+ },
32
+ });
33
+ await aaManager.ensureSenderBalance(w, sender, buyWei + prefundWei + parseOkb('0.0001'), `buyFirstDex/buyer${i + 1}/fund`);
34
+ const signed = await aaManager.signUserOp(userOp, w);
35
+ return signed.userOp;
36
+ });
37
+ const ops = await Promise.all(buyOpPromises);
38
+ return ops.filter((op) => op !== null);
39
+ }
40
+ /**
41
+ * ✅ 优化 3: 合并 Approve 和 Sell
42
+ */
43
+ export async function buildSellUserOps(params) {
44
+ const { sellers, sellerAis, sellAmountsWei, tokenAddress, effectiveRouter, deadline, wokbAddress, sellerAllowances, nonceMap, consumeInitCode, aaManager, config, defaultApproveGasLimit, defaultSellGasLimit } = params;
45
+ const sellOpPromises = sellers.map(async (w, i) => {
46
+ const ai = sellerAis[i];
47
+ const sender = ai.sender;
48
+ const sellWei = sellAmountsWei[i] ?? 0n;
49
+ if (sellWei <= 0n)
50
+ return null;
51
+ const allowance = sellerAllowances.get(sender) ?? 0n;
52
+ const needApprove = allowance < sellWei;
53
+ if (needApprove) {
54
+ // ✅ 合并 approve + sell 到一个 UserOp
55
+ const approveData = encodeApproveCall(effectiveRouter, ethers.MaxUint256);
56
+ const sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellWei, 0n, [tokenAddress, wokbAddress], sender, deadline);
57
+ const batchCallData = encodeExecuteBatch([tokenAddress, effectiveRouter], [0n, 0n], [approveData, sellSwapData]);
58
+ const { userOp, prefundWei } = await aaManager.buildUserOpWithFixedGas({
59
+ ownerWallet: w,
60
+ sender,
61
+ nonce: nonceMap.next(sender),
62
+ initCode: consumeInitCode(sender),
63
+ callData: batchCallData,
64
+ deployed: ai.deployed,
65
+ fixedGas: {
66
+ ...(config.fixedGas ?? {}),
67
+ callGasLimit: config.fixedGas?.callGasLimit ?? (defaultApproveGasLimit + defaultSellGasLimit),
68
+ },
69
+ });
70
+ await aaManager.ensureSenderBalance(w, sender, prefundWei + parseOkb('0.0001'), `buyFirstDex/seller${i + 1}/batch-fund`);
71
+ const signed = await aaManager.signUserOp(userOp, w);
72
+ return signed.userOp;
73
+ }
74
+ else {
75
+ // 只需要 sell
76
+ const sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellWei, 0n, [tokenAddress, wokbAddress], sender, deadline);
77
+ const sellCallData = encodeExecute(effectiveRouter, 0n, sellSwapData);
78
+ const { userOp, prefundWei } = await aaManager.buildUserOpWithFixedGas({
79
+ ownerWallet: w,
80
+ sender,
81
+ nonce: nonceMap.next(sender),
82
+ initCode: consumeInitCode(sender),
83
+ callData: sellCallData,
84
+ deployed: ai.deployed,
85
+ fixedGas: {
86
+ ...(config.fixedGas ?? {}),
87
+ callGasLimit: config.fixedGas?.callGasLimit ?? defaultSellGasLimit,
88
+ },
89
+ });
90
+ await aaManager.ensureSenderBalance(w, sender, prefundWei + parseOkb('0.0001'), `buyFirstDex/seller${i + 1}/sell-fund`);
91
+ const signed = await aaManager.signUserOp(userOp, w);
92
+ return signed.userOp;
93
+ }
94
+ });
95
+ const ops = await Promise.all(sellOpPromises);
96
+ return ops.filter((op) => op !== null);
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.5.51",
3
+ "version": "1.5.53",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",