four-flap-meme-sdk 1.5.96 → 1.5.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ export { fourPrivateBuyMerkle, fourPrivateSellMerkle, fourBatchPrivateBuyMerkle,
9
9
  export { disperseWithBundleMerkle, sweepWithBundleMerkle, pairwiseTransferWithBundleMerkle } from './utils.js';
10
10
  export { fourPancakeProxyBatchBuyMerkle, fourPancakeProxyBatchSellMerkle, approveFourPancakeProxy, approveFourPancakeProxyBatch } from './pancake-proxy.js';
11
11
  export { approveFourTokenManagerBatch, type ApproveFourTokenManagerBatchParams, type ApproveFourTokenManagerBatchResult } from './approve-tokenmanager.js';
12
- export { submitBundleToMerkle, submitMultipleBundles, submitMultipleBundlesParallel, type MerkleSubmitConfig, type SubmitBundleResult, submitBundleToBlockRazor, submitMultipleBundlesToBlockRazor, submitMultipleBundlesToBlockRazorParallel, type BlockRazorSubmitConfig, type BlockRazorSubmitResult, submitDirectToRpc, submitDirectToRpcSequential, // ✅ 新增:顺序广播并等待确认(用于多跳)
12
+ export { submitBundleToMerkle, submitMultipleBundles, submitMultipleBundlesParallel, type MerkleSubmitConfig, type SubmitBundleResult, submitBundleToBlockRazor, submitMultipleBundlesToBlockRazor, submitMultipleBundlesToBlockRazorParallel, type BlockRazorSubmitConfig, type BlockRazorSubmitResult, submitDirectToRpc, submitDirectToRpcSequential, // ✅ 顺序广播并等待确认(用于多跳)
13
+ submitDirectToRpcSequentialNoWait, // ✅ 顺序广播但不等待确认(用于 AA handleOps + tail)
13
14
  submitDirectToRpcParallel, type DirectSubmitConfig, type DirectSubmitResult, type DirectTxResult } from './submit.js';
14
15
  export { fourBundleBuyFirstMerkle, type FourBundleBuyFirstSignParams, type FourBuyFirstSignConfig, type FourBuyFirstResult } from './swap-buy-first.js';
@@ -22,7 +22,8 @@ submitBundleToMerkle, submitMultipleBundles, submitMultipleBundlesParallel,
22
22
  // ✅ BlockRazor 提交方法
23
23
  submitBundleToBlockRazor, submitMultipleBundlesToBlockRazor, submitMultipleBundlesToBlockRazorParallel,
24
24
  // ✅ Monad 等链的逐笔广播方法
25
- submitDirectToRpc, submitDirectToRpcSequential, // ✅ 新增:顺序广播并等待确认(用于多跳)
25
+ submitDirectToRpc, submitDirectToRpcSequential, // ✅ 顺序广播并等待确认(用于多跳)
26
+ submitDirectToRpcSequentialNoWait, // ✅ 顺序广播但不等待确认(用于 AA handleOps + tail)
26
27
  submitDirectToRpcParallel } from './submit.js';
27
28
  // 先买后卖换手方法
28
29
  export { fourBundleBuyFirstMerkle } from './swap-buy-first.js';
@@ -260,6 +260,17 @@ export declare function submitDirectToRpc(signedTransactions: string[], config:
260
260
  * @returns 广播结果
261
261
  */
262
262
  export declare function submitDirectToRpcSequential(signedTransactions: string[], config: DirectSubmitConfig): Promise<DirectSubmitResult>;
263
+ /**
264
+ * ✅ 顺序广播但不等待确认(用于 AA handleOps + tail 等 nonce 连续的场景)
265
+ *
266
+ * 按顺序逐笔发送交易,每笔只等待广播成功(RPC 返回 txHash),不等待链上确认。
267
+ * 这样可以保证交易按顺序进入 mempool,同时避免等待确认超时。
268
+ *
269
+ * @param signedTransactions 签名后的交易数组
270
+ * @param config 直接广播配置
271
+ * @returns 广播结果
272
+ */
273
+ export declare function submitDirectToRpcSequentialNoWait(signedTransactions: string[], config: DirectSubmitConfig): Promise<DirectSubmitResult>;
263
274
  /**
264
275
  * 并行逐笔广播到 RPC(速度更快,但可能有 nonce 冲突)
265
276
  *
@@ -510,6 +510,94 @@ export async function submitDirectToRpcSequential(signedTransactions, config) {
510
510
  errorSummary: errors.length > 0 ? errors.join('; ') : undefined
511
511
  };
512
512
  }
513
+ /**
514
+ * ✅ 顺序广播但不等待确认(用于 AA handleOps + tail 等 nonce 连续的场景)
515
+ *
516
+ * 按顺序逐笔发送交易,每笔只等待广播成功(RPC 返回 txHash),不等待链上确认。
517
+ * 这样可以保证交易按顺序进入 mempool,同时避免等待确认超时。
518
+ *
519
+ * @param signedTransactions 签名后的交易数组
520
+ * @param config 直接广播配置
521
+ * @returns 广播结果
522
+ */
523
+ export async function submitDirectToRpcSequentialNoWait(signedTransactions, config) {
524
+ const totalTransactions = signedTransactions?.length ?? 0;
525
+ if (!signedTransactions || signedTransactions.length === 0) {
526
+ return {
527
+ code: false,
528
+ totalTransactions: 0,
529
+ successCount: 0,
530
+ failedCount: 0,
531
+ txHashes: [],
532
+ results: [],
533
+ errorSummary: 'signedTransactions cannot be empty'
534
+ };
535
+ }
536
+ if (!config.rpcUrl) {
537
+ return {
538
+ code: false,
539
+ totalTransactions,
540
+ successCount: 0,
541
+ failedCount: totalTransactions,
542
+ txHashes: [],
543
+ results: [],
544
+ errorSummary: 'rpcUrl is required in config'
545
+ };
546
+ }
547
+ const chainId = config.chainId ?? 143;
548
+ const chainName = config.chainName ?? 'MONAD';
549
+ const provider = new ethers.JsonRpcProvider(config.rpcUrl, {
550
+ chainId,
551
+ name: chainName
552
+ });
553
+ const results = [];
554
+ const txHashes = [];
555
+ const errors = [];
556
+ // ✅ 顺序广播,每笔只等待广播成功(不等待链上确认)
557
+ for (let i = 0; i < signedTransactions.length; i++) {
558
+ const signedTx = signedTransactions[i];
559
+ try {
560
+ // 广播交易,等待 RPC 返回 txHash
561
+ const txResponse = await provider.broadcastTransaction(signedTx);
562
+ results.push({
563
+ index: i,
564
+ success: true,
565
+ txHash: txResponse.hash
566
+ });
567
+ txHashes.push(txResponse.hash);
568
+ console.log(`✅ [${chainName}] 交易 ${i + 1}/${totalTransactions} 广播成功: ${txResponse.hash}`);
569
+ }
570
+ catch (error) {
571
+ const errorMessage = error?.message || String(error);
572
+ console.error(`❌ [${chainName}] 交易 ${i + 1}/${totalTransactions} 广播失败:`, errorMessage);
573
+ // ✅ 兼容:nonce too low / already known 等情况下,交易可能已经被接收
574
+ const recovered = await recoverTxHashIfAlreadySeen(provider, signedTx, errorMessage);
575
+ if (recovered) {
576
+ results.push({ index: i, success: true, txHash: recovered, error: errorMessage });
577
+ txHashes.push(recovered);
578
+ continue;
579
+ }
580
+ results.push({
581
+ index: i,
582
+ success: false,
583
+ error: errorMessage
584
+ });
585
+ errors.push(`交易 ${i + 1}: ${errorMessage}`);
586
+ // ✅ 如果某笔失败,继续尝试后续交易(可能是 nonce 问题可以自动恢复)
587
+ }
588
+ }
589
+ const successCount = txHashes.length;
590
+ const failedCount = totalTransactions - successCount;
591
+ return {
592
+ code: successCount > 0,
593
+ totalTransactions,
594
+ successCount,
595
+ failedCount,
596
+ txHashes,
597
+ results,
598
+ errorSummary: errors.length > 0 ? errors.join('; ') : undefined
599
+ };
600
+ }
513
601
  /**
514
602
  * 并行逐笔广播到 RPC(速度更快,但可能有 nonce 冲突)
515
603
  *
package/dist/index.d.ts CHANGED
@@ -49,7 +49,8 @@ export { flapBundleCurveToDex, type CurveToDexChain, type DexPoolType, type Curv
49
49
  export { flapBundleCreateToDex, type CreateToDexChain, type CreateToDexSignConfig, type CreateTokenInfo, type FlapCreateToDexParams, type FlapCreateToDexResult } from './flap/portal-bundle-merkle/create-to-dex.js';
50
50
  export { PROFIT_CONFIG } from './utils/constants.js';
51
51
  export { quoteV2, quoteV3, quote, getTokenToNativeQuote, getNativeToTokenQuote, getWrappedNativeAddress, getStableCoins, V3_FEE_TIERS, QUOTE_CONFIG, type QuoteParams, type QuoteResult, type SupportedChain, } from './utils/quote-helpers.js';
52
- export { submitDirectToRpc, submitDirectToRpcSequential, // ✅ 新增:顺序广播并等待确认(用于多跳)
52
+ export { submitDirectToRpc, submitDirectToRpcSequential, // ✅ 顺序广播并等待确认(用于多跳)
53
+ submitDirectToRpcSequentialNoWait, // ✅ 顺序广播但不等待确认(用于 AA handleOps + tail)
53
54
  submitDirectToRpcParallel, type DirectSubmitConfig, type DirectSubmitResult, type DirectTxResult } from './contracts/tm-bundle-merkle/submit.js';
54
55
  export { directV2BatchBuy, directV2BatchSell, directV3BatchBuy, directV3BatchSell, getRouterAddress, DIRECT_ROUTERS, type DirectV2BuyParams, type DirectV2SellParams, type DirectV3BuyParams, type DirectV3SellParams, type DirectRouterResult, type DirectRouterSignConfig, type DexKey, type RouterVersion, } from './dex/index.js';
55
56
  export * as XLayer from './xlayer/index.js';
package/dist/index.js CHANGED
@@ -82,7 +82,8 @@ export { PROFIT_CONFIG } from './utils/constants.js';
82
82
  // ✅ V2/V3 报价工具(统一管理)
83
83
  export { quoteV2, quoteV3, quote, getTokenToNativeQuote, getNativeToTokenQuote, getWrappedNativeAddress, getStableCoins, V3_FEE_TIERS, QUOTE_CONFIG, } from './utils/quote-helpers.js';
84
84
  // ✅ Monad 等不支持 Bundle 的链:逐笔 RPC 广播方法(服务器端使用)
85
- export { submitDirectToRpc, submitDirectToRpcSequential, // ✅ 新增:顺序广播并等待确认(用于多跳)
85
+ export { submitDirectToRpc, submitDirectToRpcSequential, // ✅ 顺序广播并等待确认(用于多跳)
86
+ submitDirectToRpcSequentialNoWait, // ✅ 顺序广播但不等待确认(用于 AA handleOps + tail)
86
87
  submitDirectToRpcParallel } from './contracts/tm-bundle-merkle/submit.js';
87
88
  // ============================================================
88
89
  // ✅ DEX 直接交易(不走代理合约)
@@ -10,7 +10,7 @@
10
10
  import { Wallet, Interface, Contract, ethers } from 'ethers';
11
11
  import { FLAP_PORTAL, ENTRYPOINT_ABI, PORTAL_ABI, DEFAULT_CALL_GAS_LIMIT_SELL, DEFAULT_WITHDRAW_RESERVE, WOKB, POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, } from './constants.js';
12
12
  import { AAAccountManager, encodeExecute, encodeExecuteBatch, createWallet } from './aa-account.js';
13
- import { encodeBuyCall, encodeBuyCallV3, encodeSellCall, encodeApproveCall, encodeTransferCall, encodeCreateCallV2, encodeCreateCallV3, encodeCreateCallV4, PortalQuery, parseOkb, formatOkb, lpFeeProfileToV3Fee, } from './portal-ops.js';
13
+ import { encodeBuyCallV3, encodeSellCall, encodeApproveCall, encodeTransferCall, encodeCreateCallV2, encodeCreateCallV3, encodeCreateCallV4, PortalQuery, parseOkb, formatOkb, lpFeeProfileToV3Fee, } from './portal-ops.js';
14
14
  import { mapWithConcurrency } from '../utils/concurrency.js';
15
15
  import { PROFIT_CONFIG, ZERO_ADDRESS } from '../utils/constants.js';
16
16
  import { DexQuery } from './dex.js';
@@ -808,6 +808,7 @@ export class BundleExecutor {
808
808
  if (dexType === 'V3') {
809
809
  routerAddress = POTATOSWAP_V3_ROUTER;
810
810
  const v3Fee = lpFeeProfileToV3Fee(tokenState.lpFeeProfile);
811
+ // ✅ V3 卖出需要 unwrap WOKB 到 OKB
811
812
  swapData = encodeSwapExactTokensForETHV3({
812
813
  tokenIn: tokenAddress,
813
814
  tokenOut: WOKB,
@@ -816,7 +817,8 @@ export class BundleExecutor {
816
817
  deadline,
817
818
  amountIn: it.sellAmount,
818
819
  amountOutMinimum: 0n,
819
- sqrtPriceLimitX96: 0n
820
+ sqrtPriceLimitX96: 0n,
821
+ unwrapRecipient: it.sender, // ✅ 添加 unwrap 接收者
820
822
  });
821
823
  }
822
824
  else {
@@ -1569,9 +1571,11 @@ export class BundleExecutor {
1569
1571
  });
1570
1572
  totalCurveBuyWei = curveBuyData.reduce((sum, d) => sum + d.buyWei, 0n);
1571
1573
  // ✅ 并行构建和签名所有内盘买入 UserOps
1574
+ // ✅ 使用 encodeBuyCallV3(支持 extensionData),与 V4 代币创建保持一致
1575
+ const extensionData = params.extensionData ?? '0x';
1572
1576
  const signedCurveBuyOps = await mapWithConcurrency(curveBuyData, 5, async (data) => {
1573
1577
  const { wallet, info, buyWei, gasLimit } = data;
1574
- const buyData = encodeBuyCall(tokenAddress, buyWei, 0n);
1578
+ const buyData = encodeBuyCallV3(tokenAddress, buyWei, 0n, extensionData);
1575
1579
  const buyCallData = encodeExecute(FLAP_PORTAL, buyWei, buyData);
1576
1580
  const buyOpRes = await aaManager.buildUserOpWithFixedGas({
1577
1581
  ownerWallet: wallet,
@@ -1586,17 +1590,48 @@ export class BundleExecutor {
1586
1590
  return signedBuyOp.userOp;
1587
1591
  });
1588
1592
  ops1.push(...signedCurveBuyOps);
1593
+ // === 向内盘买家 AA 账户转入 OKB(prefund + 买入金额)===
1594
+ // ✅ 修复 AA21 didn't pay prefund 错误
1595
+ const feeData = await provider.getFeeData();
1596
+ const gasPrice = feeData.gasPrice ?? 100000000n;
1597
+ const chainId = (await provider.getNetwork()).chainId;
1598
+ let currentNonce = params.payerStartNonce ?? (await provider.getTransactionCount(payerWallet.address, 'pending'));
1599
+ const DEFAULT_PREFUND_ESTIMATE = parseOkb('0.002'); // 预估每个 UserOp 的 prefund
1600
+ const BUFFER_WEI = parseOkb('0.0005'); // 缓冲金额
1601
+ for (let i = 0; i < curveBuyerWallets.length; i++) {
1602
+ const buyerSender = curveBuyerInfos[i].sender;
1603
+ const buyWei = curveBuyData[i].buyWei;
1604
+ // 计算需要转入的金额:买入金额 + prefund + 缓冲
1605
+ const requiredAmount = buyWei + DEFAULT_PREFUND_ESTIMATE + BUFFER_WEI;
1606
+ // 检查买家 AA 账户当前余额
1607
+ const currentBalance = await provider.getBalance(buyerSender);
1608
+ if (currentBalance < requiredAmount) {
1609
+ const transferAmount = requiredAmount - currentBalance;
1610
+ console.log(`[内盘买家${i + 1}] 向 AA 账户 ${buyerSender} 转入 ${formatOkb(transferAmount)} OKB`);
1611
+ const fundTxRequest = {
1612
+ to: buyerSender,
1613
+ value: transferAmount,
1614
+ nonce: currentNonce,
1615
+ gasLimit: 21000n,
1616
+ gasPrice,
1617
+ chainId
1618
+ };
1619
+ const signedFundTx = await payerWallet.signTransaction(fundTxRequest);
1620
+ signedTransactions.push(signedFundTx);
1621
+ currentNonce++;
1622
+ }
1623
+ }
1589
1624
  // 签名第一个 handleOps
1590
- const startNonce = params.payerStartNonce ?? (await provider.getTransactionCount(payerWallet.address, 'pending'));
1591
1625
  const signedMainTx = await this.signHandleOpsTx({
1592
1626
  ops: ops1,
1593
1627
  payerWallet: payerWallet,
1594
1628
  beneficiary: params.beneficiary ?? payerWallet.address,
1595
- nonce: startNonce,
1629
+ nonce: currentNonce,
1596
1630
  gasLimit: effConfig.gasLimit,
1597
1631
  gasPrice: effConfig.minGasPriceGwei ? ethers.parseUnits(effConfig.minGasPriceGwei.toString(), 'gwei') : undefined
1598
1632
  });
1599
1633
  signedTransactions.push(signedMainTx);
1634
+ currentNonce++;
1600
1635
  // --- 4. 构建外盘买入 handleOps (如果启用) ---
1601
1636
  if (enableDexBuy && dexBuyerPrivateKeys.length > 0) {
1602
1637
  const dexBuyerWallets = dexBuyerPrivateKeys.map(pk => createWallet(pk, config));
@@ -1614,10 +1649,34 @@ export class BundleExecutor {
1614
1649
  return { wallet, info: dexBuyerInfos[i], buyWei };
1615
1650
  });
1616
1651
  totalDexBuyWei = dexBuyData.reduce((sum, d) => sum + d.buyWei, 0n);
1652
+ // === 向外盘买家 AA 账户转入 OKB(prefund + 买入金额)===
1653
+ for (let i = 0; i < dexBuyerWallets.length; i++) {
1654
+ const buyerSender = dexBuyerInfos[i].sender;
1655
+ const buyWei = dexBuyData[i].buyWei;
1656
+ // 计算需要转入的金额:买入金额 + prefund + 缓冲
1657
+ const requiredAmount = buyWei + DEFAULT_PREFUND_ESTIMATE + BUFFER_WEI;
1658
+ // 检查买家 AA 账户当前余额
1659
+ const dexBuyerBalance = await provider.getBalance(buyerSender);
1660
+ if (dexBuyerBalance < requiredAmount) {
1661
+ const transferAmount = requiredAmount - dexBuyerBalance;
1662
+ console.log(`[外盘买家${i + 1}] 向 AA 账户 ${buyerSender} 转入 ${formatOkb(transferAmount)} OKB`);
1663
+ const fundTxRequest = {
1664
+ to: buyerSender,
1665
+ value: transferAmount,
1666
+ nonce: currentNonce,
1667
+ gasLimit: 21000n,
1668
+ gasPrice,
1669
+ chainId
1670
+ };
1671
+ const signedFundTx = await payerWallet.signTransaction(fundTxRequest);
1672
+ signedTransactions.push(signedFundTx);
1673
+ currentNonce++;
1674
+ }
1675
+ }
1617
1676
  // ✅ 并行构建和签名所有外盘买入 UserOps
1618
1677
  const signedDexBuyOps = await mapWithConcurrency(dexBuyData, 5, async (data) => {
1619
1678
  const { wallet, info, buyWei } = data;
1620
- // ✅ 外盘买入:使用 Portal swapExactInput 进行自动路由
1679
+ // ✅ 外盘买入:使用 DEX Router 进行交换
1621
1680
  let swapData;
1622
1681
  let routerAddress;
1623
1682
  if (dexType === 'V3') {
@@ -1655,11 +1714,12 @@ export class BundleExecutor {
1655
1714
  ops: ops2,
1656
1715
  payerWallet: payerWallet,
1657
1716
  beneficiary: params.beneficiary ?? payerWallet.address,
1658
- nonce: startNonce + 1,
1717
+ nonce: currentNonce,
1659
1718
  gasLimit: effConfig.gasLimit,
1660
1719
  gasPrice: effConfig.minGasPriceGwei ? ethers.parseUnits(effConfig.minGasPriceGwei.toString(), 'gwei') : undefined
1661
1720
  });
1662
1721
  signedTransactions.push(signedDexTx);
1722
+ currentNonce++;
1663
1723
  }
1664
1724
  // --- 5. 利润提取 (Direct EOA transfer) ---
1665
1725
  let totalProfitWei = 0n;
@@ -1669,12 +1729,11 @@ export class BundleExecutor {
1669
1729
  totalProfitWei += calculateProfitWei(totalDexBuyWei, profitSettings.profitBps);
1670
1730
  }
1671
1731
  if (totalProfitWei > 0n) {
1672
- const profitTxNonce = startNonce + (enableDexBuy ? 2 : 1);
1673
1732
  const signedProfitTx = await this.signProfitTransaction({
1674
1733
  payerWallet,
1675
1734
  profitWei: totalProfitWei,
1676
1735
  recipient: profitSettings.profitRecipient,
1677
- nonce: profitTxNonce
1736
+ nonce: currentNonce
1678
1737
  });
1679
1738
  signedTransactions.push(signedProfitTx);
1680
1739
  }
@@ -74,6 +74,6 @@ export declare const PORTAL_ABI: readonly ["function swapExactInput((address inp
74
74
  export declare const ERC20_ABI: readonly ["function balanceOf(address account) view returns (uint256)", "function allowance(address owner, address spender) view returns (uint256)", "function approve(address spender, uint256 amount) returns (bool)", "function transfer(address to, uint256 amount) returns (bool)", "function decimals() view returns (uint8)", "function symbol() view returns (string)", "function name() view returns (string)"];
75
75
  /** PotatoSwap V2 Router ABI */
76
76
  export declare const POTATOSWAP_V2_ROUTER_ABI: readonly ["function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external payable returns (uint[] memory amounts)", "function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)", "function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)", "function swapExactETHForTokensSupportingFeeOnTransferTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external payable", "function swapExactTokensForETHSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external", "function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external", "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)", "function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts)", "function WETH() external pure returns (address)"];
77
- /** PotatoSwap V3 Router ABI (SwapRouter02) */
78
- export declare const POTATOSWAP_V3_ROUTER_ABI: readonly ["function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountOut)", "function exactOutputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountIn)"];
77
+ /** PotatoSwap V3 Router ABI (SwapRouter - Legacy 版本,deadline 在 struct 内部) */
78
+ export declare const POTATOSWAP_V3_ROUTER_ABI: readonly ["function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountOut)", "function exactOutputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountIn)", "function multicall(bytes[] calldata data) external payable returns (bytes[] memory results)", "function unwrapWETH9(uint256 amountMinimum, address recipient) external payable", "function refundETH() external payable"];
79
79
  export type HexString = `0x${string}`;
@@ -146,8 +146,15 @@ export const POTATOSWAP_V2_ROUTER_ABI = [
146
146
  'function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts)',
147
147
  'function WETH() external pure returns (address)',
148
148
  ];
149
- /** PotatoSwap V3 Router ABI (SwapRouter02) */
149
+ /** PotatoSwap V3 Router ABI (SwapRouter - Legacy 版本,deadline 在 struct 内部) */
150
150
  export const POTATOSWAP_V3_ROUTER_ABI = [
151
+ // exactInputSingle - 单跳交换,deadline 在 params 内部(Legacy 版本)
151
152
  'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountOut)',
152
153
  'function exactOutputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountIn)',
154
+ // multicall - 打包多个调用(Legacy 版本不含 deadline 参数)
155
+ 'function multicall(bytes[] calldata data) external payable returns (bytes[] memory results)',
156
+ // unwrapWETH9 - 将 WOKB 转换为 OKB(用于卖出后获取原生币)
157
+ 'function unwrapWETH9(uint256 amountMinimum, address recipient) external payable',
158
+ // refundETH - 退还多余的 ETH(用于买入时的找零)
159
+ 'function refundETH() external payable',
153
160
  ];
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { Wallet, ethers } from 'ethers';
5
5
  import { AANonceMap, } from './types.js';
6
- import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, WOKB, MULTICALL3, } from './constants.js';
6
+ import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, POTATOSWAP_V3_FACTORY, WOKB, MULTICALL3, } from './constants.js';
7
7
  import { AAAccountManager, encodeExecute } from './aa-account.js';
