four-flap-meme-sdk 1.3.56 → 1.3.57
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/dex/direct-router.js +24 -1
- package/dist/flap/portal-bundle-merkle/swap-buy-first.js +0 -6
- package/dist/flap/portal-bundle-merkle/swap.js +7 -17
- package/dist/pancake/bundle-buy-first.d.ts +0 -2
- package/dist/pancake/bundle-buy-first.js +21 -97
- package/dist/pancake/bundle-swap.d.ts +0 -3
- package/dist/pancake/bundle-swap.js +23 -87
- 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
|
@@ -16,6 +16,27 @@ import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
|
16
16
|
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
17
17
|
const DEFAULT_GAS_LIMIT = 300000;
|
|
18
18
|
const DEADLINE_MINUTES = 20;
|
|
19
|
+
/**
|
|
20
|
+
* 截断小数位数,避免超过代币精度导致 parseUnits 报错
|
|
21
|
+
* 例如:truncateDecimals("21906.025000000000000000", 1) => "21906.0"
|
|
22
|
+
*/
|
|
23
|
+
function truncateDecimals(value, decimals) {
|
|
24
|
+
if (!value || decimals < 0)
|
|
25
|
+
return value;
|
|
26
|
+
const parts = value.split('.');
|
|
27
|
+
if (parts.length === 1)
|
|
28
|
+
return value; // 没有小数点
|
|
29
|
+
const integerPart = parts[0];
|
|
30
|
+
const decimalPart = parts[1];
|
|
31
|
+
if (decimals === 0)
|
|
32
|
+
return integerPart;
|
|
33
|
+
// 截断到指定小数位数
|
|
34
|
+
const truncatedDecimal = decimalPart.slice(0, decimals);
|
|
35
|
+
// 如果截断后没有小数部分,只返回整数部分
|
|
36
|
+
if (!truncatedDecimal || truncatedDecimal === '')
|
|
37
|
+
return integerPart;
|
|
38
|
+
return `${integerPart}.${truncatedDecimal}`;
|
|
39
|
+
}
|
|
19
40
|
/** Router 地址配置 */
|
|
20
41
|
export const DIRECT_ROUTERS = {
|
|
21
42
|
BSC: {
|
|
@@ -448,7 +469,9 @@ export async function directV2BatchSell(params) {
|
|
|
448
469
|
const sellAmountsWei = [];
|
|
449
470
|
for (let i = 0; i < wallets.length; i++) {
|
|
450
471
|
if (sellAmounts && sellAmounts[i]) {
|
|
451
|
-
|
|
472
|
+
// ✅ 截断小数位数,避免超过代币精度导致 parseUnits 报错
|
|
473
|
+
const truncatedAmount = truncateDecimals(sellAmounts[i], tokenDecimals);
|
|
474
|
+
sellAmountsWei.push(ethers.parseUnits(truncatedAmount, tokenDecimals));
|
|
452
475
|
}
|
|
453
476
|
else if (sellPercentages && sellPercentages[i]) {
|
|
454
477
|
const pct = Math.min(100, Math.max(0, sellPercentages[i]));
|
|
@@ -300,15 +300,9 @@ async function calculateBuyerFunds({ buyer, buyerFunds, buyerFundsPercentage, re
|
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
else {
|
|
303
|
-
// ERC20 购买:检查代币余额
|
|
304
303
|
if (buyerBalance < buyerFundsWei) {
|
|
305
304
|
throw new Error(`买方代币余额不足: 需要 ${ethers.formatUnits(buyerFundsWei, quoteTokenDecimals)},实际 ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`);
|
|
306
305
|
}
|
|
307
|
-
// ✅ ERC20 购买时,还需要检查买方是否有足够 BNB 支付 Gas
|
|
308
|
-
const buyerBnbBalance = await buyer.provider.getBalance(buyer.address);
|
|
309
|
-
if (buyerBnbBalance < reserveGas) {
|
|
310
|
-
throw new Error(`买方 BNB 余额不足 (用于支付 Gas): 需要 ${ethers.formatEther(reserveGas)} ${nativeToken},实际 ${ethers.formatEther(buyerBnbBalance)} ${nativeToken}`);
|
|
311
|
-
}
|
|
312
306
|
}
|
|
313
307
|
return { buyerFundsWei, buyerBalance };
|
|
314
308
|
}
|
|
@@ -194,9 +194,7 @@ export async function flapBundleSwapMerkle(params) {
|
|
|
194
194
|
portalGasCost: finalGasLimit * gasPrice,
|
|
195
195
|
provider: chainContext.provider,
|
|
196
196
|
chainContext,
|
|
197
|
-
seller
|
|
198
|
-
useNativeToken, // ✅ 传递是否使用原生代币
|
|
199
|
-
quoteTokenDecimals // ✅ 传递代币精度
|
|
197
|
+
seller
|
|
200
198
|
})
|
|
201
199
|
]);
|
|
202
200
|
// 构建交易请求
|
|
@@ -366,21 +364,13 @@ async function calculateBuyerNeed({ buyer, quotedNative, reserveGasEth, slippage
|
|
|
366
364
|
: (useNativeToken ? buyerBalance - reserveGas : buyerBalance);
|
|
367
365
|
return { reserveGas, buyerBalance, buyerNeedTotal, maxBuyerValue };
|
|
368
366
|
}
|
|
369
|
-
async function validateBalances({ buyerNeed, buyerAddress, portalGasCost, provider, chainContext, seller
|
|
367
|
+
async function validateBalances({ buyerNeed, buyerAddress, portalGasCost, provider, chainContext, seller }) {
|
|
368
|
+
const buyerBalance = buyerNeed.buyerBalance;
|
|
370
369
|
const sellerBalance = await provider.getBalance(seller.address);
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (!useNativeToken) {
|
|
376
|
-
// ERC20 购买时,买方仍需要 BNB 支付 Gas
|
|
377
|
-
const buyerBnbBalance = await provider.getBalance(buyerAddress);
|
|
378
|
-
const buyerGasCost = portalGasCost; // 买方交易的 Gas 费用
|
|
379
|
-
if (buyerBnbBalance < buyerGasCost) {
|
|
380
|
-
throw new Error(`买方 BNB 余额不足 (用于支付 Gas):\n` +
|
|
381
|
-
` - 需要: ${ethers.formatEther(buyerGasCost)} ${chainContext.nativeToken}\n` +
|
|
382
|
-
` - 实际: ${ethers.formatEther(buyerBnbBalance)} ${chainContext.nativeToken}`);
|
|
383
|
-
}
|
|
370
|
+
if (buyerBalance < buyerNeed.buyerNeedTotal) {
|
|
371
|
+
throw new Error(`买方余额不足:\n` +
|
|
372
|
+
` - 需要: ${ethers.formatEther(buyerNeed.buyerNeedTotal)} ${chainContext.nativeToken}\n` +
|
|
373
|
+
` - 实际: ${ethers.formatEther(buyerBalance)} ${chainContext.nativeToken}`);
|
|
384
374
|
}
|
|
385
375
|
if (sellerBalance < portalGasCost) {
|
|
386
376
|
throw new Error(`卖方余额不足: 需要 ${ethers.formatEther(portalGasCost)} ${chainContext.nativeToken} (Gas),实际 ${ethers.formatEther(sellerBalance)} ${chainContext.nativeToken}`);
|
|
@@ -53,8 +53,6 @@ export interface PancakeBundleBuyFirstSignParams {
|
|
|
53
53
|
buyerFunds?: string;
|
|
54
54
|
buyerFundsPercentage?: number;
|
|
55
55
|
config: PancakeBuyFirstSignConfig;
|
|
56
|
-
quoteToken?: string;
|
|
57
|
-
quoteTokenDecimals?: number;
|
|
58
56
|
}
|
|
59
57
|
export interface PancakeBundleBuyFirstParams {
|
|
60
58
|
buyerPrivateKey: string;
|
|
@@ -49,8 +49,6 @@ const PANCAKE_V2_ROUTER_ADDRESS = '0x10ED43C718714eb63d5aA57B78B54704E256024E';
|
|
|
49
49
|
const PANCAKE_V3_QUOTER_ADDRESS = '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997';
|
|
50
50
|
const FLAT_FEE = ethers.parseEther('0.0001');
|
|
51
51
|
const WBNB_ADDRESS = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
|
|
52
|
-
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
53
|
-
const ERC20_BALANCE_OF_ABI = ['function balanceOf(address) view returns (uint256)'];
|
|
54
52
|
const ERC20_ALLOWANCE_ABI = [
|
|
55
53
|
'function allowance(address,address) view returns (uint256)',
|
|
56
54
|
'function approve(address,uint256) returns (bool)',
|
|
@@ -58,11 +56,7 @@ const ERC20_ALLOWANCE_ABI = [
|
|
|
58
56
|
];
|
|
59
57
|
const APPROVE_INTERFACE = new ethers.Interface(['function approve(address,uint256) returns (bool)']);
|
|
60
58
|
export async function pancakeBundleBuyFirstMerkle(params) {
|
|
61
|
-
const { buyerPrivateKey, sellerPrivateKey, tokenAddress, routeParams, buyerFunds, buyerFundsPercentage, config
|
|
62
|
-
// ✅ 判断是否使用原生代币(BNB)或 ERC20 代币(如 USDT)
|
|
63
|
-
const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
|
|
64
|
-
console.log('🔍 PancakeSwap BuyFirst - quoteToken:', quoteToken);
|
|
65
|
-
console.log('🔍 PancakeSwap BuyFirst - useNativeToken:', useNativeToken);
|
|
59
|
+
const { buyerPrivateKey, sellerPrivateKey, tokenAddress, routeParams, buyerFunds, buyerFundsPercentage, config } = params;
|
|
66
60
|
const context = createPancakeContext(config);
|
|
67
61
|
const buyer = new Wallet(buyerPrivateKey, context.provider);
|
|
68
62
|
const seller = new Wallet(sellerPrivateKey, context.provider);
|
|
@@ -71,11 +65,7 @@ export async function pancakeBundleBuyFirstMerkle(params) {
|
|
|
71
65
|
buyer,
|
|
72
66
|
buyerFunds,
|
|
73
67
|
buyerFundsPercentage,
|
|
74
|
-
reserveGas: config.reserveGasBNB
|
|
75
|
-
useNativeToken,
|
|
76
|
-
quoteToken,
|
|
77
|
-
quoteTokenDecimals,
|
|
78
|
-
provider: context.provider
|
|
68
|
+
reserveGas: config.reserveGasBNB
|
|
79
69
|
});
|
|
80
70
|
const quoteResult = await quoteTokenOutput({
|
|
81
71
|
routeParams,
|
|
@@ -89,8 +79,7 @@ export async function pancakeBundleBuyFirstMerkle(params) {
|
|
|
89
79
|
sellAmountToken: quoteResult.quotedTokenOut,
|
|
90
80
|
buyer,
|
|
91
81
|
seller,
|
|
92
|
-
tokenAddress
|
|
93
|
-
useNativeToken
|
|
82
|
+
tokenAddress
|
|
94
83
|
});
|
|
95
84
|
const quotedNative = await quoteSellerNative({
|
|
96
85
|
provider: context.provider,
|
|
@@ -168,11 +157,7 @@ export async function pancakeBundleBuyFirstMerkle(params) {
|
|
|
168
157
|
buyerBalance: buyerFundsInfo.buyerBalance,
|
|
169
158
|
reserveGas: buyerFundsInfo.reserveGas,
|
|
170
159
|
gasLimit: finalGasLimit,
|
|
171
|
-
gasPrice
|
|
172
|
-
useNativeToken,
|
|
173
|
-
quoteTokenDecimals,
|
|
174
|
-
provider: context.provider,
|
|
175
|
-
buyerAddress: buyer.address
|
|
160
|
+
gasPrice
|
|
176
161
|
});
|
|
177
162
|
const allTransactions = [];
|
|
178
163
|
if (approvalTx)
|
|
@@ -185,9 +170,7 @@ export async function pancakeBundleBuyFirstMerkle(params) {
|
|
|
185
170
|
metadata: {
|
|
186
171
|
buyerAddress: buyer.address,
|
|
187
172
|
sellerAddress: seller.address,
|
|
188
|
-
buyAmount:
|
|
189
|
-
? ethers.formatEther(buyerFundsInfo.buyerFundsWei)
|
|
190
|
-
: ethers.formatUnits(buyerFundsInfo.buyerFundsWei, quoteTokenDecimals),
|
|
173
|
+
buyAmount: ethers.formatEther(buyerFundsInfo.buyerFundsWei),
|
|
191
174
|
sellAmount: quoteResult.quotedTokenOut.toString(),
|
|
192
175
|
hasApproval: !!approvalTx,
|
|
193
176
|
profitAmount: profitAmount > 0n ? ethers.formatEther(profitAmount) : undefined
|
|
@@ -202,31 +185,16 @@ function createPancakeContext(config) {
|
|
|
202
185
|
});
|
|
203
186
|
return { chainId, provider };
|
|
204
187
|
}
|
|
205
|
-
async function calculateBuyerFunds({ buyer, buyerFunds, buyerFundsPercentage, reserveGas
|
|
188
|
+
async function calculateBuyerFunds({ buyer, buyerFunds, buyerFundsPercentage, reserveGas }) {
|
|
189
|
+
const buyerBalance = await buyer.provider.getBalance(buyer.address);
|
|
206
190
|
const reserveGasWei = ethers.parseEther((reserveGas ?? 0.0005).toString());
|
|
207
|
-
// ✅ 根据是否使用原生代币获取不同的余额
|
|
208
|
-
let buyerBalance;
|
|
209
|
-
if (useNativeToken) {
|
|
210
|
-
buyerBalance = await buyer.provider.getBalance(buyer.address);
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
// ERC20 代币余额
|
|
214
|
-
const erc20 = new Contract(quoteToken, ERC20_BALANCE_OF_ABI, provider || buyer.provider);
|
|
215
|
-
buyerBalance = await erc20.balanceOf(buyer.address);
|
|
216
|
-
}
|
|
217
191
|
let buyerFundsWei;
|
|
218
192
|
if (buyerFunds !== undefined) {
|
|
219
|
-
|
|
220
|
-
buyerFundsWei = useNativeToken
|
|
221
|
-
? ethers.parseEther(String(buyerFunds))
|
|
222
|
-
: ethers.parseUnits(String(buyerFunds), quoteTokenDecimals);
|
|
193
|
+
buyerFundsWei = ethers.parseEther(String(buyerFunds));
|
|
223
194
|
}
|
|
224
195
|
else if (buyerFundsPercentage !== undefined) {
|
|
225
196
|
const pct = Math.max(0, Math.min(100, buyerFundsPercentage));
|
|
226
|
-
|
|
227
|
-
const spendable = useNativeToken
|
|
228
|
-
? (buyerBalance > reserveGasWei ? buyerBalance - reserveGasWei : 0n)
|
|
229
|
-
: buyerBalance;
|
|
197
|
+
const spendable = buyerBalance > reserveGasWei ? buyerBalance - reserveGasWei : 0n;
|
|
230
198
|
buyerFundsWei = (spendable * BigInt(Math.floor(pct * 100))) / 10000n;
|
|
231
199
|
}
|
|
232
200
|
else {
|
|
@@ -235,23 +203,6 @@ async function calculateBuyerFunds({ buyer, buyerFunds, buyerFundsPercentage, re
|
|
|
235
203
|
if (buyerFundsWei <= 0n) {
|
|
236
204
|
throw new Error('buyerFunds 需要大于 0');
|
|
237
205
|
}
|
|
238
|
-
// ✅ 余额检查
|
|
239
|
-
if (useNativeToken) {
|
|
240
|
-
if (buyerBalance < buyerFundsWei + reserveGasWei) {
|
|
241
|
-
throw new Error(`买方余额不足: 需要 ${ethers.formatEther(buyerFundsWei + reserveGasWei)} BNB,实际 ${ethers.formatEther(buyerBalance)} BNB`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
// ERC20 购买:检查代币余额
|
|
246
|
-
if (buyerBalance < buyerFundsWei) {
|
|
247
|
-
throw new Error(`买方代币余额不足: 需要 ${ethers.formatUnits(buyerFundsWei, quoteTokenDecimals)},实际 ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`);
|
|
248
|
-
}
|
|
249
|
-
// ✅ ERC20 购买时,还需要检查买方是否有足够 BNB 支付 Gas
|
|
250
|
-
const buyerBnbBalance = await buyer.provider.getBalance(buyer.address);
|
|
251
|
-
if (buyerBnbBalance < reserveGasWei) {
|
|
252
|
-
throw new Error(`买方 BNB 余额不足 (用于支付 Gas): 需要 ${ethers.formatEther(reserveGasWei)} BNB,实际 ${ethers.formatEther(buyerBnbBalance)} BNB`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
206
|
return { buyerFundsWei, buyerBalance, reserveGas: reserveGasWei };
|
|
256
207
|
}
|
|
257
208
|
async function quoteTokenOutput({ routeParams, buyerFundsWei, provider }) {
|
|
@@ -343,31 +294,26 @@ async function ensureSellerApproval({ tokenAddress, seller, provider, decimals,
|
|
|
343
294
|
type: txType
|
|
344
295
|
});
|
|
345
296
|
}
|
|
346
|
-
async function buildRouteTransactions({ routeParams, buyerFundsWei, sellAmountToken, buyer, seller, tokenAddress
|
|
297
|
+
async function buildRouteTransactions({ routeParams, buyerFundsWei, sellAmountToken, buyer, seller, tokenAddress }) {
|
|
347
298
|
const proxyBuyer = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, buyer);
|
|
348
299
|
const proxySeller = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, seller);
|
|
349
300
|
const deadline = Math.floor(Date.now() / 1000) + 600;
|
|
350
|
-
// ✅ ERC20 购买时,value 只需要 FLAT_FEE
|
|
351
|
-
const buyValue = useNativeToken ? buyerFundsWei + FLAT_FEE : FLAT_FEE;
|
|
352
301
|
if (routeParams.routeType === 'v2') {
|
|
353
302
|
const { v2Path } = routeParams;
|
|
354
303
|
const reversePath = [...v2Path].reverse();
|
|
355
|
-
const buyUnsigned = await proxyBuyer.swapV2.populateTransaction(buyerFundsWei, 0n, v2Path, buyer.address, deadline, { value:
|
|
356
|
-
);
|
|
304
|
+
const buyUnsigned = await proxyBuyer.swapV2.populateTransaction(buyerFundsWei, 0n, v2Path, buyer.address, deadline, { value: buyerFundsWei + FLAT_FEE });
|
|
357
305
|
const sellUnsigned = await proxySeller.swapV2.populateTransaction(sellAmountToken, 0n, reversePath, seller.address, deadline, { value: FLAT_FEE });
|
|
358
306
|
return { buyUnsigned, sellUnsigned };
|
|
359
307
|
}
|
|
360
308
|
if (routeParams.routeType === 'v3-single') {
|
|
361
309
|
const { v3TokenIn, v3TokenOut, v3Fee } = routeParams;
|
|
362
|
-
const buyUnsigned = await proxyBuyer.swapV3Single.populateTransaction(v3TokenIn, v3TokenOut, v3Fee, buyerFundsWei, 0n, buyer.address, { value:
|
|
363
|
-
);
|
|
310
|
+
const buyUnsigned = await proxyBuyer.swapV3Single.populateTransaction(v3TokenIn, v3TokenOut, v3Fee, buyerFundsWei, 0n, buyer.address, { value: buyerFundsWei + FLAT_FEE });
|
|
364
311
|
const sellUnsigned = await proxySeller.swapV3Single.populateTransaction(v3TokenOut, v3TokenIn, v3Fee, sellAmountToken, 0n, seller.address, { value: FLAT_FEE });
|
|
365
312
|
return { buyUnsigned, sellUnsigned };
|
|
366
313
|
}
|
|
367
314
|
const { v3LpAddresses, v3ExactTokenIn } = routeParams;
|
|
368
315
|
const exactTokenOut = v3ExactTokenIn.toLowerCase() === WBNB_ADDRESS.toLowerCase() ? tokenAddress : WBNB_ADDRESS;
|
|
369
|
-
const buyUnsigned = await proxyBuyer.swapV3MultiHop.populateTransaction(v3LpAddresses, v3ExactTokenIn, buyerFundsWei, 0n, buyer.address, { value:
|
|
370
|
-
);
|
|
316
|
+
const buyUnsigned = await proxyBuyer.swapV3MultiHop.populateTransaction(v3LpAddresses, v3ExactTokenIn, buyerFundsWei, 0n, buyer.address, { value: buyerFundsWei + FLAT_FEE });
|
|
371
317
|
const sellUnsigned = await proxySeller.swapV3MultiHop.populateTransaction(v3LpAddresses, exactTokenOut, sellAmountToken, 0n, seller.address, { value: FLAT_FEE });
|
|
372
318
|
return { buyUnsigned, sellUnsigned };
|
|
373
319
|
}
|
|
@@ -425,41 +371,19 @@ async function buildProfitTransaction({ seller, profitAmount, profitNonce, gasPr
|
|
|
425
371
|
type: txType
|
|
426
372
|
});
|
|
427
373
|
}
|
|
428
|
-
|
|
429
|
-
const gasCost = gasLimit * gasPrice;
|
|
374
|
+
function validateFinalBalances({ sameAddress, buyerFundsWei, buyerBalance, reserveGas, gasLimit, gasPrice }) {
|
|
430
375
|
if (sameAddress) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
throw new Error(`账户余额不足:\n - 需要: ${ethers.formatEther(requiredCombined)} BNB(含两笔Gas与两笔手续费)\n - 实际: ${ethers.formatEther(buyerBalance)} BNB`);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
// ERC20:检查代币余额 + BNB Gas 余额
|
|
440
|
-
const requiredToken = buyerFundsWei + FLAT_FEE * 2n;
|
|
441
|
-
if (buyerBalance < requiredToken) {
|
|
442
|
-
throw new Error(`账户代币余额不足:\n - 需要: ${ethers.formatUnits(requiredToken, quoteTokenDecimals)}\n - 实际: ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`);
|
|
443
|
-
}
|
|
444
|
-
// 检查 BNB Gas
|
|
445
|
-
if (provider && buyerAddress) {
|
|
446
|
-
const bnbBalance = await provider.getBalance(buyerAddress);
|
|
447
|
-
const requiredGas = gasCost * 2n;
|
|
448
|
-
if (bnbBalance < requiredGas) {
|
|
449
|
-
throw new Error(`账户 BNB 余额不足 (用于支付 Gas):\n - 需要: ${ethers.formatEther(requiredGas)} BNB\n - 实际: ${ethers.formatEther(bnbBalance)} BNB`);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
376
|
+
const gasCost = gasLimit * gasPrice;
|
|
377
|
+
const requiredCombined = buyerFundsWei + FLAT_FEE * 2n + gasCost * 2n;
|
|
378
|
+
if (buyerBalance < requiredCombined) {
|
|
379
|
+
throw new Error(`账户余额不足:\n - 需要: ${ethers.formatEther(requiredCombined)} BNB(含两笔Gas与两笔手续费)\n - 实际: ${ethers.formatEther(buyerBalance)} BNB`);
|
|
452
380
|
}
|
|
453
381
|
return;
|
|
454
382
|
}
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
if (buyerBalance < requiredBuyer) {
|
|
459
|
-
throw new Error(`买方余额不足:\n - 需要: ${ethers.formatEther(requiredBuyer)} BNB\n - 实际: ${ethers.formatEther(buyerBalance)} BNB`);
|
|
460
|
-
}
|
|
383
|
+
const requiredBuyer = buyerFundsWei + FLAT_FEE + reserveGas;
|
|
384
|
+
if (buyerBalance < requiredBuyer) {
|
|
385
|
+
throw new Error(`买方余额不足:\n - 需要: ${ethers.formatEther(requiredBuyer)} BNB\n - 实际: ${ethers.formatEther(buyerBalance)} BNB`);
|
|
461
386
|
}
|
|
462
|
-
// ERC20 余额已在 calculateBuyerFunds 中检查过
|
|
463
387
|
}
|
|
464
388
|
function countTruthy(values) {
|
|
465
389
|
return values.filter(Boolean).length;
|
|
@@ -48,8 +48,6 @@ export interface PancakeBundleSwapSignParams {
|
|
|
48
48
|
routeParams: RouteParams;
|
|
49
49
|
slippageTolerance?: number;
|
|
50
50
|
config: PancakeSwapSignConfig;
|
|
51
|
-
quoteToken?: string;
|
|
52
|
-
quoteTokenDecimals?: number;
|
|
53
51
|
}
|
|
54
52
|
export interface PancakeBundleSwapParams {
|
|
55
53
|
sellerPrivateKey: string;
|
|
@@ -75,6 +73,5 @@ export type PancakeSwapResult = {
|
|
|
75
73
|
};
|
|
76
74
|
/**
|
|
77
75
|
* PancakeSwap捆绑换手(V2/V3通用)
|
|
78
|
-
* ✅ 支持 quoteToken:传入 USDT 等地址时,卖出得到该代币,买入使用该代币
|
|
79
76
|
*/
|
|
80
77
|
export declare function pancakeBundleSwapMerkle(params: PancakeBundleSwapSignParams): Promise<PancakeSwapResult>;
|
|
@@ -69,25 +69,21 @@ async function quoteSellOutput({ routeParams, sellAmountWei, provider }) {
|
|
|
69
69
|
}
|
|
70
70
|
throw new Error('V3 多跳需要提供 v2Path 用于价格预估');
|
|
71
71
|
}
|
|
72
|
-
async function buildSwapTransactions({ routeParams, sellAmountWei, buyAmountBNB, buyer, seller, tokenAddress
|
|
72
|
+
async function buildSwapTransactions({ routeParams, sellAmountWei, buyAmountBNB, buyer, seller, tokenAddress }) {
|
|
73
73
|
const proxySeller = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, seller);
|
|
74
74
|
const proxyBuyer = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, buyer);
|
|
75
75
|
const deadline = Math.floor(Date.now() / 1000) + 600;
|
|
76
|
-
// ✅ ERC20 购买时,value 只需要 FLAT_FEE
|
|
77
|
-
const buyValue = useNativeToken ? buyAmountBNB + FLAT_FEE : FLAT_FEE;
|
|
78
76
|
if (routeParams.routeType === 'v2') {
|
|
79
77
|
const { v2Path } = routeParams;
|
|
80
78
|
const reversePath = [...v2Path].reverse();
|
|
81
79
|
const sellUnsigned = await proxySeller.swapV2.populateTransaction(sellAmountWei, 0n, v2Path, seller.address, deadline, { value: FLAT_FEE });
|
|
82
|
-
const buyUnsigned = await proxyBuyer.swapV2.populateTransaction(buyAmountBNB, 0n, reversePath, buyer.address, deadline, { value:
|
|
83
|
-
);
|
|
80
|
+
const buyUnsigned = await proxyBuyer.swapV2.populateTransaction(buyAmountBNB, 0n, reversePath, buyer.address, deadline, { value: buyAmountBNB + FLAT_FEE });
|
|
84
81
|
return { sellUnsigned, buyUnsigned };
|
|
85
82
|
}
|
|
86
83
|
if (routeParams.routeType === 'v3-single') {
|
|
87
84
|
const { v3TokenIn, v3TokenOut, v3Fee } = routeParams;
|
|
88
85
|
const sellUnsigned = await proxySeller.swapV3Single.populateTransaction(v3TokenIn, v3TokenOut, v3Fee, sellAmountWei, 0n, seller.address, { value: FLAT_FEE });
|
|
89
|
-
const buyUnsigned = await proxyBuyer.swapV3Single.populateTransaction(v3TokenOut, v3TokenIn, v3Fee, buyAmountBNB, 0n, buyer.address, { value:
|
|
90
|
-
);
|
|
86
|
+
const buyUnsigned = await proxyBuyer.swapV3Single.populateTransaction(v3TokenOut, v3TokenIn, v3Fee, buyAmountBNB, 0n, buyer.address, { value: buyAmountBNB + FLAT_FEE });
|
|
91
87
|
return { sellUnsigned, buyUnsigned };
|
|
92
88
|
}
|
|
93
89
|
const { v3LpAddresses, v3ExactTokenIn } = routeParams;
|
|
@@ -95,38 +91,18 @@ async function buildSwapTransactions({ routeParams, sellAmountWei, buyAmountBNB,
|
|
|
95
91
|
? tokenAddress
|
|
96
92
|
: WBNB_ADDRESS;
|
|
97
93
|
const sellUnsigned = await proxySeller.swapV3MultiHop.populateTransaction(v3LpAddresses, v3ExactTokenIn, sellAmountWei, 0n, seller.address, { value: FLAT_FEE });
|
|
98
|
-
const buyUnsigned = await proxyBuyer.swapV3MultiHop.populateTransaction(v3LpAddresses, exactTokenOut, buyAmountBNB, 0n, buyer.address, { value:
|
|
99
|
-
);
|
|
94
|
+
const buyUnsigned = await proxyBuyer.swapV3MultiHop.populateTransaction(v3LpAddresses, exactTokenOut, buyAmountBNB, 0n, buyer.address, { value: buyAmountBNB + FLAT_FEE });
|
|
100
95
|
return { sellUnsigned, buyUnsigned };
|
|
101
96
|
}
|
|
102
|
-
async function calculateBuyerBudget({ buyer, quotedBNBOut, reserveGasBNB, slippageTolerance
|
|
97
|
+
async function calculateBuyerBudget({ buyer, quotedBNBOut, reserveGasBNB, slippageTolerance }) {
|
|
98
|
+
const buyerBalance = await buyer.provider.getBalance(buyer.address);
|
|
103
99
|
const reserveGas = ethers.parseEther((reserveGasBNB ?? 0.0005).toString());
|
|
104
100
|
const buyAmountBNB = applySlippage(quotedBNBOut, slippageTolerance);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
buyerBalance = await buyer.provider.getBalance(buyer.address);
|
|
109
|
-
const requiredBalance = buyAmountBNB + FLAT_FEE + reserveGas;
|
|
110
|
-
if (buyerBalance < requiredBalance) {
|
|
111
|
-
throw new Error(`买方余额不足: 需要 ${ethers.formatEther(requiredBalance)} BNB, 实际 ${ethers.formatEther(buyerBalance)} BNB`);
|
|
112
|
-
}
|
|
113
|
-
return { buyerBalance, reserveGas, requiredBalance, buyAmountBNB, useNativeToken };
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
// ERC20 代币余额
|
|
117
|
-
const erc20 = new Contract(quoteToken, ERC20_BALANCE_OF_ABI, provider || buyer.provider);
|
|
118
|
-
buyerBalance = await erc20.balanceOf(buyer.address);
|
|
119
|
-
const requiredBalance = buyAmountBNB + FLAT_FEE; // ERC20 不需要预留 Gas 在代币余额中
|
|
120
|
-
if (buyerBalance < requiredBalance) {
|
|
121
|
-
throw new Error(`买方代币余额不足: 需要 ${ethers.formatUnits(requiredBalance, quoteTokenDecimals)}, 实际 ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`);
|
|
122
|
-
}
|
|
123
|
-
// ✅ ERC20 购买时,还需要检查买方是否有足够 BNB 支付 Gas
|
|
124
|
-
const buyerBnbBalance = await buyer.provider.getBalance(buyer.address);
|
|
125
|
-
if (buyerBnbBalance < reserveGas) {
|
|
126
|
-
throw new Error(`买方 BNB 余额不足 (用于支付 Gas): 需要 ${ethers.formatEther(reserveGas)} BNB, 实际 ${ethers.formatEther(buyerBnbBalance)} BNB`);
|
|
127
|
-
}
|
|
128
|
-
return { buyerBalance, reserveGas, requiredBalance, buyAmountBNB, useNativeToken };
|
|
101
|
+
const requiredBalance = buyAmountBNB + FLAT_FEE + reserveGas;
|
|
102
|
+
if (buyerBalance < requiredBalance) {
|
|
103
|
+
throw new Error(`买方余额不足: 需要 ${ethers.formatEther(requiredBalance)} BNB, 实际 ${ethers.formatEther(buyerBalance)} BNB`);
|
|
129
104
|
}
|
|
105
|
+
return { buyerBalance, reserveGas, requiredBalance, buyAmountBNB };
|
|
130
106
|
}
|
|
131
107
|
function applySlippage(amount, tolerancePercent = 0.5) {
|
|
132
108
|
if (amount === 0n) {
|
|
@@ -175,41 +151,19 @@ function calculateProfitAmount(estimatedBNBOut) {
|
|
|
175
151
|
}
|
|
176
152
|
return (estimatedBNBOut * BigInt(PROFIT_CONFIG.RATE_BPS)) / 10000n;
|
|
177
153
|
}
|
|
178
|
-
|
|
179
|
-
const gasCost = gasLimit * gasPrice;
|
|
154
|
+
function validateFinalBalances({ sameAddress, buyerBalance, buyAmountBNB, reserveGas, gasLimit, gasPrice }) {
|
|
180
155
|
if (sameAddress) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
throw new Error(`账户余额不足:\n - 需要: ${ethers.formatEther(requiredCombined)} BNB(含两笔Gas与两笔手续费)\n - 实际: ${ethers.formatEther(buyerBalance)} BNB`);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
// ERC20:检查代币余额 + BNB Gas 余额
|
|
190
|
-
const requiredToken = buyAmountBNB + FLAT_FEE * 2n;
|
|
191
|
-
if (buyerBalance < requiredToken) {
|
|
192
|
-
throw new Error(`账户代币余额不足:\n - 需要: ${ethers.formatUnits(requiredToken, quoteTokenDecimals)}\n - 实际: ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`);
|
|
193
|
-
}
|
|
194
|
-
// 检查 BNB Gas
|
|
195
|
-
if (provider && buyerAddress) {
|
|
196
|
-
const bnbBalance = await provider.getBalance(buyerAddress);
|
|
197
|
-
const requiredGas = gasCost * 2n;
|
|
198
|
-
if (bnbBalance < requiredGas) {
|
|
199
|
-
throw new Error(`账户 BNB 余额不足 (用于支付 Gas):\n - 需要: ${ethers.formatEther(requiredGas)} BNB\n - 实际: ${ethers.formatEther(bnbBalance)} BNB`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
156
|
+
const gasCost = gasLimit * gasPrice;
|
|
157
|
+
const requiredCombined = buyAmountBNB + FLAT_FEE * 2n + gasCost * 2n;
|
|
158
|
+
if (buyerBalance < requiredCombined) {
|
|
159
|
+
throw new Error(`账户余额不足:\n - 需要: ${ethers.formatEther(requiredCombined)} BNB(含两笔Gas与两笔手续费)\n - 实际: ${ethers.formatEther(buyerBalance)} BNB`);
|
|
202
160
|
}
|
|
203
161
|
return;
|
|
204
162
|
}
|
|
205
|
-
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
if (buyerBalance < requiredBuyer) {
|
|
209
|
-
throw new Error(`买方余额不足:\n - 需要: ${ethers.formatEther(requiredBuyer)} BNB\n - 实际: ${ethers.formatEther(buyerBalance)} BNB`);
|
|
210
|
-
}
|
|
163
|
+
const requiredBuyer = buyAmountBNB + FLAT_FEE + reserveGas;
|
|
164
|
+
if (buyerBalance < requiredBuyer) {
|
|
165
|
+
throw new Error(`买方余额不足:\n - 需要: ${ethers.formatEther(requiredBuyer)} BNB\n - 实际: ${ethers.formatEther(buyerBalance)} BNB`);
|
|
211
166
|
}
|
|
212
|
-
// ERC20 余额已在 calculateBuyerBudget 中检查过
|
|
213
167
|
}
|
|
214
168
|
function countTruthy(values) {
|
|
215
169
|
return values.filter(Boolean).length;
|
|
@@ -316,18 +270,11 @@ const ERC20_ALLOWANCE_ABI = [
|
|
|
316
270
|
'function decimals() view returns (uint8)'
|
|
317
271
|
];
|
|
318
272
|
const APPROVE_INTERFACE = new ethers.Interface(['function approve(address,uint256) returns (bool)']);
|
|
319
|
-
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
320
|
-
const ERC20_BALANCE_OF_ABI = ['function balanceOf(address) view returns (uint256)'];
|
|
321
273
|
/**
|
|
322
274
|
* PancakeSwap捆绑换手(V2/V3通用)
|
|
323
|
-
* ✅ 支持 quoteToken:传入 USDT 等地址时,卖出得到该代币,买入使用该代币
|
|
324
275
|
*/
|
|
325
276
|
export async function pancakeBundleSwapMerkle(params) {
|
|
326
|
-
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKey, tokenAddress, routeParams, slippageTolerance = 0.5, config
|
|
327
|
-
// ✅ 判断是否使用原生代币(BNB)或 ERC20 代币(如 USDT)
|
|
328
|
-
const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
|
|
329
|
-
console.log('🔍 PancakeSwap Swap - quoteToken:', quoteToken);
|
|
330
|
-
console.log('🔍 PancakeSwap Swap - useNativeToken:', useNativeToken);
|
|
277
|
+
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKey, tokenAddress, routeParams, slippageTolerance = 0.5, config } = params;
|
|
331
278
|
const context = createPancakeContext(config);
|
|
332
279
|
const seller = new Wallet(sellerPrivateKey, context.provider);
|
|
333
280
|
const buyer = new Wallet(buyerPrivateKey, context.provider);
|
|
@@ -350,11 +297,7 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
350
297
|
buyer,
|
|
351
298
|
quotedBNBOut: quoteResult.estimatedBNBOut,
|
|
352
299
|
reserveGasBNB: config.reserveGasBNB,
|
|
353
|
-
slippageTolerance
|
|
354
|
-
useNativeToken,
|
|
355
|
-
quoteToken,
|
|
356
|
-
quoteTokenDecimals,
|
|
357
|
-
provider: context.provider
|
|
300
|
+
slippageTolerance
|
|
358
301
|
});
|
|
359
302
|
const swapUnsigned = await buildSwapTransactions({
|
|
360
303
|
routeParams,
|
|
@@ -362,8 +305,7 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
362
305
|
buyAmountBNB: buyerBudget.buyAmountBNB,
|
|
363
306
|
buyer,
|
|
364
307
|
seller,
|
|
365
|
-
tokenAddress
|
|
366
|
-
useNativeToken
|
|
308
|
+
tokenAddress
|
|
367
309
|
});
|
|
368
310
|
const finalGasLimit = getGasLimit(config);
|
|
369
311
|
const gasPrice = await getGasPrice(context.provider, config);
|
|
@@ -411,11 +353,7 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
411
353
|
buyAmountBNB: buyerBudget.buyAmountBNB,
|
|
412
354
|
reserveGas: buyerBudget.reserveGas,
|
|
413
355
|
gasLimit: finalGasLimit,
|
|
414
|
-
gasPrice
|
|
415
|
-
useNativeToken,
|
|
416
|
-
quoteTokenDecimals,
|
|
417
|
-
provider: context.provider,
|
|
418
|
-
buyerAddress: buyer.address
|
|
356
|
+
gasPrice
|
|
419
357
|
});
|
|
420
358
|
const signedTransactions = [];
|
|
421
359
|
if (approvalTx)
|
|
@@ -429,9 +367,7 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
429
367
|
sellerAddress: seller.address,
|
|
430
368
|
buyerAddress: buyer.address,
|
|
431
369
|
sellAmount: ethers.formatUnits(sellAmountWei, decimals),
|
|
432
|
-
buyAmount:
|
|
433
|
-
? ethers.formatEther(buyerBudget.buyAmountBNB)
|
|
434
|
-
: ethers.formatUnits(buyerBudget.buyAmountBNB, quoteTokenDecimals),
|
|
370
|
+
buyAmount: ethers.formatEther(buyerBudget.buyAmountBNB),
|
|
435
371
|
hasApproval: !!approvalTx,
|
|
436
372
|
profitAmount: profitAmount > 0n ? ethers.formatEther(profitAmount) : undefined
|
|
437
373
|
}
|
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
|
-
}
|