four-flap-meme-sdk 1.5.98 → 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.
@@ -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 {
@@ -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.98",
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",