four-flap-meme-sdk 1.6.64 → 1.6.66
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,7 +17,7 @@ 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, } from './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, } from './constants.js';
|
|
21
21
|
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
22
22
|
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
23
23
|
// ============================================================================
|
|
@@ -73,11 +73,20 @@ export async function buildBundleBuyOps(params) {
|
|
|
73
73
|
const wokb = params.wrappedOkbAddress || WOKB;
|
|
74
74
|
const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
|
|
75
75
|
const portal = FLAP_PORTAL;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
76
|
+
// ✅ V2/V3 模式使用默认 Router 地址(与 bundle.ts 保持一致)
|
|
77
|
+
let routerAddress = params.routerAddress || '';
|
|
78
|
+
if (poolType === 'v3') {
|
|
79
|
+
if (!routerAddress || routerAddress.toLowerCase() !== POTATOSWAP_V3_ROUTER.toLowerCase()) {
|
|
80
|
+
if (routerAddress && routerAddress !== POTATOSWAP_V3_ROUTER) {
|
|
81
|
+
console.warn(`[buildBundleBuyOps] V3 Router 校正: 前端传入 ${routerAddress} → 使用正确的 ${POTATOSWAP_V3_ROUTER}`);
|
|
82
|
+
}
|
|
83
|
+
routerAddress = POTATOSWAP_V3_ROUTER;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (poolType === 'v2' && !routerAddress) {
|
|
87
|
+
routerAddress = POTATOSWAP_V2_ROUTER;
|
|
80
88
|
}
|
|
89
|
+
// 参数校验
|
|
81
90
|
if (params.ownerPrivateKeys.length !== params.buyAmountsOkb.length) {
|
|
82
91
|
throw new Error('[buildBundleBuyOps] 私钥数量与买入金额数量不一致');
|
|
83
92
|
}
|
|
@@ -240,11 +249,20 @@ export async function buildBundleSellOps(params) {
|
|
|
240
249
|
const wokb = params.wrappedOkbAddress || WOKB;
|
|
241
250
|
const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
|
|
242
251
|
const portal = FLAP_PORTAL;
|
|
243
|
-
const routerAddress = params.routerAddress || '';
|
|
244
252
|
const tokenDecimals = params.tokenDecimals ?? 18;
|
|
245
253
|
const nonceOffsetMap = params.nonceOffsetMap ?? new Map();
|
|
246
|
-
|
|
247
|
-
|
|
254
|
+
// ✅ V2/V3 模式使用默认 Router 地址(与 bundle.ts 保持一致)
|
|
255
|
+
let routerAddress = params.routerAddress || '';
|
|
256
|
+
if (poolType === 'v3') {
|
|
257
|
+
if (!routerAddress || routerAddress.toLowerCase() !== POTATOSWAP_V3_ROUTER.toLowerCase()) {
|
|
258
|
+
if (routerAddress && routerAddress !== POTATOSWAP_V3_ROUTER) {
|
|
259
|
+
console.warn(`[buildBundleSellOps] V3 Router 校正: 前端传入 ${routerAddress} → 使用正确的 ${POTATOSWAP_V3_ROUTER}`);
|
|
260
|
+
}
|
|
261
|
+
routerAddress = POTATOSWAP_V3_ROUTER;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (poolType === 'v2' && !routerAddress) {
|
|
265
|
+
routerAddress = POTATOSWAP_V2_ROUTER;
|
|
248
266
|
}
|
|
249
267
|
if (params.ownerPrivateKeys.length !== params.sellAmounts.length) {
|
|
250
268
|
throw new Error('[buildBundleSellOps] 私钥数量与卖出金额数量不一致');
|
|
@@ -13,8 +13,7 @@ const DEX_BUY_CALL_GAS_LIMIT = 650000n; // DEX V3 买入操作
|
|
|
13
13
|
const PREFUND_BUFFER_PERCENT = 200n; // prefund buffer 百分比 (200 = 2.0x)
|
|
14
14
|
import { AAAccountManager, encodeExecute, encodeExecuteBatch, encodeExecuteViaMulticall3 } from './aa-account.js';
|
|
15
15
|
import { encodeApproveCall, lpFeeProfileToV3Fee, } from './portal-ops.js';
|
|
16
|
-
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, encodeSwapExactTokensForTokensSupportingFee,
|
|
17
|
-
} from './dex.js';
|
|
16
|
+
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, encodeSwapExactTokensForTokensSupportingFee, } from './dex.js';
|
|
18
17
|
import { BundleExecutor } from './bundle.js';
|
|
19
18
|
import { PROFIT_CONFIG, ZERO_ADDRESS } from '../utils/constants.js';
|
|
20
19
|
const multicallIface = new ethers.Interface([
|
|
@@ -204,9 +203,10 @@ export class AADexSwapExecutor {
|
|
|
204
203
|
initIfNeeded(buyerSender, !!buyerAi?.deployed, buyerOwner.address);
|
|
205
204
|
const decimals = await this.bundleExecutor['getErc20Decimals'](tokenAddress);
|
|
206
205
|
const sellerTokenBal = await this.aaManager.getErc20Balance(tokenAddress, sellerSender);
|
|
206
|
+
// ✅ 当 sellPercent = 100 时直接使用完整余额,避免精度损失
|
|
207
207
|
const sellAmountWei = sellAmount
|
|
208
208
|
? ethers.parseUnits(String(sellAmount), decimals)
|
|
209
|
-
: (sellerTokenBal * BigInt(sellPercent)) / 100n;
|
|
209
|
+
: (sellPercent >= 100 ? sellerTokenBal : (sellerTokenBal * BigInt(sellPercent)) / 100n);
|
|
210
210
|
if (sellAmountWei <= 0n)
|
|
211
211
|
throw new Error('卖出数量无效');
|
|
212
212
|
// 授权检查
|
|
@@ -472,9 +472,10 @@ export class AADexSwapExecutor {
|
|
|
472
472
|
}
|
|
473
473
|
const decimals = await this.bundleExecutor['getErc20Decimals'](tokenAddress);
|
|
474
474
|
const sellerTokenBal = await this.aaManager.getErc20Balance(tokenAddress, sellerAi.sender);
|
|
475
|
+
// ✅ 当 sellPercent = 100 时直接使用完整余额,避免精度损失
|
|
475
476
|
const sellAmountWei = sellAmount
|
|
476
477
|
? ethers.parseUnits(String(sellAmount), decimals)
|
|
477
|
-
: (sellerTokenBal * BigInt(sellPercent)) / 100n;
|
|
478
|
+
: (sellPercent >= 100 ? sellerTokenBal : (sellerTokenBal * BigInt(sellPercent)) / 100n);
|
|
478
479
|
let needApprove = false;
|
|
479
480
|
if (!skipApprovalCheck) {
|
|
480
481
|
const allowance = await this.aaManager.getErc20Allowance(tokenAddress, sellerAi.sender, effectiveRouter);
|
|
@@ -585,19 +586,32 @@ export class AADexSwapExecutor {
|
|
|
585
586
|
const totalBuyerPrefundWithBuffer = (useNativeToken && capitalMode)
|
|
586
587
|
? buyerAis.reduce((sum, ai) => sum + estimateBuyerPrefundWithBuffer(ai.deployed), 0n)
|
|
587
588
|
: 0n;
|
|
588
|
-
// ✅
|
|
589
|
-
|
|
590
|
-
//
|
|
589
|
+
// ✅ 计算利润(不再压缩到"卖出-买入-prefund"上限,按配置直接刮取)
|
|
590
|
+
let profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
591
|
+
// ✅ 关键修复:如果分发总额 > 预估卖出所得,按比例缩减分发金额
|
|
592
|
+
// 这样可以避免 seller 需要用自己的 OKB 补贴分发
|
|
591
593
|
const totalDistributeNeeded = totalBuyWei + totalBuyerPrefundWithBuffer + profitWei;
|
|
594
|
+
let adjustedBuyAmountsWei = buyAmountsWei;
|
|
595
|
+
let adjustedProfitWei = profitWei;
|
|
592
596
|
if (quotedSellOutWei > 0n && totalDistributeNeeded > quotedSellOutWei) {
|
|
593
|
-
|
|
597
|
+
// 计算缩减比例,留 5% 安全边际给 seller 的 prefund
|
|
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 批量换手] ⚠️ 分发金额超过预估卖出,已按比例缩减:', {
|
|
594
606
|
quotedSellOutWei: ethers.formatEther(quotedSellOutWei),
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
607
|
+
originalTotalBuyWei: ethers.formatEther(totalBuyWei),
|
|
608
|
+
adjustedTotalBuyWei: ethers.formatEther(adjustedTotalBuyWei),
|
|
609
|
+
originalProfitWei: ethers.formatEther(profitWei),
|
|
610
|
+
adjustedProfitWei: ethers.formatEther(adjustedProfitWei),
|
|
611
|
+
scaleRatio: `${Number(scaleRatio) / 100}%`,
|
|
600
612
|
});
|
|
613
|
+
// 更新变量
|
|
614
|
+
profitWei = adjustedProfitWei;
|
|
601
615
|
}
|
|
602
616
|
// ✅ 利润在分发阶段通过 AA 内部刮取
|
|
603
617
|
// ✅ 资金分发逻辑(仅 capitalMode=true 时执行,对应 BSC flapQuickBatchSwapMerkle)
|
|
@@ -684,7 +698,7 @@ export class AADexSwapExecutor {
|
|
|
684
698
|
// - 原生代币模式:分发金额 = 买入金额 + 买方 prefund
|
|
685
699
|
// - ERC20 模式:只分发 ERC20 代币,prefund 需要 buyer 自己有 OKB
|
|
686
700
|
const items = buyerSenders.map((to, i) => {
|
|
687
|
-
const buyAmount =
|
|
701
|
+
const buyAmount = adjustedBuyAmountsWei[i] ?? 0n;
|
|
688
702
|
const prefund = buyerPrefunds[i] ?? 0n;
|
|
689
703
|
return {
|
|
690
704
|
to,
|
|
@@ -856,7 +870,7 @@ export class AADexSwapExecutor {
|
|
|
856
870
|
const chainHops = allGeneratedHopWallets[buyerIdx];
|
|
857
871
|
const buyerSender = buyerSenders[buyerIdx];
|
|
858
872
|
const buyerAi = buyerAis[buyerIdx];
|
|
859
|
-
const buyAmount =
|
|
873
|
+
const buyAmount = adjustedBuyAmountsWei[buyerIdx] ?? 0n;
|
|
860
874
|
if (buyAmount <= 0n)
|
|
861
875
|
continue;
|
|
862
876
|
// ✅ 计算 buyer 的 prefund(用于买入,使用常量)
|
|
@@ -997,66 +1011,36 @@ export class AADexSwapExecutor {
|
|
|
997
1011
|
});
|
|
998
1012
|
outOps.push(signedProfitTransfer.userOp);
|
|
999
1013
|
}
|
|
1000
|
-
// ✅ 计算每个买家应该买入的代币数量(用于 V3 exactOutput 模式)
|
|
1001
|
-
// 逻辑:卖出 sellAmountWei 个代币 → 买家按比例买回
|
|
1002
|
-
// targetTokenAmounts[i] = sellAmountWei * (buyAmountsWei[i] / totalBuyWei)
|
|
1003
|
-
//
|
|
1004
|
-
// exactOutput 模式优势:
|
|
1005
|
-
// 1. 精确控制每个买家买入的代币数量
|
|
1006
|
-
// 2. amountInMaximum 设为分发金额,确保不超支
|
|
1007
|
-
// 3. 多余的 OKB 通过 refundETH 退回买家钱包
|
|
1008
|
-
const targetTokenAmounts = capitalMode && totalBuyWei > 0n
|
|
1009
|
-
? buyAmountsWei.map(buyWei => (sellAmountWei * buyWei) / totalBuyWei)
|
|
1010
|
-
: buyAmountsWei.map(() => 0n); // 非资金利用率模式不使用 exactOutput
|
|
1011
|
-
console.log('[AA DEX 批量换手] 目标代币数量计算:', {
|
|
1012
|
-
sellAmountWei: sellAmountWei.toString(),
|
|
1013
|
-
totalBuyWei: ethers.formatEther(totalBuyWei),
|
|
1014
|
-
targetTokenAmounts: targetTokenAmounts.map(a => a.toString()),
|
|
1015
|
-
useExactOutput: capitalMode && isV3Trade && useNativeToken,
|
|
1016
|
-
});
|
|
1017
1014
|
// Batch Buy ops(分发后再执行)- ✅ 支持 V2/V3,支持 ERC20 稳定币
|
|
1015
|
+
// ⚠️ 使用 exactInput 模式:用分发到的 OKB 尽可能多地买代币
|
|
1016
|
+
// 不使用 exactOutput,因为卖出后价格下跌,可能导致 OKB 不够买回目标数量
|
|
1018
1017
|
for (let i = 0; i < buyerOwners.length; i++) {
|
|
1019
1018
|
const ai = buyerAis[i];
|
|
1020
|
-
const buyWei =
|
|
1021
|
-
const targetTokenOut = targetTokenAmounts[i]; // 目标买入的代币数量
|
|
1019
|
+
const buyWei = adjustedBuyAmountsWei[i]; // 分发给买家的 OKB 金额(已按比例调整)
|
|
1022
1020
|
let buyCallData;
|
|
1023
1021
|
if (useNativeToken) {
|
|
1024
1022
|
// ✅ 原生代币模式:OKB → Token
|
|
1023
|
+
// ⚠️ 重要:使用 exactInput 而不是 exactOutput!
|
|
1024
|
+
// 原因:卖出后价格下跌,exactOutput 需要更多 OKB 买回相同数量代币
|
|
1025
|
+
// 如果 OKB 不够,exactOutput 会 revert,导致买入失败
|
|
1026
|
+
// 解决方案:使用 exactInput,让买家用分到的 OKB 尽可能多地买代币
|
|
1025
1027
|
let buySwapData;
|
|
1026
1028
|
if (isV3Trade) {
|
|
1027
|
-
//
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
amountInMaximum: buyWei, // ✅ 最多花费的 OKB(分发金额)
|
|
1040
|
-
sqrtPriceLimitX96: 0n,
|
|
1041
|
-
});
|
|
1042
|
-
console.log(`[AA DEX V3 exactOutput] Buyer ${i}: 目标代币 ${targetTokenOut.toString()}, 最大花费 ${ethers.formatEther(buyWei)} OKB`);
|
|
1043
|
-
}
|
|
1044
|
-
else {
|
|
1045
|
-
// 非资金利用率模式:使用 exactInput(固定 OKB 买尽可能多的代币)
|
|
1046
|
-
buySwapData = encodeSwapExactETHForTokensV3({
|
|
1047
|
-
tokenIn: WOKB,
|
|
1048
|
-
tokenOut: tokenAddress,
|
|
1049
|
-
fee: lpFeeProfileToV3Fee(lpFeeProfile),
|
|
1050
|
-
recipient: ai.sender,
|
|
1051
|
-
deadline,
|
|
1052
|
-
amountIn: buyWei,
|
|
1053
|
-
amountOutMinimum: 0n,
|
|
1054
|
-
sqrtPriceLimitX96: 0n,
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1029
|
+
// V3 模式:使用 exactInput(固定 OKB 买尽可能多的代币)
|
|
1030
|
+
buySwapData = encodeSwapExactETHForTokensV3({
|
|
1031
|
+
tokenIn: WOKB,
|
|
1032
|
+
tokenOut: tokenAddress,
|
|
1033
|
+
fee: lpFeeProfileToV3Fee(lpFeeProfile),
|
|
1034
|
+
recipient: ai.sender,
|
|
1035
|
+
deadline,
|
|
1036
|
+
amountIn: buyWei,
|
|
1037
|
+
amountOutMinimum: 0n, // 不设最小输出,确保交易成功
|
|
1038
|
+
sqrtPriceLimitX96: 0n,
|
|
1039
|
+
});
|
|
1040
|
+
console.log(`[AA DEX V3 exactInput] Buyer ${i}: 花费 ${ethers.formatEther(buyWei)} OKB 买入代币`);
|
|
1057
1041
|
}
|
|
1058
1042
|
else {
|
|
1059
|
-
// V2
|
|
1043
|
+
// V2 模式:使用 exactInput
|
|
1060
1044
|
buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], ai.sender, deadline);
|
|
1061
1045
|
}
|
|
1062
1046
|
buyCallData = encodeExecute(effectiveRouter, buyWei, buySwapData);
|
package/dist/xlayer/wash-ops.js
CHANGED
|
@@ -16,7 +16,7 @@ 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, } from './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, } from './constants.js';
|
|
20
20
|
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
21
21
|
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
22
22
|
// ============================================================================
|
|
@@ -79,14 +79,26 @@ export async function buildWashOps(params) {
|
|
|
79
79
|
const wokb = params.wrappedOkbAddress || WOKB;
|
|
80
80
|
const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
|
|
81
81
|
const portal = FLAP_PORTAL;
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
// ✅ V2/V3 模式使用默认 Router 地址(与 bundle.ts 保持一致)
|
|
83
|
+
// 避免前端传入错误地址导致交易失败
|
|
84
|
+
let routerAddress = params.routerAddress || '';
|
|
85
|
+
if (poolType === 'v3') {
|
|
86
|
+
// V3 模式:强制使用正确的 PotatoSwap V3 Router
|
|
87
|
+
if (!routerAddress || routerAddress.toLowerCase() !== POTATOSWAP_V3_ROUTER.toLowerCase()) {
|
|
88
|
+
if (routerAddress && routerAddress !== POTATOSWAP_V3_ROUTER) {
|
|
89
|
+
console.warn(`[buildWashOps] V3 Router 校正: 前端传入 ${routerAddress} → 使用正确的 ${POTATOSWAP_V3_ROUTER}`);
|
|
90
|
+
}
|
|
91
|
+
routerAddress = POTATOSWAP_V3_ROUTER;
|
|
92
|
+
}
|
|
86
93
|
}
|
|
87
|
-
if (poolType === '
|
|
88
|
-
|
|
94
|
+
else if (poolType === 'v2') {
|
|
95
|
+
// V2 模式:使用默认 PotatoSwap V2 Router(如果未提供)
|
|
96
|
+
if (!routerAddress) {
|
|
97
|
+
routerAddress = POTATOSWAP_V2_ROUTER;
|
|
98
|
+
console.log(`[buildWashOps] V2 Router 使用默认值: ${routerAddress}`);
|
|
99
|
+
}
|
|
89
100
|
}
|
|
101
|
+
// 参数校验
|
|
90
102
|
if (params.ownerPrivateKeys.length !== params.buyAmountsOkb.length) {
|
|
91
103
|
throw new Error('[buildWashOps] 私钥数量与买入金额数量不一致');
|
|
92
104
|
}
|