8
8
  import { encodeApproveCall, lpFeeProfileToV3Fee, } from './portal-ops.js';
9
9
  import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, } from './dex.js';
@@ -12,6 +12,64 @@ import { PROFIT_CONFIG } from '../utils/constants.js';
12
12
  const multicallIface = new ethers.Interface([
13
13
  'function aggregate3Value((address target,bool allowFailure,uint256 value,bytes callData)[] calls) payable returns ((bool success,bytes returnData)[] returnData)',
14
14
  ]);
15
+ // ============================================================================
16
+ // V3 Slot0 报价(用于 XLayer 没有 V3 Quoter 的情况)
17
+ // ============================================================================
18
+ const V3_FACTORY_ABI = [
19
+ 'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)',
20
+ ];
21
+ const V3_POOL_ABI = [
22
+ 'function slot0() view returns (uint160 sqrtPriceX96,int24 tick,uint16 observationIndex,uint16 observationCardinality,uint16 observationCardinalityNext,uint8 feeProtocol,bool unlocked)',
23
+ 'function token0() view returns (address)',
24
+ 'function token1() view returns (address)',
25
+ ];
26
+ const V3_FEE_DENOMINATOR = 1000000n;
27
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
28
+ /**
29
+ * 使用 V3 Pool 的 slot0 获取 Token → WOKB 的报价
30
+ * 这是一个现货价计算,比 V3 Quoter 更简单但精度稍低
31
+ */
32
+ async function quoteV3ViaSlot0(params) {
33
+ try {
34
+ const { provider, tokenIn, amountIn, fee } = params;
35
+ if (!tokenIn || amountIn <= 0n)
36
+ return 0n;
37
+ const tokenInLower = tokenIn.toLowerCase();
38
+ const wokbLower = WOKB.toLowerCase();
39
+ if (tokenInLower === wokbLower)
40
+ return amountIn;
41
+ const factory = new ethers.Contract(POTATOSWAP_V3_FACTORY, V3_FACTORY_ABI, provider);
42
+ const poolAddr = await factory.getPool(tokenIn, WOKB, fee);
43
+ if (!poolAddr || poolAddr.toLowerCase() === ZERO_ADDRESS.toLowerCase())
44
+ return 0n;
45
+ const pool = new ethers.Contract(poolAddr, V3_POOL_ABI, provider);
46
+ const [t0, t1, slot0] = await Promise.all([pool.token0(), pool.token1(), pool.slot0()]);
47
+ if (!t0 || !t1 || !slot0)
48
+ return 0n;
49
+ const sqrtPriceX96 = BigInt(slot0[0]);
50
+ if (sqrtPriceX96 <= 0n)
51
+ return 0n;
52
+ // 扣除手续费
53
+ const amountInLessFee = (amountIn * (V3_FEE_DENOMINATOR - BigInt(fee))) / V3_FEE_DENOMINATOR;
54
+ if (amountInLessFee <= 0n)
55
+ return 0n;
56
+ const Q192 = 2n ** 192n;
57
+ const num = sqrtPriceX96 * sqrtPriceX96;
58
+ const t0Lower = String(t0).toLowerCase();
59
+ const t1Lower = String(t1).toLowerCase();
60
+ // sqrtPriceX96 表示 token1/token0 的现货价
61
+ if (tokenInLower === t0Lower && wokbLower === t1Lower) {
62
+ return (amountInLessFee * num) / Q192;
63
+ }
64
+ if (tokenInLower === t1Lower && wokbLower === t0Lower) {
65
+ return (amountInLessFee * Q192) / num;
66
+ }
67
+ return 0n;
68
+ }
69
+ catch {
70
+ return 0n;
71
+ }
72
+ }
15
73
  function chunkArray(arr, size) {
16
74
  const out = [];
17
75
  const n = Math.max(1, Math.floor(size));
@@ -140,15 +198,37 @@ export class AADexSwapExecutor {
140
198
  needApprove = allowance < sellAmountWei;
141
199
  }
142
200
  // 估算卖出输出(用于利润提取与 buy 预算)
201
+ // ✅ V3 模式:V2 报价可能失败,需要使用 slot0 报价
143
202
  const quotedSellOutWei = await (async () => {
203
+ // 如果是 V3 模式,优先使用 slot0 报价
204
+ if (isV3Trade) {
205
+ const v3Fee = lpFeeProfileToV3Fee(lpFeeProfile);
206
+ const slot0Quote = await quoteV3ViaSlot0({
207
+ provider,
208
+ tokenIn: tokenAddress,
209
+ amountIn: sellAmountWei,
210
+ fee: v3Fee,
211
+ });
212
+ if (slot0Quote > 0n) {
213
+ console.log(`[AA V3 捆绑换手] slot0 报价成功: ${ethers.formatEther(slot0Quote)} OKB`);
214
+ return slot0Quote;
215
+ }
216
+ // slot0 报价失败,尝试 V2 fallback
217
+ }
144
218
  try {
219
+ // V2 报价(大多数代币同时有 V2/V3 池子)
145
220
  return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
146
221
  }
147
222
  catch {
223
+ // V2 报价失败时返回 0
148
224
  return 0n;
149
225
  }
150
226
  })();
151
227
  // buyAmount:若未传则按 quotedSellOut 计算;利润从 quotedSellOut 中刮取,但要保证买入资金充足
228
+ // ✅ 修复:如果报价失败且未传入 buyAmountOkb,抛出明确错误
229
+ if (!buyAmountOkb && quotedSellOutWei === 0n) {
230
+ throw new Error('AA 捆绑换手:无法获取报价(V3 池子不存在或流动性不足),请明确传入 buyAmountOkb 参数');
231
+ }
152
232
  const requestedBuyWei = buyAmountOkb
153
233
  ? ethers.parseEther(String(buyAmountOkb))
154
234
  : (quotedSellOutWei * BigInt(10000 - slippageBps)) / 10000n;
@@ -177,16 +257,18 @@ export class AADexSwapExecutor {
177
257
  const deadline = this.getDexDeadline();
178
258
  let sellSwapData;
179
259
  if (isV3Trade) {
180
- // V3: 使用 exactInputSingle
260
+ // V3: 使用 multicall(exactInputSingle + unwrapWETH9)
261
+ // 注意:V3 卖出得到的是 WOKB,需要 unwrap 成 OKB
181
262
  sellSwapData = encodeSwapExactTokensForETHV3({
182
263
  tokenIn: tokenAddress,
183
264
  tokenOut: WOKB,
184
265
  fee: lpFeeProfileToV3Fee(lpFeeProfile),
185
- recipient: sellerSender,
266
+ recipient: sellerSender, // 会被函数内部替换为 ADDRESS_THIS
186
267
  deadline,
187
268
  amountIn: sellAmountWei,
188
269
  amountOutMinimum: 0n,
189
270
  sqrtPriceLimitX96: 0n,
271
+ unwrapRecipient: sellerSender, // ✅ unwrap 后发送到 sellerSender
190
272
  });
191
273
  }
192
274
  else {
@@ -368,16 +450,18 @@ export class AADexSwapExecutor {
368
450
  const deadline = this.getDexDeadline();
369
451
  let sellSwapData;
370
452
  if (isV3Trade) {
371
- // V3: 使用 exactInputSingle
453
+ // V3: 使用 multicall(exactInputSingle + unwrapWETH9)
454
+ // 注意:V3 卖出得到的是 WOKB,需要 unwrap 成 OKB
372
455
  sellSwapData = encodeSwapExactTokensForETHV3({
373
456
  tokenIn: tokenAddress,
374
457
  tokenOut: WOKB,
375
458
  fee: lpFeeProfileToV3Fee(lpFeeProfile),
376
- recipient: sellerAi.sender,
459
+ recipient: sellerAi.sender, // 会被函数内部替换为 ADDRESS_THIS
377
460
  deadline,
378
461
  amountIn: sellAmountWei,
379
462
  amountOutMinimum: 0n,
380
463
  sqrtPriceLimitX96: 0n,
464
+ unwrapRecipient: sellerAi.sender, // ✅ unwrap 后发送到 sellerAi.sender
381
465
  });
382
466
  }
383
467
  else {
@@ -398,7 +482,22 @@ export class AADexSwapExecutor {
398
482
  const buyAmountsWei = buyAmountsOkb.map(a => ethers.parseEther(a));
399
483
  const totalBuyWei = buyAmountsWei.reduce((a, b) => a + (b ?? 0n), 0n);
400
484
  // Profit op:估算卖出输出,按比例刮取(但必须保证分发/买入资金充足)
485
+ // ✅ V3 模式:优先使用 slot0 报价
401
486
  const quotedSellOutWei = await (async () => {
487
+ // 如果是 V3 模式,优先使用 slot0 报价
488
+ if (isV3Trade) {
489
+ const v3Fee = lpFeeProfileToV3Fee(lpFeeProfile);
490
+ const slot0Quote = await quoteV3ViaSlot0({
491
+ provider,
492
+ tokenIn: tokenAddress,
493
+ amountIn: sellAmountWei,
494
+ fee: v3Fee,
495
+ });
496
+ if (slot0Quote > 0n) {
497
+ console.log(`[AA V3 批量换手] slot0 报价成功: ${ethers.formatEther(slot0Quote)} OKB`);
498
+ return slot0Quote;
499
+ }
500
+ }
402
501
  try {
403
502
  return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
404
503
  }
@@ -306,6 +306,7 @@ export class DexBundleExecutor {
306
306
  const sellNonce = nonceMap.next(sender);
307
307
  let sellSwapData;
308
308
  if (isV3) {
309
+ // ✅ V3 卖出需要 unwrap WOKB 到 OKB
309
310
  sellSwapData = encodeSwapExactTokensForETHV3({
310
311
  tokenIn: tokenAddress,
311
312
  tokenOut: WOKB,
@@ -315,6 +316,7 @@ export class DexBundleExecutor {
315
316
  amountIn: sellAmount,
316
317
  amountOutMinimum: 0n,
317
318
  sqrtPriceLimitX96: 0n,
319
+ unwrapRecipient: sender, // ✅ 添加 unwrap 接收者
318
320
  });
319
321
  }
320
322
  else {
@@ -7,21 +7,10 @@
7
7
  * - 通过 AA 账户执行
8
8
  */
9
9
  import type { XLayerConfig, DexSwapResult } from './types.js';
10
- export declare function encodeSwapExactETHForTokensV3(params: {
11
- tokenIn: string;
12
- tokenOut: string;
13
- fee: number;
14
- recipient: string;
15
- deadline: number;
16
- amountIn: bigint;
17
- amountOutMinimum: bigint;
18
- sqrtPriceLimitX96: bigint;
19
- }): string;
20
10
  /**
21
- * 编码 swapExactTokensForETHV3 调用
22
- * 对齐 Uniswap V3 的 exactInputSingle 逻辑
11
+ * V3 exactInputSingle 参数结构(Legacy 版本,包含 deadline)
23
12
  */
24
- export declare function encodeSwapExactTokensForETHV3(params: {
13
+ interface V3ExactInputSingleParams {
25
14
  tokenIn: string;
26
15
  tokenOut: string;
27
16
  fee: number;
@@ -30,6 +19,37 @@ export declare function encodeSwapExactTokensForETHV3(params: {
30
19
  amountIn: bigint;
31
20
  amountOutMinimum: bigint;
32
21
  sqrtPriceLimitX96: bigint;
22
+ }
23
+ /**
24
+ * 编码 unwrapWETH9 调用(将 WOKB 转换为 OKB)
25
+ */
26
+ export declare function encodeUnwrapWETH9(amountMinimum: bigint, recipient: string): string;
27
+ /**
28
+ * 编码 refundETH 调用(退还多余的 ETH)
29
+ */
30
+ export declare function encodeRefundETH(): string;
31
+ /**
32
+ * 编码 V3 multicall 调用(Legacy 版本,不含 deadline)
33
+ */
34
+ export declare function encodeV3Multicall(data: string[]): string;
35
+ /**
36
+ * ✅ 编码 V3 买入调用(OKB → Token)
37
+ *
38
+ * 对于 AA 模式:直接返回 exactInputSingle 编码(不需要 multicall 包装)
39
+ * - AA 的 execute 会正确传递 msg.value
40
+ * - V3 Router 会自动将原生币包装为 WOKB
41
+ *
42
+ * 对于 EOA 模式:调用者需要自己用 multicall 包装
43
+ */
44
+ export declare function encodeSwapExactETHForTokensV3(params: V3ExactInputSingleParams): string;
45
+ /**
46
+ * ✅ 编码 V3 卖出调用(Token → OKB)
47
+ * 使用 multicall 包装 exactInputSingle + unwrapWETH9
48
+ *
49
+ * 注意:V3 卖出返回的是 WOKB(ERC20),需要调用 unwrapWETH9 转换为 OKB
50
+ */
51
+ export declare function encodeSwapExactTokensForETHV3(params: V3ExactInputSingleParams & {
52
+ unwrapRecipient?: string;
33
53
  }): string;
34
54
  /**
35
55
  * 编码 swapExactETHForTokens 调用
@@ -190,3 +210,4 @@ export declare function quoteOkbToToken(okbAmount: bigint, tokenAddress: string,
190
210
  * 快速获取 Token -> OKB 报价
191
211
  */
192
212
  export declare function quoteTokenToOkb(tokenAmount: bigint, tokenAddress: string, config?: DexConfig): Promise<bigint>;
213
+ export {};
@@ -15,15 +15,62 @@ import { encodeApproveCall, parseOkb, formatOkb } from './portal-ops.js';
15
15
  // ============================================================================
16
16
  const routerIface = new Interface(POTATOSWAP_V2_ROUTER_ABI);
17
17
  const v3RouterIface = new Interface(POTATOSWAP_V3_ROUTER_ABI);
18
- export function encodeSwapExactETHForTokensV3(params) {
18
+ /**
19
+ * 编码 exactInputSingle 调用(内部使用)
20
+ */
21
+ function encodeExactInputSingle(params) {
19
22
  return v3RouterIface.encodeFunctionData('exactInputSingle', [params]);
20
23
  }
21
24
  /**
22
- * 编码 swapExactTokensForETHV3 调用
23
- * 对齐 Uniswap V3 的 exactInputSingle 逻辑
25
+ * 编码 unwrapWETH9 调用(将 WOKB 转换为 OKB)
26
+ */
27
+ export function encodeUnwrapWETH9(amountMinimum, recipient) {
28
+ return v3RouterIface.encodeFunctionData('unwrapWETH9', [amountMinimum, recipient]);
29
+ }
30
+ /**
31
+ * 编码 refundETH 调用(退还多余的 ETH)
32
+ */
33
+ export function encodeRefundETH() {
34
+ return v3RouterIface.encodeFunctionData('refundETH', []);
35
+ }
36
+ /**
37
+ * 编码 V3 multicall 调用(Legacy 版本,不含 deadline)
38
+ */
39
+ export function encodeV3Multicall(data) {
40
+ return v3RouterIface.encodeFunctionData('multicall', [data]);
41
+ }
42
+ /**
43
+ * ✅ 编码 V3 买入调用(OKB → Token)
44
+ *
45
+ * 对于 AA 模式:直接返回 exactInputSingle 编码(不需要 multicall 包装)
46
+ * - AA 的 execute 会正确传递 msg.value
47
+ * - V3 Router 会自动将原生币包装为 WOKB
48
+ *
49
+ * 对于 EOA 模式:调用者需要自己用 multicall 包装
50
+ */
51
+ export function encodeSwapExactETHForTokensV3(params) {
52
+ // 直接返回 exactInputSingle 编码
53
+ // V3 Router 会自动处理原生币 → WOKB 的转换
54
+ return encodeExactInputSingle(params);
55
+ }
56
+ /**
57
+ * ✅ 编码 V3 卖出调用(Token → OKB)
58
+ * 使用 multicall 包装 exactInputSingle + unwrapWETH9
59
+ *
60
+ * 注意:V3 卖出返回的是 WOKB(ERC20),需要调用 unwrapWETH9 转换为 OKB
24
61
  */
25
62
  export function encodeSwapExactTokensForETHV3(params) {
26
- return v3RouterIface.encodeFunctionData('exactInputSingle', [params]);
63
+ // V3 卖出需要:1) 先 swap 到 Router 地址;2) 再 unwrap 到最终接收者
64
+ const ADDRESS_THIS = '0x0000000000000000000000000000000000000002'; // V3 Router 的 ADDRESS_THIS
65
+ const swapParams = {
66
+ ...params,
67
+ recipient: ADDRESS_THIS, // 先发送到 Router 内部
68
+ };
69
+ const swapData = encodeExactInputSingle(swapParams);
70
+ // unwrap 到最终接收者
71
+ const finalRecipient = params.unwrapRecipient || params.recipient;
72
+ const unwrapData = encodeUnwrapWETH9(0n, finalRecipient); // amountMinimum = 0,不做最小值检查
73
+ return encodeV3Multicall([swapData, unwrapData]);
27
74
  }
28
75
  /**
29
76
  * 编码 swapExactETHForTokens 调用
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.5.96",
3
+ "version": "1.5.99",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",