four-flap-meme-sdk 1.6.66 → 1.6.68
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.
|
@@ -17,8 +17,8 @@ import { createAAAccountManager, encodeExecute, createWallet } from './aa-accoun
|
|
|
17
17
|
import { encodeBuyCall, encodeSellCall, PortalQuery, lpFeeProfileToV3Fee } from './portal-ops.js';
|
|
18
18
|
import { encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, } from './dex.js';
|
|
19
19
|
import { encodeApproveCall } from './portal-ops.js';
|
|
20
|
-
import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, } from './constants.js';
|
|
21
|
-
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
20
|
+
import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, POTATOSWAP_V3_FACTORY, } from './constants.js';
|
|
21
|
+
import { PROFIT_CONFIG, ZERO_ADDRESS } from '../utils/constants.js';
|
|
22
22
|
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
23
23
|
// ============================================================================
|
|
24
24
|
// 工具函数
|
|
@@ -63,6 +63,69 @@ function normalizeConfig(cfg) {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
// ============================================================================
|
|
66
|
+
// V3 报价工具函数
|
|
67
|
+
// ============================================================================
|
|
68
|
+
const V3_FACTORY_ABI = [
|
|
69
|
+
'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)',
|
|
70
|
+
];
|
|
71
|
+
const V3_POOL_ABI = [
|
|
72
|
+
'function slot0() view returns (uint160 sqrtPriceX96,int24 tick,uint16 observationIndex,uint16 observationCardinality,uint16 observationCardinalityNext,uint8 feeProtocol,bool unlocked)',
|
|
73
|
+
'function token0() view returns (address)',
|
|
74
|
+
'function token1() view returns (address)',
|
|
75
|
+
];
|
|
76
|
+
const V3_FEE_DENOMINATOR = 1000000n;
|
|
77
|
+
/**
|
|
78
|
+
* 使用 V3 Pool 的 slot0 获取 Token → WOKB 的报价(卖出方向)
|
|
79
|
+
*/
|
|
80
|
+
async function quoteV3SellViaSlot0(params) {
|
|
81
|
+
try {
|
|
82
|
+
const { rpcUrl, tokenAddress, tokenAmount, fee } = params;
|
|
83
|
+
if (!tokenAddress || tokenAmount <= 0n)
|
|
84
|
+
return 0n;
|
|
85
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
86
|
+
const wokbLower = WOKB.toLowerCase();
|
|
87
|
+
const tokenLower = tokenAddress.toLowerCase();
|
|
88
|
+
if (wokbLower === tokenLower)
|
|
89
|
+
return tokenAmount;
|
|
90
|
+
const factory = new ethers.Contract(POTATOSWAP_V3_FACTORY, V3_FACTORY_ABI, provider);
|
|
91
|
+
const poolAddr = await factory.getPool(tokenAddress, WOKB, fee);
|
|
92
|
+
if (!poolAddr || poolAddr.toLowerCase() === ZERO_ADDRESS.toLowerCase())
|
|
93
|
+
return 0n;
|
|
94
|
+
const pool = new ethers.Contract(poolAddr, V3_POOL_ABI, provider);
|
|
95
|
+
const [t0, t1, slot0] = await Promise.all([pool.token0(), pool.token1(), pool.slot0()]);
|
|
96
|
+
if (!t0 || !t1 || !slot0)
|
|
97
|
+
return 0n;
|
|
98
|
+
const sqrtPriceX96 = BigInt(slot0[0]);
|
|
99
|
+
if (sqrtPriceX96 <= 0n)
|
|
100
|
+
return 0n;
|
|
101
|
+
// 扣除手续费
|
|
102
|
+
const amountInLessFee = (tokenAmount * (V3_FEE_DENOMINATOR - BigInt(fee))) / V3_FEE_DENOMINATOR;
|
|
103
|
+
if (amountInLessFee <= 0n)
|
|
104
|
+
return 0n;
|
|
105
|
+
const Q192 = 2n ** 192n;
|
|
106
|
+
const num = sqrtPriceX96 * sqrtPriceX96;
|
|
107
|
+
const t0Lower = String(t0).toLowerCase();
|
|
108
|
+
const t1Lower = String(t1).toLowerCase();
|
|
109
|
+
// sqrtPriceX96 表示 token1/token0 的现货价
|
|
110
|
+
// 卖出方向:Token → WOKB
|
|
111
|
+
if (tokenLower === t0Lower && wokbLower === t1Lower) {
|
|
112
|
+
// Token 是 token0,WOKB 是 token1
|
|
113
|
+
// price = token1/token0 = WOKB/Token, 所以 wokbOut = tokenIn * price = tokenIn * num / Q192
|
|
114
|
+
return (amountInLessFee * num) / Q192;
|
|
115
|
+
}
|
|
116
|
+
if (tokenLower === t1Lower && wokbLower === t0Lower) {
|
|
117
|
+
// Token 是 token1,WOKB 是 token0
|
|
118
|
+
// price = token1/token0 = Token/WOKB, 所以 wokbOut = tokenIn / price = tokenIn * Q192 / num
|
|
119
|
+
return (amountInLessFee * Q192) / num;
|
|
120
|
+
}
|
|
121
|
+
return 0n;
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
console.warn('[quoteV3SellViaSlot0] 报价失败:', e);
|
|
125
|
+
return 0n;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ============================================================================
|
|
66
129
|
// 买入 UserOps 构建
|
|
67
130
|
// ============================================================================
|
|
68
131
|
/**
|
|
@@ -337,6 +400,21 @@ export async function buildBundleSellOps(params) {
|
|
|
337
400
|
targetContract = portal;
|
|
338
401
|
}
|
|
339
402
|
else if (poolType === 'v3') {
|
|
403
|
+
// ✅ V3 卖出报价:使用 slot0 计算预期输出,并应用 2% 滑点保护
|
|
404
|
+
let amountOutMinimum = 0n;
|
|
405
|
+
try {
|
|
406
|
+
const quotedOut = await quoteV3SellViaSlot0({
|
|
407
|
+
rpcUrl: config.rpcUrl,
|
|
408
|
+
tokenAddress: params.tokenAddress,
|
|
409
|
+
tokenAmount: sellAmount,
|
|
410
|
+
fee: v3Fee,
|
|
411
|
+
});
|
|
412
|
+
// 应用 2% 滑点保护
|
|
413
|
+
amountOutMinimum = (quotedOut * 98n) / 100n;
|
|
414
|
+
}
|
|
415
|
+
catch (e) {
|
|
416
|
+
console.warn(`[buildBundleSellOps] V3 卖出报价失败,使用 amountOutMinimum=0:`, e);
|
|
417
|
+
}
|
|
340
418
|
swapData = encodeSwapExactTokensForETHV3({
|
|
341
419
|
tokenIn: params.tokenAddress,
|
|
342
420
|
tokenOut: wokb,
|
|
@@ -345,7 +423,7 @@ export async function buildBundleSellOps(params) {
|
|
|
345
423
|
unwrapRecipient: sender,
|
|
346
424
|
deadline,
|
|
347
425
|
amountIn: sellAmount,
|
|
348
|
-
amountOutMinimum
|
|
426
|
+
amountOutMinimum,
|
|
349
427
|
sqrtPriceLimitX96: 0n,
|
|
350
428
|
});
|
|
351
429
|
targetContract = routerAddress;
|
|
@@ -203,10 +203,9 @@ export class AADexSwapExecutor {
|
|
|
203
203
|
initIfNeeded(buyerSender, !!buyerAi?.deployed, buyerOwner.address);
|
|
204
204
|
const decimals = await this.bundleExecutor['getErc20Decimals'](tokenAddress);
|
|
205
205
|
const sellerTokenBal = await this.aaManager.getErc20Balance(tokenAddress, sellerSender);
|
|
206
|
-
// ✅ 当 sellPercent = 100 时直接使用完整余额,避免精度损失
|
|
207
206
|
const sellAmountWei = sellAmount
|
|
208
207
|
? ethers.parseUnits(String(sellAmount), decimals)
|
|
209
|
-
: (
|
|
208
|
+
: (sellerTokenBal * BigInt(sellPercent)) / 100n;
|
|
210
209
|
if (sellAmountWei <= 0n)
|
|
211
210
|
throw new Error('卖出数量无效');
|
|
212
211
|
// 授权检查
|
|
@@ -472,10 +471,9 @@ export class AADexSwapExecutor {
|
|
|
472
471
|
}
|
|
473
472
|
const decimals = await this.bundleExecutor['getErc20Decimals'](tokenAddress);
|
|
474
473
|
const sellerTokenBal = await this.aaManager.getErc20Balance(tokenAddress, sellerAi.sender);
|
|
475
|
-
// ✅ 当 sellPercent = 100 时直接使用完整余额,避免精度损失
|
|
476
474
|
const sellAmountWei = sellAmount
|
|
477
475
|
? ethers.parseUnits(String(sellAmount), decimals)
|
|
478
|
-
: (
|
|
476
|
+
: (sellerTokenBal * BigInt(sellPercent)) / 100n;
|
|
479
477
|
let needApprove = false;
|
|
480
478
|
if (!skipApprovalCheck) {
|
|
481
479
|
const allowance = await this.aaManager.getErc20Allowance(tokenAddress, sellerAi.sender, effectiveRouter);
|
|
@@ -586,32 +584,19 @@ export class AADexSwapExecutor {
|
|
|
586
584
|
const totalBuyerPrefundWithBuffer = (useNativeToken && capitalMode)
|
|
587
585
|
? buyerAis.reduce((sum, ai) => sum + estimateBuyerPrefundWithBuffer(ai.deployed), 0n)
|
|
588
586
|
: 0n;
|
|
589
|
-
// ✅
|
|
590
|
-
|
|
591
|
-
//
|
|
592
|
-
// 这样可以避免 seller 需要用自己的 OKB 补贴分发
|
|
587
|
+
// ✅ 计算利润(不再压缩到“卖出-买入-prefund”上限,按配置直接刮取)
|
|
588
|
+
const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
589
|
+
// 资金充足性提示:不再阻断,记录警告后继续分发
|
|
593
590
|
const totalDistributeNeeded = totalBuyWei + totalBuyerPrefundWithBuffer + profitWei;
|
|
594
|
-
let adjustedBuyAmountsWei = buyAmountsWei;
|
|
595
|
-
let adjustedProfitWei = profitWei;
|
|
596
591
|
if (quotedSellOutWei > 0n && totalDistributeNeeded > quotedSellOutWei) {
|
|
597
|
-
|
|
598
|
-
const safeQuoted = (quotedSellOutWei * 95n) / 100n; // 留 5% 给 seller prefund
|
|
599
|
-
const scaleRatio = safeQuoted > 0n ? (safeQuoted * 10000n) / totalDistributeNeeded : 0n;
|
|
600
|
-
// 按比例缩减 buyAmounts
|
|
601
|
-
adjustedBuyAmountsWei = buyAmountsWei.map(w => (w * scaleRatio) / 10000n);
|
|
602
|
-
const adjustedTotalBuyWei = adjustedBuyAmountsWei.reduce((a, b) => a + b, 0n);
|
|
603
|
-
// 按比例缩减 profit
|
|
604
|
-
adjustedProfitWei = (profitWei * scaleRatio) / 10000n;
|
|
605
|
-
console.warn('[AA DEX 批量换手] ⚠️ 分发金额超过预估卖出,已按比例缩减:', {
|
|
592
|
+
console.warn('[AA DEX 批量换手] ⚠️ 预估卖出金额不足,仍继续分发:', {
|
|
606
593
|
quotedSellOutWei: ethers.formatEther(quotedSellOutWei),
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
594
|
+
totalBuyWei: ethers.formatEther(totalBuyWei),
|
|
595
|
+
totalBuyerPrefund: ethers.formatEther(totalBuyerPrefundWithBuffer),
|
|
596
|
+
profitWei: ethers.formatEther(profitWei),
|
|
597
|
+
totalNeeded: ethers.formatEther(totalDistributeNeeded),
|
|
598
|
+
shortfall: ethers.formatEther(totalDistributeNeeded - quotedSellOutWei),
|
|
612
599
|
});
|
|
613
|
-
// 更新变量
|
|
614
|
-
profitWei = adjustedProfitWei;
|
|
615
600
|
}
|
|
616
601
|
// ✅ 利润在分发阶段通过 AA 内部刮取
|
|
617
602
|
// ✅ 资金分发逻辑(仅 capitalMode=true 时执行,对应 BSC flapQuickBatchSwapMerkle)
|
|
@@ -698,7 +683,7 @@ export class AADexSwapExecutor {
|
|
|
698
683
|
// - 原生代币模式:分发金额 = 买入金额 + 买方 prefund
|
|
699
684
|
// - ERC20 模式:只分发 ERC20 代币,prefund 需要 buyer 自己有 OKB
|
|
700
685
|
const items = buyerSenders.map((to, i) => {
|
|
701
|
-
const buyAmount =
|
|
686
|
+
const buyAmount = buyAmountsWei[i] ?? 0n;
|
|
702
687
|
const prefund = buyerPrefunds[i] ?? 0n;
|
|
703
688
|
return {
|
|
704
689
|
to,
|
|
@@ -870,7 +855,7 @@ export class AADexSwapExecutor {
|
|
|
870
855
|
const chainHops = allGeneratedHopWallets[buyerIdx];
|
|
871
856
|
const buyerSender = buyerSenders[buyerIdx];
|
|
872
857
|
const buyerAi = buyerAis[buyerIdx];
|
|
873
|
-
const buyAmount =
|
|
858
|
+
const buyAmount = buyAmountsWei[buyerIdx] ?? 0n;
|
|
874
859
|
if (buyAmount <= 0n)
|
|
875
860
|
continue;
|
|
876
861
|
// ✅ 计算 buyer 的 prefund(用于买入,使用常量)
|
|
@@ -1016,7 +1001,7 @@ export class AADexSwapExecutor {
|
|
|
1016
1001
|
// 不使用 exactOutput,因为卖出后价格下跌,可能导致 OKB 不够买回目标数量
|
|
1017
1002
|
for (let i = 0; i < buyerOwners.length; i++) {
|
|
1018
1003
|
const ai = buyerAis[i];
|
|
1019
|
-
const buyWei =
|
|
1004
|
+
const buyWei = buyAmountsWei[i]; // 分发给买家的 OKB 金额
|
|
1020
1005
|
let buyCallData;
|
|
1021
1006
|
if (useNativeToken) {
|
|
1022
1007
|
// ✅ 原生代币模式:OKB → Token
|
package/dist/xlayer/wash-ops.js
CHANGED
|
@@ -16,10 +16,73 @@ import { ethers } from 'ethers';
|
|
|
16
16
|
import { createAAAccountManager, encodeExecute, createWallet } from './aa-account.js';
|
|
17
17
|
import { encodeBuyCall, encodeSellCall, PortalQuery, lpFeeProfileToV3Fee } from './portal-ops.js';
|
|
18
18
|
import { encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, DexQuery, } from './dex.js';
|
|
19
|
-
import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, } from './constants.js';
|
|
20
|
-
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
19
|
+
import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, POTATOSWAP_V3_FACTORY, } from './constants.js';
|
|
20
|
+
import { PROFIT_CONFIG, ZERO_ADDRESS } from '../utils/constants.js';
|
|
21
21
|
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
22
22
|
// ============================================================================
|
|
23
|
+
// V3 报价工具函数
|
|
24
|
+
// ============================================================================
|
|
25
|
+
const V3_FACTORY_ABI = [
|
|
26
|
+
'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)',
|
|
27
|
+
];
|
|
28
|
+
const V3_POOL_ABI = [
|
|
29
|
+
'function slot0() view returns (uint160 sqrtPriceX96,int24 tick,uint16 observationIndex,uint16 observationCardinality,uint16 observationCardinalityNext,uint8 feeProtocol,bool unlocked)',
|
|
30
|
+
'function token0() view returns (address)',
|
|
31
|
+
'function token1() view returns (address)',
|
|
32
|
+
];
|
|
33
|
+
const V3_FEE_DENOMINATOR = 1000000n;
|
|
34
|
+
/**
|
|
35
|
+
* 使用 V3 Pool 的 slot0 获取 WOKB → Token 的报价(买入方向)
|
|
36
|
+
*/
|
|
37
|
+
async function quoteV3BuyViaSlot0(params) {
|
|
38
|
+
try {
|
|
39
|
+
const { rpcUrl, tokenAddress, wokbAmount, fee } = params;
|
|
40
|
+
if (!tokenAddress || wokbAmount <= 0n)
|
|
41
|
+
return 0n;
|
|
42
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
43
|
+
const wokbLower = WOKB.toLowerCase();
|
|
44
|
+
const tokenLower = tokenAddress.toLowerCase();
|
|
45
|
+
if (wokbLower === tokenLower)
|
|
46
|
+
return wokbAmount;
|
|
47
|
+
const factory = new ethers.Contract(POTATOSWAP_V3_FACTORY, V3_FACTORY_ABI, provider);
|
|
48
|
+
const poolAddr = await factory.getPool(tokenAddress, WOKB, fee);
|
|
49
|
+
if (!poolAddr || poolAddr.toLowerCase() === ZERO_ADDRESS.toLowerCase())
|
|
50
|
+
return 0n;
|
|
51
|
+
const pool = new ethers.Contract(poolAddr, V3_POOL_ABI, provider);
|
|
52
|
+
const [t0, t1, slot0] = await Promise.all([pool.token0(), pool.token1(), pool.slot0()]);
|
|
53
|
+
if (!t0 || !t1 || !slot0)
|
|
54
|
+
return 0n;
|
|
55
|
+
const sqrtPriceX96 = BigInt(slot0[0]);
|
|
56
|
+
if (sqrtPriceX96 <= 0n)
|
|
57
|
+
return 0n;
|
|
58
|
+
// 扣除手续费
|
|
59
|
+
const amountInLessFee = (wokbAmount * (V3_FEE_DENOMINATOR - BigInt(fee))) / V3_FEE_DENOMINATOR;
|
|
60
|
+
if (amountInLessFee <= 0n)
|
|
61
|
+
return 0n;
|
|
62
|
+
const Q192 = 2n ** 192n;
|
|
63
|
+
const num = sqrtPriceX96 * sqrtPriceX96;
|
|
64
|
+
const t0Lower = String(t0).toLowerCase();
|
|
65
|
+
const t1Lower = String(t1).toLowerCase();
|
|
66
|
+
// sqrtPriceX96 表示 token1/token0 的现货价
|
|
67
|
+
// 买入方向:WOKB → Token
|
|
68
|
+
if (wokbLower === t0Lower && tokenLower === t1Lower) {
|
|
69
|
+
// WOKB 是 token0,Token 是 token1
|
|
70
|
+
// price = token1/token0, 所以 tokenOut = wokbIn * price = wokbIn * num / Q192
|
|
71
|
+
return (amountInLessFee * num) / Q192;
|
|
72
|
+
}
|
|
73
|
+
if (wokbLower === t1Lower && tokenLower === t0Lower) {
|
|
74
|
+
// WOKB 是 token1,Token 是 token0
|
|
75
|
+
// price = token1/token0 = WOKB/Token, 所以 tokenOut = wokbIn / price = wokbIn * Q192 / num
|
|
76
|
+
return (amountInLessFee * Q192) / num;
|
|
77
|
+
}
|
|
78
|
+
return 0n;
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
console.warn('[quoteV3BuyViaSlot0] 报价失败:', e);
|
|
82
|
+
return 0n;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ============================================================================
|
|
23
86
|
// 工具函数
|
|
24
87
|
// ============================================================================
|
|
25
88
|
/**
|
|
@@ -203,13 +266,19 @@ export async function buildWashOps(params) {
|
|
|
203
266
|
});
|
|
204
267
|
}
|
|
205
268
|
else if (poolType === 'v3') {
|
|
206
|
-
// V3 使用
|
|
207
|
-
const portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
|
|
269
|
+
// ✅ V3 使用 slot0 现货价计算预期代币输出(与 dex-bundle-swap 保持一致)
|
|
208
270
|
expectedTokenAmounts = await mapWithConcurrency(buyWeiList, 4, async (buyWei) => {
|
|
209
271
|
if (buyWei <= 0n)
|
|
210
272
|
return 0n;
|
|
211
273
|
try {
|
|
212
|
-
|
|
274
|
+
const quoted = await quoteV3BuyViaSlot0({
|
|
275
|
+
rpcUrl: config.rpcUrl,
|
|
276
|
+
tokenAddress: params.tokenAddress,
|
|
277
|
+
wokbAmount: buyWei,
|
|
278
|
+
fee: v3Fee,
|
|
279
|
+
});
|
|
280
|
+
// 应用 2% 滑点保护,避免因价格波动导致卖出失败
|
|
281
|
+
return (quoted * 98n) / 100n;
|
|
213
282
|
}
|
|
214
283
|
catch {
|
|
215
284
|
return 0n;
|
|
@@ -276,6 +345,7 @@ export async function buildWashOps(params) {
|
|
|
276
345
|
fee: v3Fee,
|
|
277
346
|
recipient: sender, // ✅ 使用已验证的 sender 变量
|
|
278
347
|
unwrapRecipient: sender, // ✅ 使用已验证的 sender 变量
|
|
348
|
+
routerAddress, // ✅ 传递正确的 Router 地址
|
|
279
349
|
deadline,
|
|
280
350
|
amountIn: expectedTokenAmount,
|
|
281
351
|
amountOutMinimum: 0n,
|