four-flap-meme-sdk 1.6.60 → 1.6.63
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.
- package/dist/xlayer/aa-account.js +10 -2
- package/dist/xlayer/bundle-buy-first.js +33 -3
- package/dist/xlayer/dex-bundle-swap.js +91 -49
- package/dist/xlayer/dex.d.ts +37 -1
- package/dist/xlayer/dex.js +47 -1
- package/dist/xlayer/portal-bundle-swap.js +3 -3
- package/dist/xlayer/types.d.ts +2 -0
- package/dist/xlayer/wash-ops.js +18 -5
- package/package.json +1 -1
- package/dist/flap/portal-bundle-merkle/encryption.d.ts +0 -16
- package/dist/flap/portal-bundle-merkle/encryption.js +0 -146
|
@@ -503,8 +503,16 @@ export class AAAccountManager {
|
|
|
503
503
|
*/
|
|
504
504
|
async buildUserOpWithFixedGas(params) {
|
|
505
505
|
await this.syncEntryPointIfNeeded();
|
|
506
|
-
|
|
507
|
-
|
|
506
|
+
// ✅ 关键修复:优先使用传入的 fixedGas.gasPrice,确保整个流程使用一致的 gasPrice
|
|
507
|
+
// 这样可以避免 feeData 缓存过期导致的 gasPrice 不一致问题
|
|
508
|
+
let legacyGasPrice;
|
|
509
|
+
if (params.fixedGas?.gasPrice !== undefined) {
|
|
510
|
+
legacyGasPrice = params.fixedGas.gasPrice;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
const feeData = await this.getFeeData();
|
|
514
|
+
legacyGasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? DEFAULT_GAS_PRICE;
|
|
515
|
+
}
|
|
508
516
|
const paymasterAndData = this.buildPaymasterAndData();
|
|
509
517
|
const callGasLimit = params.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_SELL;
|
|
510
518
|
const verificationGasLimit = params.deployed
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { ethers } from 'ethers';
|
|
16
16
|
import { createAAAccountManager, encodeExecute, createWallet } from './aa-account.js';
|
|
17
|
-
import { encodeBuyCall, encodeSellCall } from './portal-ops.js';
|
|
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
20
|
import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, } from './constants.js';
|
|
@@ -71,7 +71,6 @@ function normalizeConfig(cfg) {
|
|
|
71
71
|
export async function buildBundleBuyOps(params) {
|
|
72
72
|
const poolType = params.poolType || 'flap';
|
|
73
73
|
const wokb = params.wrappedOkbAddress || WOKB;
|
|
74
|
-
const v3Fee = params.v3Fee || 2500;
|
|
75
74
|
const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
|
|
76
75
|
const portal = FLAP_PORTAL;
|
|
77
76
|
const routerAddress = params.routerAddress || '';
|
|
@@ -85,6 +84,22 @@ export async function buildBundleBuyOps(params) {
|
|
|
85
84
|
// ✅ 归一化配置,填充默认值(与前端 normalizeXLayerAAConfig 保持一致)
|
|
86
85
|
const config = normalizeConfig(params.config);
|
|
87
86
|
const aaManager = createAAAccountManager(config);
|
|
87
|
+
// ✅ V3 模式:直接从链上读取 lpFeeProfile 获取正确的 fee(与 bundleGraduateBuy 保持一致)
|
|
88
|
+
let v3Fee = params.v3Fee || 2500;
|
|
89
|
+
if (poolType === 'v3') {
|
|
90
|
+
try {
|
|
91
|
+
const portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
|
|
92
|
+
const tokenState = await portalQuery.getTokenV7(params.tokenAddress);
|
|
93
|
+
const correctV3Fee = lpFeeProfileToV3Fee(tokenState.lpFeeProfile);
|
|
94
|
+
if (correctV3Fee !== v3Fee) {
|
|
95
|
+
console.log(`[buildBundleBuyOps] V3 Fee 校正: 前端传入 ${v3Fee}, 链上 lpFeeProfile=${tokenState.lpFeeProfile} → 正确 fee=${correctV3Fee}`);
|
|
96
|
+
}
|
|
97
|
+
v3Fee = correctV3Fee;
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
console.warn(`[buildBundleBuyOps] 读取 lpFeeProfile 失败,使用前端传入的 v3Fee=${v3Fee}:`, e);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
88
103
|
const ownerWallets = params.ownerPrivateKeys.map(pk => createWallet(pk, config));
|
|
89
104
|
const owners = ownerWallets.map(w => w.address);
|
|
90
105
|
const accounts = await aaManager.getMultipleAccountInfo(owners);
|
|
@@ -223,7 +238,6 @@ export async function buildBundleBuyOps(params) {
|
|
|
223
238
|
export async function buildBundleSellOps(params) {
|
|
224
239
|
const poolType = params.poolType || 'flap';
|
|
225
240
|
const wokb = params.wrappedOkbAddress || WOKB;
|
|
226
|
-
const v3Fee = params.v3Fee || 2500;
|
|
227
241
|
const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
|
|
228
242
|
const portal = FLAP_PORTAL;
|
|
229
243
|
const routerAddress = params.routerAddress || '';
|
|
@@ -238,6 +252,22 @@ export async function buildBundleSellOps(params) {
|
|
|
238
252
|
// ✅ 归一化配置,填充默认值(与前端 normalizeXLayerAAConfig 保持一致)
|
|
239
253
|
const config = normalizeConfig(params.config);
|
|
240
254
|
const aaManager = createAAAccountManager(config);
|
|
255
|
+
// ✅ V3 模式:直接从链上读取 lpFeeProfile 获取正确的 fee(与 bundleGraduateBuy 保持一致)
|
|
256
|
+
let v3Fee = params.v3Fee || 2500;
|
|
257
|
+
if (poolType === 'v3') {
|
|
258
|
+
try {
|
|
259
|
+
const portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
|
|
260
|
+
const tokenState = await portalQuery.getTokenV7(params.tokenAddress);
|
|
261
|
+
const correctV3Fee = lpFeeProfileToV3Fee(tokenState.lpFeeProfile);
|
|
262
|
+
if (correctV3Fee !== v3Fee) {
|
|
263
|
+
console.log(`[buildBundleSellOps] V3 Fee 校正: 前端传入 ${v3Fee}, 链上 lpFeeProfile=${tokenState.lpFeeProfile} → 正确 fee=${correctV3Fee}`);
|
|
264
|
+
}
|
|
265
|
+
v3Fee = correctV3Fee;
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
console.warn(`[buildBundleSellOps] 读取 lpFeeProfile 失败,使用前端传入的 v3Fee=${v3Fee}:`, e);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
241
271
|
const ownerWallets = params.ownerPrivateKeys.map(pk => createWallet(pk, config));
|
|
242
272
|
const owners = ownerWallets.map(w => w.address);
|
|
243
273
|
const accounts = await aaManager.getMultipleAccountInfo(owners);
|
|
@@ -7,12 +7,13 @@ import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, POTATOSWAP_V3_FACTORY, WOKB
|
|
|
7
7
|
// 多跳 prefund 估算用的 gas 常量(可配置)
|
|
8
8
|
const HOP_CALL_GAS_LIMIT = 150000n; // hop 转账操作
|
|
9
9
|
const DEX_BUY_CALL_GAS_LIMIT = 650000n; // DEX V3 买入操作
|
|
10
|
-
// ✅ 增加余额检查 buffer 从
|
|
11
|
-
// 预充值使用 200% buffer,这里使用
|
|
12
|
-
const PREFUND_BUFFER_PERCENT =
|
|
10
|
+
// ✅ 增加余额检查 buffer 从 180% 到 250%,确保 gasPrice 波动时也有足够余额
|
|
11
|
+
// 预充值使用 200% buffer,这里使用 250% 作为安全阈值(覆盖更大的 gasPrice 波动)
|
|
12
|
+
const PREFUND_BUFFER_PERCENT = 250n; // prefund buffer 百分比 (250 = 2.5x)
|
|
13
13
|
import { AAAccountManager, encodeExecute, encodeExecuteBatch, encodeExecuteViaMulticall3 } from './aa-account.js';
|
|
14
14
|
import { encodeApproveCall, lpFeeProfileToV3Fee, } from './portal-ops.js';
|
|
15
|
-
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, encodeSwapExactTokensForTokensSupportingFee,
|
|
15
|
+
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, encodeSwapExactTokensForTokensSupportingFee, encodeSwapExactOutputForTokensV3, // ✅ 新增:精确输出买入(V3 exactOutput)
|
|
16
|
+
} from './dex.js';
|
|
16
17
|
import { BundleExecutor } from './bundle.js';
|
|
17
18
|
import { PROFIT_CONFIG, ZERO_ADDRESS } from '../utils/constants.js';
|
|
18
19
|
const multicallIface = new ethers.Interface([
|
|
@@ -478,6 +479,10 @@ export class AADexSwapExecutor {
|
|
|
478
479
|
const allowance = await this.aaManager.getErc20Allowance(tokenAddress, sellerAi.sender, effectiveRouter);
|
|
479
480
|
needApprove = allowance < sellAmountWei;
|
|
480
481
|
}
|
|
482
|
+
// ✅ 关键修复:在构建 UserOps 之前获取 feeData,整个方法使用同一个 gasPrice
|
|
483
|
+
// 这样可以避免 feeData 缓存过期导致的不一致问题
|
|
484
|
+
const fixedFeeData = await this.aaManager.getFeeData();
|
|
485
|
+
const fixedGasPrice = fixedFeeData.gasPrice ?? fixedFeeData.maxFeePerGas ?? 5000000000n;
|
|
481
486
|
const outOps = [];
|
|
482
487
|
if (needApprove) {
|
|
483
488
|
const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
@@ -487,6 +492,7 @@ export class AADexSwapExecutor {
|
|
|
487
492
|
initCode: consumeInitCode(sellerAi.sender),
|
|
488
493
|
callData: encodeExecute(tokenAddress, 0n, encodeApproveCall(effectiveRouter)),
|
|
489
494
|
deployed: sellerAi.deployed,
|
|
495
|
+
fixedGas: { gasPrice: fixedGasPrice }, // ✅ 使用统一的 gasPrice
|
|
490
496
|
});
|
|
491
497
|
const signedApprove = await this.aaManager.signUserOp(userOp, sellerOwner);
|
|
492
498
|
outOps.push(signedApprove.userOp);
|
|
@@ -557,31 +563,36 @@ export class AADexSwapExecutor {
|
|
|
557
563
|
return 0n;
|
|
558
564
|
}
|
|
559
565
|
})();
|
|
560
|
-
// ✅
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
const feeDataEarly = await this.aaManager.getFeeData();
|
|
564
|
-
const gasPriceEarly = feeDataEarly.gasPrice ?? feeDataEarly.maxFeePerGas ?? 5000000000n;
|
|
565
|
-
const estimateBuyerPrefundEarly = (deployed) => {
|
|
566
|
+
// ✅ 估算 prefund 的函数(整个方法统一使用)
|
|
567
|
+
// 注意:分发金额使用带 buffer 的 prefund,确保 buyer 有足够余额
|
|
568
|
+
const estimateBuyerPrefundWithBuffer = (deployed) => {
|
|
566
569
|
const callGas = DEX_BUY_CALL_GAS_LIMIT;
|
|
567
570
|
const verifyGas = deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
|
|
568
571
|
const preVerifyGas = PRE_VERIFICATION_GAS;
|
|
569
572
|
const totalGas = callGas + verifyGas + preVerifyGas;
|
|
570
|
-
return (totalGas *
|
|
573
|
+
return (totalGas * fixedGasPrice * PREFUND_BUFFER_PERCENT) / 100n;
|
|
574
|
+
};
|
|
575
|
+
// ✅ 估算实际 prefund(不带 buffer,用于 UserOp 构建时的计算)
|
|
576
|
+
const estimateActualPrefund = (deployed) => {
|
|
577
|
+
const callGas = DEX_BUY_CALL_GAS_LIMIT;
|
|
578
|
+
const verifyGas = deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
|
|
579
|
+
const preVerifyGas = PRE_VERIFICATION_GAS;
|
|
580
|
+
const totalGas = callGas + verifyGas + preVerifyGas;
|
|
581
|
+
return totalGas * fixedGasPrice;
|
|
571
582
|
};
|
|
572
583
|
// ✅ 计算所有 buyer 需要的 prefund 总和(仅原生代币模式 + capitalMode 需要)
|
|
573
|
-
const
|
|
574
|
-
? buyerAis.reduce((sum, ai) => sum +
|
|
584
|
+
const totalBuyerPrefundWithBuffer = (useNativeToken && capitalMode)
|
|
585
|
+
? buyerAis.reduce((sum, ai) => sum + estimateBuyerPrefundWithBuffer(ai.deployed), 0n)
|
|
575
586
|
: 0n;
|
|
576
587
|
// ✅ 计算利润(不再压缩到“卖出-买入-prefund”上限,按配置直接刮取)
|
|
577
588
|
const profitWei = extractProfit ? calculateProfitWei(quotedSellOutWei, profitBps) : 0n;
|
|
578
589
|
// 资金充足性提示:不再阻断,记录警告后继续分发
|
|
579
|
-
const totalDistributeNeeded = totalBuyWei +
|
|
590
|
+
const totalDistributeNeeded = totalBuyWei + totalBuyerPrefundWithBuffer + profitWei;
|
|
580
591
|
if (quotedSellOutWei > 0n && totalDistributeNeeded > quotedSellOutWei) {
|
|
581
592
|
console.warn('[AA DEX 批量换手] ⚠️ 预估卖出金额不足,仍继续分发:', {
|
|
582
593
|
quotedSellOutWei: ethers.formatEther(quotedSellOutWei),
|
|
583
594
|
totalBuyWei: ethers.formatEther(totalBuyWei),
|
|
584
|
-
totalBuyerPrefund: ethers.formatEther(
|
|
595
|
+
totalBuyerPrefund: ethers.formatEther(totalBuyerPrefundWithBuffer),
|
|
585
596
|
profitWei: ethers.formatEther(profitWei),
|
|
586
597
|
totalNeeded: ethers.formatEther(totalDistributeNeeded),
|
|
587
598
|
shortfall: ethers.formatEther(totalDistributeNeeded - quotedSellOutWei),
|
|
@@ -636,23 +647,12 @@ export class AADexSwapExecutor {
|
|
|
636
647
|
if (capitalMode && buyerSenders.length > 0 && totalBuyWei > 0n) {
|
|
637
648
|
if (effectiveHopCount <= 0) {
|
|
638
649
|
console.log('[AA DEX 批量换手] 进入直接分发模式 (effectiveHopCount <= 0)');
|
|
639
|
-
// ✅
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
const
|
|
643
|
-
const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n; // 5 gwei fallback
|
|
644
|
-
const estimateBuyerPrefund = (deployed) => {
|
|
645
|
-
// 买入操作的 gas(使用常量)
|
|
646
|
-
const callGas = DEX_BUY_CALL_GAS_LIMIT;
|
|
647
|
-
const verifyGas = deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
|
|
648
|
-
const preVerifyGas = PRE_VERIFICATION_GAS;
|
|
649
|
-
const totalGas = callGas + verifyGas + preVerifyGas;
|
|
650
|
-
return (totalGas * gasPrice * PREFUND_BUFFER_PERCENT) / 100n;
|
|
651
|
-
};
|
|
652
|
-
// 计算每个买方需要的 prefund
|
|
653
|
-
const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefund(ai.deployed));
|
|
650
|
+
// ✅ 关键修复:使用开始时获取的 fixedGasPrice 计算 prefund
|
|
651
|
+
// 这样确保检查、分发和 UserOp 构建使用同一个 gasPrice,避免不一致
|
|
652
|
+
// 计算每个买方需要的 prefund(带 buffer,用于余额检查和分发)
|
|
653
|
+
const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefundWithBuffer(ai.deployed));
|
|
654
654
|
const totalBuyerPrefund = buyerPrefunds.reduce((a, b) => a + b, 0n);
|
|
655
|
-
console.log(`[AA DEX 直接分发] 买方数量: ${buyerSenders.length}, 总 prefund 预估: ${ethers.formatEther(totalBuyerPrefund)} OKB`);
|
|
655
|
+
console.log(`[AA DEX 直接分发] 买方数量: ${buyerSenders.length}, 总 prefund 预估: ${ethers.formatEther(totalBuyerPrefund)} OKB, fixedGasPrice: ${fixedGasPrice.toString()}`);
|
|
656
656
|
// ✅ 关键修复:检查 buyer 钱包是否有足够的 OKB 支付 prefund
|
|
657
657
|
// ERC-4337 handleOps 执行顺序:先验证所有 UserOps(需要 prefund),再执行所有 UserOps
|
|
658
658
|
// 在 Validation 阶段,Disperse UserOp 还没执行,所以 Buyer 还没收到资金
|
|
@@ -738,14 +738,13 @@ export class AADexSwapExecutor {
|
|
|
738
738
|
usePrefundedHops: hasPrefundedHops,
|
|
739
739
|
usePaymaster: hasPaymaster,
|
|
740
740
|
});
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
// 预估 prefund 的函数(使用全局常量)
|
|
741
|
+
// ✅ 使用开始时获取的 fixedGasPrice,确保整个方法一致性
|
|
742
|
+
// 预估 prefund 的函数(使用 fixedGasPrice)
|
|
744
743
|
const estimatePrefund = (callGas, deployed) => {
|
|
745
744
|
const verifyGas = deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
|
|
746
745
|
const preVerifyGas = PRE_VERIFICATION_GAS;
|
|
747
746
|
const totalGas = callGas + verifyGas + preVerifyGas;
|
|
748
|
-
return (totalGas *
|
|
747
|
+
return (totalGas * fixedGasPrice * PREFUND_BUFFER_PERCENT) / 100n;
|
|
749
748
|
};
|
|
750
749
|
// ✅ 构建 hop 钱包列表
|
|
751
750
|
// 如果有预充值的 hop 钱包,使用它们;否则生成新钱包(需要 Paymaster)
|
|
@@ -931,6 +930,7 @@ export class AADexSwapExecutor {
|
|
|
931
930
|
deployed: currentHop.deployed,
|
|
932
931
|
fixedGas: {
|
|
933
932
|
callGasLimit: HOP_CALL_GAS_LIMIT,
|
|
933
|
+
gasPrice: fixedGasPrice, // ✅ 使用统一的 gasPrice
|
|
934
934
|
},
|
|
935
935
|
});
|
|
936
936
|
const signedHop = await this.aaManager.signUserOp(hopUserOp, currentHop.wallet);
|
|
@@ -957,6 +957,7 @@ export class AADexSwapExecutor {
|
|
|
957
957
|
deployed: lastHop.deployed,
|
|
958
958
|
fixedGas: {
|
|
959
959
|
callGasLimit: HOP_CALL_GAS_LIMIT,
|
|
960
|
+
gasPrice: fixedGasPrice, // ✅ 使用统一的 gasPrice
|
|
960
961
|
},
|
|
961
962
|
});
|
|
962
963
|
const signedLastHop = await this.aaManager.signUserOp(lastHopUserOp, lastHop.wallet);
|
|
@@ -995,40 +996,80 @@ export class AADexSwapExecutor {
|
|
|
995
996
|
});
|
|
996
997
|
outOps.push(signedProfitTransfer.userOp);
|
|
997
998
|
}
|
|
999
|
+
// ✅ 计算每个买家应该买入的代币数量(用于 V3 exactOutput 模式)
|
|
1000
|
+
// 逻辑:卖出 sellAmountWei 个代币 → 买家按比例买回
|
|
1001
|
+
// targetTokenAmounts[i] = sellAmountWei * (buyAmountsWei[i] / totalBuyWei)
|
|
1002
|
+
//
|
|
1003
|
+
// exactOutput 模式优势:
|
|
1004
|
+
// 1. 精确控制每个买家买入的代币数量
|
|
1005
|
+
// 2. amountInMaximum 设为分发金额,确保不超支
|
|
1006
|
+
// 3. 多余的 OKB 通过 refundETH 退回买家钱包
|
|
1007
|
+
const targetTokenAmounts = capitalMode && totalBuyWei > 0n
|
|
1008
|
+
? buyAmountsWei.map(buyWei => (sellAmountWei * buyWei) / totalBuyWei)
|
|
1009
|
+
: buyAmountsWei.map(() => 0n); // 非资金利用率模式不使用 exactOutput
|
|
1010
|
+
console.log('[AA DEX 批量换手] 目标代币数量计算:', {
|
|
1011
|
+
sellAmountWei: sellAmountWei.toString(),
|
|
1012
|
+
totalBuyWei: ethers.formatEther(totalBuyWei),
|
|
1013
|
+
targetTokenAmounts: targetTokenAmounts.map(a => a.toString()),
|
|
1014
|
+
useExactOutput: capitalMode && isV3Trade && useNativeToken,
|
|
1015
|
+
});
|
|
998
1016
|
// Batch Buy ops(分发后再执行)- ✅ 支持 V2/V3,支持 ERC20 稳定币
|
|
999
1017
|
for (let i = 0; i < buyerOwners.length; i++) {
|
|
1000
1018
|
const ai = buyerAis[i];
|
|
1001
|
-
const buyWei = buyAmountsWei[i];
|
|
1019
|
+
const buyWei = buyAmountsWei[i]; // 分发给买家的 OKB 金额(作为 amountInMaximum)
|
|
1020
|
+
const targetTokenOut = targetTokenAmounts[i]; // 目标买入的代币数量
|
|
1002
1021
|
let buyCallData;
|
|
1003
1022
|
if (useNativeToken) {
|
|
1004
1023
|
// ✅ 原生代币模式:OKB → Token
|
|
1005
1024
|
let buySwapData;
|
|
1006
1025
|
if (isV3Trade) {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1026
|
+
// ✅ V3 资金利用率模式:使用 exactOutput 精确买入目标代币数量
|
|
1027
|
+
// - amountOut: 精确的代币数量(sellAmountWei 按比例分配)
|
|
1028
|
+
// - amountInMaximum: 最多花费的 OKB(分发给该买家的金额)
|
|
1029
|
+
// - refundETH: 多余的 OKB 自动退回
|
|
1030
|
+
if (capitalMode && targetTokenOut > 0n) {
|
|
1031
|
+
buySwapData = encodeSwapExactOutputForTokensV3({
|
|
1032
|
+
tokenIn: WOKB,
|
|
1033
|
+
tokenOut: tokenAddress,
|
|
1034
|
+
fee: lpFeeProfileToV3Fee(lpFeeProfile),
|
|
1035
|
+
recipient: ai.sender,
|
|
1036
|
+
deadline,
|
|
1037
|
+
amountOut: targetTokenOut, // ✅ 精确买入的代币数量
|
|
1038
|
+
amountInMaximum: buyWei, // ✅ 最多花费的 OKB(分发金额)
|
|
1039
|
+
sqrtPriceLimitX96: 0n,
|
|
1040
|
+
});
|
|
1041
|
+
console.log(`[AA DEX V3 exactOutput] Buyer ${i}: 目标代币 ${targetTokenOut.toString()}, 最大花费 ${ethers.formatEther(buyWei)} OKB`);
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
// 非资金利用率模式:使用 exactInput(固定 OKB 买尽可能多的代币)
|
|
1045
|
+
buySwapData = encodeSwapExactETHForTokensV3({
|
|
1046
|
+
tokenIn: WOKB,
|
|
1047
|
+
tokenOut: tokenAddress,
|
|
1048
|
+
fee: lpFeeProfileToV3Fee(lpFeeProfile),
|
|
1049
|
+
recipient: ai.sender,
|
|
1050
|
+
deadline,
|
|
1051
|
+
amountIn: buyWei,
|
|
1052
|
+
amountOutMinimum: 0n,
|
|
1053
|
+
sqrtPriceLimitX96: 0n,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1017
1056
|
}
|
|
1018
1057
|
else {
|
|
1058
|
+
// V2 模式:不支持 exactOutput,使用 exactInput
|
|
1019
1059
|
buySwapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], ai.sender, deadline);
|
|
1020
1060
|
}
|
|
1021
1061
|
buyCallData = encodeExecute(effectiveRouter, buyWei, buySwapData);
|
|
1022
1062
|
}
|
|
1023
1063
|
else {
|
|
1024
1064
|
// ✅ ERC20 模式:quoteToken → Token(需要先 approve)
|
|
1065
|
+
// 注意:ERC20 模式暂不支持 exactOutput,保持 exactInput
|
|
1025
1066
|
const approveData = encodeApproveCall(effectiveRouter, buyWei);
|
|
1026
1067
|
const swapData = encodeSwapExactTokensForTokensSupportingFee(buyWei, 0n, [quoteToken, tokenAddress], ai.sender, deadline);
|
|
1027
1068
|
buyCallData = encodeExecuteBatch([quoteToken, effectiveRouter], [0n, 0n], [approveData, swapData]);
|
|
1028
1069
|
}
|
|
1029
|
-
// ✅ 关键修复:使用 buildUserOpWithFixedGas,确保 callGasLimit 与预检查时一致
|
|
1030
|
-
// 预检查使用 DEX_BUY_CALL_GAS_LIMIT (650,000)
|
|
1031
|
-
//
|
|
1070
|
+
// ✅ 关键修复:使用 buildUserOpWithFixedGas,确保 callGasLimit 和 gasPrice 与预检查时一致
|
|
1071
|
+
// 预检查使用 DEX_BUY_CALL_GAS_LIMIT (650,000) 和 fixedGasPrice
|
|
1072
|
+
// 这里必须使用相同的值,避免 gasPrice 不一致导致 prefund 不足
|
|
1032
1073
|
const { userOp: buyUserOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
1033
1074
|
ownerWallet: buyerOwners[i],
|
|
1034
1075
|
sender: ai.sender,
|
|
@@ -1038,6 +1079,7 @@ export class AADexSwapExecutor {
|
|
|
1038
1079
|
deployed: ai.deployed,
|
|
1039
1080
|
fixedGas: {
|
|
1040
1081
|
callGasLimit: DEX_BUY_CALL_GAS_LIMIT,
|
|
1082
|
+
gasPrice: fixedGasPrice, // ✅ 使用统一的 gasPrice
|
|
1041
1083
|
},
|
|
1042
1084
|
});
|
|
1043
1085
|
const signedBuy = await this.aaManager.signUserOp(buyUserOp, buyerOwners[i]);
|
package/dist/xlayer/dex.d.ts
CHANGED
|
@@ -20,6 +20,20 @@ interface V3ExactInputSingleParams {
|
|
|
20
20
|
amountOutMinimum: bigint;
|
|
21
21
|
sqrtPriceLimitX96: bigint;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* ✅ V3 exactOutputSingle 参数结构
|
|
25
|
+
* 精确输出模式:固定输出代币数量,输入金额可变(不超过 amountInMaximum)
|
|
26
|
+
*/
|
|
27
|
+
export interface V3ExactOutputSingleParams {
|
|
28
|
+
tokenIn: string;
|
|
29
|
+
tokenOut: string;
|
|
30
|
+
fee: number;
|
|
31
|
+
recipient: string;
|
|
32
|
+
deadline: number;
|
|
33
|
+
amountOut: bigint;
|
|
34
|
+
amountInMaximum: bigint;
|
|
35
|
+
sqrtPriceLimitX96: bigint;
|
|
36
|
+
}
|
|
23
37
|
/**
|
|
24
38
|
* 编码 unwrapWETH9 调用(将 WOKB 转换为 OKB)
|
|
25
39
|
*/
|
|
@@ -33,7 +47,7 @@ export declare function encodeRefundETH(): string;
|
|
|
33
47
|
*/
|
|
34
48
|
export declare function encodeV3Multicall(data: string[]): string;
|
|
35
49
|
/**
|
|
36
|
-
* ✅ 编码 V3 买入调用(OKB → Token
|
|
50
|
+
* ✅ 编码 V3 买入调用(OKB → Token)- exactInput 模式
|
|
37
51
|
*
|
|
38
52
|
* 对于 AA 模式:直接返回 exactInputSingle 编码(不需要 multicall 包装)
|
|
39
53
|
* - AA 的 execute 会正确传递 msg.value
|
|
@@ -42,6 +56,28 @@ export declare function encodeV3Multicall(data: string[]): string;
|
|
|
42
56
|
* 对于 EOA 模式:调用者需要自己用 multicall 包装
|
|
43
57
|
*/
|
|
44
58
|
export declare function encodeSwapExactETHForTokensV3(params: V3ExactInputSingleParams): string;
|
|
59
|
+
/**
|
|
60
|
+
* ✅ 编码 V3 精确输出买入调用(OKB → Token)- exactOutput 模式
|
|
61
|
+
*
|
|
62
|
+
* 使用 multicall 包装 exactOutputSingle + refundETH:
|
|
63
|
+
* 1. exactOutputSingle: 用最多 amountInMaximum OKB 买入精确 amountOut 个代币
|
|
64
|
+
* 2. refundETH: 将未使用的 OKB 退回给调用者
|
|
65
|
+
*
|
|
66
|
+
* 优势:
|
|
67
|
+
* - 精确控制买入的代币数量
|
|
68
|
+
* - 多余的 OKB 自动退回,不会浪费
|
|
69
|
+
*
|
|
70
|
+
* 适用场景:
|
|
71
|
+
* - 资金利用率模式:卖出后需要精确买回固定数量的代币
|
|
72
|
+
* - 确保每个买家获得精确的代币数量
|
|
73
|
+
*
|
|
74
|
+
* @param params.amountOut - 要买入的精确代币数量
|
|
75
|
+
* @param params.amountInMaximum - 最多花费的 OKB 数量(通常设为分发金额)
|
|
76
|
+
* @param params.refundRecipient - 退款接收者(默认为 recipient)
|
|
77
|
+
*/
|
|
78
|
+
export declare function encodeSwapExactOutputForTokensV3(params: V3ExactOutputSingleParams & {
|
|
79
|
+
refundRecipient?: string;
|
|
80
|
+
}): string;
|
|
45
81
|
/**
|
|
46
82
|
* ✅ 编码 V3 卖出调用(Token → OKB)
|
|
47
83
|
* 使用 multicall 包装 exactInputSingle + unwrapWETH9
|
package/dist/xlayer/dex.js
CHANGED
|
@@ -23,6 +23,24 @@ const v3RouterIface = new Interface(POTATOSWAP_V3_ROUTER_ABI);
|
|
|
23
23
|
function encodeExactInputSingle(params) {
|
|
24
24
|
return v3RouterIface.encodeFunctionData('exactInputSingle', [params]);
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* ✅ 编码 exactOutputSingle 调用(精确输出模式)
|
|
28
|
+
*
|
|
29
|
+
* 使用场景:
|
|
30
|
+
* - 需要精确控制买入代币数量时
|
|
31
|
+
* - 资金利用率模式下,卖出后按比例买回精确数量的代币
|
|
32
|
+
*
|
|
33
|
+
* 优势:
|
|
34
|
+
* - 精确控制每个地址买到的代币数量
|
|
35
|
+
* - 避免因滑点导致买入数量不一致
|
|
36
|
+
*
|
|
37
|
+
* 注意:
|
|
38
|
+
* - 如果需要的 OKB > amountInMaximum,交易会失败
|
|
39
|
+
* - 多余的 OKB 需要通过 refundETH 退回
|
|
40
|
+
*/
|
|
41
|
+
function encodeExactOutputSingle(params) {
|
|
42
|
+
return v3RouterIface.encodeFunctionData('exactOutputSingle', [params]);
|
|
43
|
+
}
|
|
26
44
|
/**
|
|
27
45
|
* 编码 unwrapWETH9 调用(将 WOKB 转换为 OKB)
|
|
28
46
|
*/
|
|
@@ -42,7 +60,7 @@ export function encodeV3Multicall(data) {
|
|
|
42
60
|
return v3RouterIface.encodeFunctionData('multicall', [data]);
|
|
43
61
|
}
|
|
44
62
|
/**
|
|
45
|
-
* ✅ 编码 V3 买入调用(OKB → Token
|
|
63
|
+
* ✅ 编码 V3 买入调用(OKB → Token)- exactInput 模式
|
|
46
64
|
*
|
|
47
65
|
* 对于 AA 模式:直接返回 exactInputSingle 编码(不需要 multicall 包装)
|
|
48
66
|
* - AA 的 execute 会正确传递 msg.value
|
|
@@ -55,6 +73,34 @@ export function encodeSwapExactETHForTokensV3(params) {
|
|
|
55
73
|
// V3 Router 会自动处理原生币 → WOKB 的转换
|
|
56
74
|
return encodeExactInputSingle(params);
|
|
57
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* ✅ 编码 V3 精确输出买入调用(OKB → Token)- exactOutput 模式
|
|
78
|
+
*
|
|
79
|
+
* 使用 multicall 包装 exactOutputSingle + refundETH:
|
|
80
|
+
* 1. exactOutputSingle: 用最多 amountInMaximum OKB 买入精确 amountOut 个代币
|
|
81
|
+
* 2. refundETH: 将未使用的 OKB 退回给调用者
|
|
82
|
+
*
|
|
83
|
+
* 优势:
|
|
84
|
+
* - 精确控制买入的代币数量
|
|
85
|
+
* - 多余的 OKB 自动退回,不会浪费
|
|
86
|
+
*
|
|
87
|
+
* 适用场景:
|
|
88
|
+
* - 资金利用率模式:卖出后需要精确买回固定数量的代币
|
|
89
|
+
* - 确保每个买家获得精确的代币数量
|
|
90
|
+
*
|
|
91
|
+
* @param params.amountOut - 要买入的精确代币数量
|
|
92
|
+
* @param params.amountInMaximum - 最多花费的 OKB 数量(通常设为分发金额)
|
|
93
|
+
* @param params.refundRecipient - 退款接收者(默认为 recipient)
|
|
94
|
+
*/
|
|
95
|
+
export function encodeSwapExactOutputForTokensV3(params) {
|
|
96
|
+
// 1. exactOutputSingle: 买入精确数量的代币
|
|
97
|
+
const swapData = encodeExactOutputSingle(params);
|
|
98
|
+
// 2. refundETH: 退还未使用的 OKB
|
|
99
|
+
// 注意:refundETH 会将多余的原生币退回给 msg.sender(AA 钱包)
|
|
100
|
+
const refundData = encodeRefundETH();
|
|
101
|
+
// 使用 multicall 打包两个调用
|
|
102
|
+
return encodeV3Multicall([swapData, refundData]);
|
|
103
|
+
}
|
|
58
104
|
/**
|
|
59
105
|
* ✅ 编码 V3 卖出调用(Token → OKB)
|
|
60
106
|
* 使用 multicall 包装 exactInputSingle + unwrapWETH9
|
|
@@ -7,9 +7,9 @@ import { FLAP_PORTAL, ZERO_ADDRESS, MULTICALL3, VERIFICATION_GAS_LIMIT_DEPLOY, V
|
|
|
7
7
|
// 多跳 prefund 估算用的 gas 常量(可配置)
|
|
8
8
|
const HOP_CALL_GAS_LIMIT = 150000n; // hop 转账操作
|
|
9
9
|
const PORTAL_BUY_CALL_GAS_LIMIT = 450000n; // Portal 买入操作
|
|
10
|
-
// ✅ 增加余额检查 buffer 从
|
|
11
|
-
// 预充值使用 200% buffer,这里使用
|
|
12
|
-
const PREFUND_BUFFER_PERCENT =
|
|
10
|
+
// ✅ 增加余额检查 buffer 从 180% 到 250%,确保 gasPrice 波动时也有足够余额
|
|
11
|
+
// 预充值使用 200% buffer,这里使用 250% 作为安全阈值(覆盖更大的 gasPrice 波动)
|
|
12
|
+
const PREFUND_BUFFER_PERCENT = 250n; // prefund buffer 百分比 (250 = 2.5x)
|
|
13
13
|
import { AAAccountManager, encodeExecute, encodeExecuteBatch, encodeExecuteViaMulticall3 } from './aa-account.js';
|
|
14
14
|
import { encodeBuyCall, encodeSellCall, encodeBuyCallWithQuote, encodeSellCallWithQuote, encodeApproveCall, PortalQuery, parseOkb, } from './portal-ops.js';
|
|
15
15
|
import { BundleExecutor } from './bundle.js';
|
package/dist/xlayer/types.d.ts
CHANGED
package/dist/xlayer/wash-ops.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { ethers } from 'ethers';
|
|
16
16
|
import { createAAAccountManager, encodeExecute, createWallet } from './aa-account.js';
|
|
17
|
-
import { encodeBuyCall, encodeSellCall, PortalQuery } from './portal-ops.js';
|
|
17
|
+
import { encodeBuyCall, encodeSellCall, PortalQuery, lpFeeProfileToV3Fee } from './portal-ops.js';
|
|
18
18
|
import { encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, DexQuery, } from './dex.js';
|
|
19
19
|
import { FLAP_PORTAL, WOKB, XLAYER_CHAIN_ID, DEFAULT_RPC_URL, ENTRYPOINT_V06, SIMPLE_ACCOUNT_FACTORY, PARTICLE_BUNDLER_URL, } from './constants.js';
|
|
20
20
|
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
@@ -77,7 +77,6 @@ function normalizeConfig(cfg) {
|
|
|
77
77
|
export async function buildWashOps(params) {
|
|
78
78
|
const poolType = params.poolType || 'flap';
|
|
79
79
|
const wokb = params.wrappedOkbAddress || WOKB;
|
|
80
|
-
const v3Fee = params.v3Fee || 2500;
|
|
81
80
|
const deadline = Math.floor(Date.now() / 1000) + 60 * Math.max(1, Number(params.deadlineMinutes ?? 20));
|
|
82
81
|
const portal = FLAP_PORTAL;
|
|
83
82
|
const routerAddress = params.routerAddress || '';
|
|
@@ -88,15 +87,29 @@ export async function buildWashOps(params) {
|
|
|
88
87
|
if (poolType === 'v3' && !routerAddress) {
|
|
89
88
|
throw new Error('[buildWashOps] poolType=v3 时必须提供 routerAddress');
|
|
90
89
|
}
|
|
91
|
-
if (poolType === 'v3' && !params.v3Fee) {
|
|
92
|
-
throw new Error('[buildWashOps] poolType=v3 时必须提供 v3Fee');
|
|
93
|
-
}
|
|
94
90
|
if (params.ownerPrivateKeys.length !== params.buyAmountsOkb.length) {
|
|
95
91
|
throw new Error('[buildWashOps] 私钥数量与买入金额数量不一致');
|
|
96
92
|
}
|
|
97
93
|
// ✅ 归一化配置,填充默认值(与前端 normalizeXLayerAAConfig 保持一致)
|
|
98
94
|
const config = normalizeConfig(params.config);
|
|
99
95
|
const aaManager = createAAAccountManager(config);
|
|
96
|
+
// ✅ V3 模式:直接从链上读取 lpFeeProfile 获取正确的 fee(与 bundleGraduateBuy 保持一致)
|
|
97
|
+
// 不再依赖前端传入的 v3Fee 参数,避免前端传入错误值导致交易失败
|
|
98
|
+
let v3Fee = params.v3Fee || 2500;
|
|
99
|
+
if (poolType === 'v3') {
|
|
100
|
+
try {
|
|
101
|
+
const portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
|
|
102
|
+
const tokenState = await portalQuery.getTokenV7(params.tokenAddress);
|
|
103
|
+
const correctV3Fee = lpFeeProfileToV3Fee(tokenState.lpFeeProfile);
|
|
104
|
+
if (correctV3Fee !== v3Fee) {
|
|
105
|
+
console.log(`[buildWashOps] V3 Fee 校正: 前端传入 ${v3Fee}, 链上 lpFeeProfile=${tokenState.lpFeeProfile} → 正确 fee=${correctV3Fee}`);
|
|
106
|
+
}
|
|
107
|
+
v3Fee = correctV3Fee;
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
console.warn(`[buildWashOps] 读取 lpFeeProfile 失败,使用前端传入的 v3Fee=${v3Fee}:`, e);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
100
113
|
// ✅ 预先过滤无效私钥并保持索引对应
|
|
101
114
|
const validEntries = [];
|
|
102
115
|
for (let i = 0; i < params.ownerPrivateKeys.length; i++) {
|
package/package.json
CHANGED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ECDH + AES-GCM 加密工具(浏览器兼容)
|
|
3
|
-
* 用于将签名交易用服务器公钥加密
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* 用服务器公钥加密签名交易(ECDH + AES-GCM)
|
|
7
|
-
*
|
|
8
|
-
* @param signedTransactions 签名后的交易数组
|
|
9
|
-
* @param publicKeyBase64 服务器提供的公钥(Base64 格式)
|
|
10
|
-
* @returns JSON 字符串 {e: 临时公钥, i: IV, d: 密文}
|
|
11
|
-
*/
|
|
12
|
-
export declare function encryptWithPublicKey(signedTransactions: string[], publicKeyBase64: string): Promise<string>;
|
|
13
|
-
/**
|
|
14
|
-
* 验证公钥格式(Base64)
|
|
15
|
-
*/
|
|
16
|
-
export declare function validatePublicKey(publicKeyBase64: string): boolean;
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ECDH + AES-GCM 加密工具(浏览器兼容)
|
|
3
|
-
* 用于将签名交易用服务器公钥加密
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* 获取全局 crypto 对象(最简单直接的方式)
|
|
7
|
-
*/
|
|
8
|
-
function getCryptoAPI() {
|
|
9
|
-
// 尝试所有可能的全局对象,优先浏览器环境
|
|
10
|
-
const cryptoObj = (typeof window !== 'undefined' && window.crypto) ||
|
|
11
|
-
(typeof self !== 'undefined' && self.crypto) ||
|
|
12
|
-
(typeof global !== 'undefined' && global.crypto) ||
|
|
13
|
-
(typeof globalThis !== 'undefined' && globalThis.crypto);
|
|
14
|
-
if (!cryptoObj) {
|
|
15
|
-
const env = typeof window !== 'undefined' ? 'Browser' : 'Node.js';
|
|
16
|
-
const protocol = typeof location !== 'undefined' ? location.protocol : 'unknown';
|
|
17
|
-
throw new Error(`❌ Crypto API 不可用。环境: ${env}, 协议: ${protocol}. ` +
|
|
18
|
-
'请确保在 HTTPS 或 localhost 下运行');
|
|
19
|
-
}
|
|
20
|
-
return cryptoObj;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* 获取 SubtleCrypto(用于加密操作)
|
|
24
|
-
*/
|
|
25
|
-
function getSubtleCrypto() {
|
|
26
|
-
const crypto = getCryptoAPI();
|
|
27
|
-
if (!crypto.subtle) {
|
|
28
|
-
const protocol = typeof location !== 'undefined' ? location.protocol : 'unknown';
|
|
29
|
-
const hostname = typeof location !== 'undefined' ? location.hostname : 'unknown';
|
|
30
|
-
throw new Error(`❌ SubtleCrypto API 不可用。协议: ${protocol}, 主机: ${hostname}. ` +
|
|
31
|
-
'请确保:1) 使用 HTTPS (或 localhost);2) 浏览器支持 Web Crypto API;' +
|
|
32
|
-
'3) 不在无痕/隐私浏览模式下');
|
|
33
|
-
}
|
|
34
|
-
return crypto.subtle;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Base64 转 ArrayBuffer(优先使用浏览器 API)
|
|
38
|
-
*/
|
|
39
|
-
function base64ToArrayBuffer(base64) {
|
|
40
|
-
// 浏览器环境(优先)
|
|
41
|
-
if (typeof atob !== 'undefined') {
|
|
42
|
-
const binaryString = atob(base64);
|
|
43
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
44
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
45
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
46
|
-
}
|
|
47
|
-
return bytes.buffer;
|
|
48
|
-
}
|
|
49
|
-
// Node.js 环境(fallback)
|
|
50
|
-
if (typeof Buffer !== 'undefined') {
|
|
51
|
-
return Buffer.from(base64, 'base64').buffer;
|
|
52
|
-
}
|
|
53
|
-
throw new Error('❌ Base64 解码不可用');
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* ArrayBuffer 转 Base64(优先使用浏览器 API)
|
|
57
|
-
*/
|
|
58
|
-
function arrayBufferToBase64(buffer) {
|
|
59
|
-
// 浏览器环境(优先)
|
|
60
|
-
if (typeof btoa !== 'undefined') {
|
|
61
|
-
const bytes = new Uint8Array(buffer);
|
|
62
|
-
let binary = '';
|
|
63
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
64
|
-
binary += String.fromCharCode(bytes[i]);
|
|
65
|
-
}
|
|
66
|
-
return btoa(binary);
|
|
67
|
-
}
|
|
68
|
-
// Node.js 环境(fallback)
|
|
69
|
-
if (typeof Buffer !== 'undefined') {
|
|
70
|
-
return Buffer.from(buffer).toString('base64');
|
|
71
|
-
}
|
|
72
|
-
throw new Error('❌ Base64 编码不可用');
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* 生成随机 Hex 字符串
|
|
76
|
-
*/
|
|
77
|
-
function randomHex(length) {
|
|
78
|
-
const crypto = getCryptoAPI();
|
|
79
|
-
const array = new Uint8Array(length);
|
|
80
|
-
crypto.getRandomValues(array);
|
|
81
|
-
return Array.from(array)
|
|
82
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
83
|
-
.join('');
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* 用服务器公钥加密签名交易(ECDH + AES-GCM)
|
|
87
|
-
*
|
|
88
|
-
* @param signedTransactions 签名后的交易数组
|
|
89
|
-
* @param publicKeyBase64 服务器提供的公钥(Base64 格式)
|
|
90
|
-
* @returns JSON 字符串 {e: 临时公钥, i: IV, d: 密文}
|
|
91
|
-
*/
|
|
92
|
-
export async function encryptWithPublicKey(signedTransactions, publicKeyBase64) {
|
|
93
|
-
try {
|
|
94
|
-
// 0. 获取 SubtleCrypto 和 Crypto API
|
|
95
|
-
const subtle = getSubtleCrypto();
|
|
96
|
-
const crypto = getCryptoAPI();
|
|
97
|
-
// 1. 准备数据
|
|
98
|
-
const payload = {
|
|
99
|
-
signedTransactions,
|
|
100
|
-
timestamp: Date.now(),
|
|
101
|
-
nonce: randomHex(8)
|
|
102
|
-
};
|
|
103
|
-
const plaintext = JSON.stringify(payload);
|
|
104
|
-
// 2. 生成临时 ECDH 密钥对
|
|
105
|
-
const ephemeralKeyPair = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey']);
|
|
106
|
-
// 3. 导入服务器公钥
|
|
107
|
-
const publicKeyBuffer = base64ToArrayBuffer(publicKeyBase64);
|
|
108
|
-
const publicKey = await subtle.importKey('raw', publicKeyBuffer, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
|
|
109
|
-
// 4. 派生共享密钥(AES-256)
|
|
110
|
-
const sharedKey = await subtle.deriveKey({ name: 'ECDH', public: publicKey }, ephemeralKeyPair.privateKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
|
|
111
|
-
// 5. AES-GCM 加密
|
|
112
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
113
|
-
const encrypted = await subtle.encrypt({ name: 'AES-GCM', iv }, sharedKey, new TextEncoder().encode(plaintext));
|
|
114
|
-
// 6. 导出临时公钥
|
|
115
|
-
const ephemeralPublicKeyRaw = await subtle.exportKey('raw', ephemeralKeyPair.publicKey);
|
|
116
|
-
// 7. 返回加密包(JSON 格式)
|
|
117
|
-
return JSON.stringify({
|
|
118
|
-
e: arrayBufferToBase64(ephemeralPublicKeyRaw), // 临时公钥
|
|
119
|
-
i: arrayBufferToBase64(iv.buffer), // IV
|
|
120
|
-
d: arrayBufferToBase64(encrypted) // 密文
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
throw new Error(`加密失败: ${error?.message || String(error)}`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* 验证公钥格式(Base64)
|
|
129
|
-
*/
|
|
130
|
-
export function validatePublicKey(publicKeyBase64) {
|
|
131
|
-
try {
|
|
132
|
-
if (!publicKeyBase64)
|
|
133
|
-
return false;
|
|
134
|
-
// Base64 字符集验证
|
|
135
|
-
if (!/^[A-Za-z0-9+/=]+$/.test(publicKeyBase64))
|
|
136
|
-
return false;
|
|
137
|
-
// ECDH P-256 公钥固定长度 65 字节(未压缩)
|
|
138
|
-
// Base64 编码后约 88 字符
|
|
139
|
-
if (publicKeyBase64.length < 80 || publicKeyBase64.length > 100)
|
|
140
|
-
return false;
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
}
|