four-flap-meme-sdk 1.3.39 → 1.3.40
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/contracts/tm-bundle-merkle/pancake-proxy.js +49 -17
- package/dist/flap/portal-bundle-merkle/encryption.d.ts +16 -0
- package/dist/flap/portal-bundle-merkle/encryption.js +146 -0
- package/dist/flap/portal-bundle-merkle/pancake-proxy.js +86 -54
- package/dist/flap/portal-bundle-merkle/swap-buy-first.js +25 -24
- package/package.json +1 -1
|
@@ -303,17 +303,25 @@ export async function fourPancakeProxyBatchBuyMerkle(params) {
|
|
|
303
303
|
const actualAmountsWei = remainingAmounts; // 扣除利润后用于购买的金额
|
|
304
304
|
const finalGasLimit = getGasLimit(config);
|
|
305
305
|
const nonceManager = new NonceManager(provider);
|
|
306
|
-
// ✅ 优化:并行获取 gasPrice
|
|
307
|
-
const [gasPrice, tokenDecimals
|
|
306
|
+
// ✅ 优化:并行获取 gasPrice 和 tokenDecimals
|
|
307
|
+
const [gasPrice, tokenDecimals] = await Promise.all([
|
|
308
308
|
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
309
|
-
getTokenDecimals(tokenAddress, provider)
|
|
310
|
-
nonceManager.getNextNoncesForWallets(buyers) // ✅ 批量获取 nonce
|
|
309
|
+
getTokenDecimals(tokenAddress, provider)
|
|
311
310
|
]);
|
|
312
|
-
// ✅
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
311
|
+
// ✅ 修复:先计算需要的 nonce 数量,再统一获取
|
|
312
|
+
// buyers[0] 可能需要 2 个 nonce(买入 + 利润转账)
|
|
313
|
+
const needProfitTx = extractProfit && totalProfit > 0n;
|
|
314
|
+
const buyer0NeedCount = needProfitTx ? 2 : 1;
|
|
315
|
+
// 获取 buyers[0] 的连续 nonces
|
|
316
|
+
const buyer0Nonces = await nonceManager.getNextNonceBatch(buyers[0], buyer0NeedCount);
|
|
317
|
+
// 获取其他 buyers 的 nonces(如果有)
|
|
318
|
+
let otherNonces = [];
|
|
319
|
+
if (buyers.length > 1) {
|
|
320
|
+
otherNonces = await nonceManager.getNextNoncesForWallets(buyers.slice(1));
|
|
321
|
+
}
|
|
322
|
+
// 组装最终的 nonces 数组
|
|
323
|
+
const nonces = [buyer0Nonces[0], ...otherNonces];
|
|
324
|
+
const profitNonce = needProfitTx ? buyer0Nonces[1] : undefined;
|
|
317
325
|
// 计算 minOutputAmounts
|
|
318
326
|
let minOuts;
|
|
319
327
|
if (params.minOutputAmounts && params.minOutputAmounts.length === buyers.length) {
|
|
@@ -401,12 +409,11 @@ export async function fourPancakeProxyBatchSellMerkle(params) {
|
|
|
401
409
|
const finalGasLimit = getGasLimit(config);
|
|
402
410
|
const nonceManager = new NonceManager(provider);
|
|
403
411
|
const extractProfit = shouldExtractProfit(config);
|
|
404
|
-
// ✅ 优化:并行获取 gasPrice、tokenDecimals
|
|
405
|
-
const [gasPrice, tokenDecimals, allowances
|
|
412
|
+
// ✅ 优化:并行获取 gasPrice、tokenDecimals 和 allowances
|
|
413
|
+
const [gasPrice, tokenDecimals, allowances] = await Promise.all([
|
|
406
414
|
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
407
415
|
getTokenDecimals(tokenAddress, provider),
|
|
408
|
-
batchCheckAllowances(provider, tokenAddress, sellers.map(w => w.address), pancakeProxyAddress)
|
|
409
|
-
nonceManager.getNextNoncesForWallets(sellers) // ✅ 批量获取 nonce
|
|
416
|
+
batchCheckAllowances(provider, tokenAddress, sellers.map(w => w.address), pancakeProxyAddress)
|
|
410
417
|
]);
|
|
411
418
|
const amountsWei = sellAmounts.map(a => ethers.parseUnits(a, tokenDecimals));
|
|
412
419
|
// 找出需要授权的钱包索引
|
|
@@ -464,7 +471,7 @@ export async function fourPancakeProxyBatchSellMerkle(params) {
|
|
|
464
471
|
// ✅ minOuts = 0,不设置滑点限制(大额交易更稳定)
|
|
465
472
|
minOuts = quotedOutputs.map(() => 0n);
|
|
466
473
|
}
|
|
467
|
-
// ✅
|
|
474
|
+
// ✅ 计算利润并找出收益最多的钱包
|
|
468
475
|
let totalProfit = 0n;
|
|
469
476
|
let maxRevenueIndex = 0;
|
|
470
477
|
let maxRevenue = 0n;
|
|
@@ -480,10 +487,35 @@ export async function fourPancakeProxyBatchSellMerkle(params) {
|
|
|
480
487
|
}
|
|
481
488
|
}
|
|
482
489
|
}
|
|
483
|
-
// ✅
|
|
490
|
+
// ✅ 修复:先计算需要的 nonce 数量,再统一获取
|
|
491
|
+
const needProfitTx = extractProfit && totalProfit > 0n && maxRevenue > 0n;
|
|
492
|
+
// 分配 nonces:收益最多的钱包可能需要 2 个 nonce(卖出 + 利润转账)
|
|
493
|
+
let nonces;
|
|
484
494
|
let profitNonce;
|
|
485
|
-
if (
|
|
486
|
-
|
|
495
|
+
if (needProfitTx) {
|
|
496
|
+
// 收益最多的钱包需要 2 个连续 nonce
|
|
497
|
+
const maxRevenueNonces = await nonceManager.getNextNonceBatch(sellers[maxRevenueIndex], 2);
|
|
498
|
+
// 其他钱包各需要 1 个 nonce
|
|
499
|
+
const otherSellers = sellers.filter((_, i) => i !== maxRevenueIndex);
|
|
500
|
+
const otherNonces = otherSellers.length > 0
|
|
501
|
+
? await nonceManager.getNextNoncesForWallets(otherSellers)
|
|
502
|
+
: [];
|
|
503
|
+
// 组装最终的 nonces 数组(保持原顺序)
|
|
504
|
+
nonces = [];
|
|
505
|
+
let otherIdx = 0;
|
|
506
|
+
for (let i = 0; i < sellers.length; i++) {
|
|
507
|
+
if (i === maxRevenueIndex) {
|
|
508
|
+
nonces.push(maxRevenueNonces[0]); // 卖出交易用第一个 nonce
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
nonces.push(otherNonces[otherIdx++]);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
profitNonce = maxRevenueNonces[1]; // 利润交易用第二个 nonce
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
// 不需要利润交易,所有钱包各 1 个 nonce
|
|
518
|
+
nonces = await nonceManager.getNextNoncesForWallets(sellers);
|
|
487
519
|
}
|
|
488
520
|
// 卖出不需要发送 BNB,只需要 flatFee
|
|
489
521
|
const needBNB = false;
|
|
@@ -0,0 +1,16 @@
|
|
|
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;
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
}
|
|
@@ -354,8 +354,8 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
354
354
|
const finalGasLimit = getGasLimit(config);
|
|
355
355
|
const extractProfit = shouldExtractProfit(config);
|
|
356
356
|
const nonceManager = new NonceManager(provider);
|
|
357
|
-
// ✅ 优化:第一批并行 - gasPrice、tokenDecimals、allowances
|
|
358
|
-
const [gasPrice, tokenDecimals
|
|
357
|
+
// ✅ 优化:第一批并行 - gasPrice、tokenDecimals、allowances
|
|
358
|
+
const [gasPrice, tokenDecimals] = await Promise.all([
|
|
359
359
|
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
360
360
|
getTokenDecimals(tokenAddress, provider),
|
|
361
361
|
ensureAllowances({
|
|
@@ -363,8 +363,7 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
363
363
|
tokenAddress,
|
|
364
364
|
owners: sellers.map(w => w.address),
|
|
365
365
|
spender: ADDRESSES.BSC.PancakeProxy
|
|
366
|
-
})
|
|
367
|
-
nonceManager.getNextNoncesForWallets(sellers) // ✅ 批量获取 nonce
|
|
366
|
+
})
|
|
368
367
|
]);
|
|
369
368
|
const amountsWei = sellAmounts.map(amount => ethers.parseUnits(amount, tokenDecimals));
|
|
370
369
|
// ✅ 优化:第二批并行 - resolveSellOutputs 和 buildSellTransactions
|
|
@@ -376,6 +375,52 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
376
375
|
routeType,
|
|
377
376
|
amountsWei
|
|
378
377
|
});
|
|
378
|
+
// ✅ 修复:先计算利润和 maxRevenueIndex,再统一分配 nonces
|
|
379
|
+
let totalProfit = 0n;
|
|
380
|
+
let maxRevenueIndex = -1;
|
|
381
|
+
let maxRevenue = 0n;
|
|
382
|
+
if (extractProfit && quotedOutputs.length > 0) {
|
|
383
|
+
for (let i = 0; i < sellers.length; i++) {
|
|
384
|
+
const quoted = quotedOutputs[i];
|
|
385
|
+
if (quoted > 0n) {
|
|
386
|
+
const { profit } = calculateProfit(quoted, config);
|
|
387
|
+
totalProfit += profit;
|
|
388
|
+
if (quoted > maxRevenue) {
|
|
389
|
+
maxRevenue = quoted;
|
|
390
|
+
maxRevenueIndex = i;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// ✅ 修复:根据是否需要利润交易,统一分配 nonces
|
|
396
|
+
const needProfitTx = extractProfit && totalProfit > 0n && maxRevenueIndex >= 0;
|
|
397
|
+
let nonces;
|
|
398
|
+
let profitNonce;
|
|
399
|
+
if (needProfitTx) {
|
|
400
|
+
// maxRevenueIndex 钱包需要 2 个连续 nonce(卖出 + 利润)
|
|
401
|
+
const maxRevenueNonces = await nonceManager.getNextNonceBatch(sellers[maxRevenueIndex], 2);
|
|
402
|
+
// 其他钱包各需要 1 个 nonce
|
|
403
|
+
const otherSellers = sellers.filter((_, i) => i !== maxRevenueIndex);
|
|
404
|
+
const otherNonces = otherSellers.length > 0
|
|
405
|
+
? await nonceManager.getNextNoncesForWallets(otherSellers)
|
|
406
|
+
: [];
|
|
407
|
+
// 组装最终的 nonces 数组(保持原顺序)
|
|
408
|
+
nonces = [];
|
|
409
|
+
let otherIdx = 0;
|
|
410
|
+
for (let i = 0; i < sellers.length; i++) {
|
|
411
|
+
if (i === maxRevenueIndex) {
|
|
412
|
+
nonces.push(maxRevenueNonces[0]); // 卖出交易用第一个 nonce
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
nonces.push(otherNonces[otherIdx++]);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
profitNonce = maxRevenueNonces[1]; // 利润交易用第二个 nonce
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// 不需要利润交易,所有钱包各 1 个 nonce
|
|
422
|
+
nonces = await nonceManager.getNextNoncesForWallets(sellers);
|
|
423
|
+
}
|
|
379
424
|
const unsignedSells = await buildSellTransactions({
|
|
380
425
|
routeType,
|
|
381
426
|
params,
|
|
@@ -399,16 +444,19 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
399
444
|
value: txValue
|
|
400
445
|
});
|
|
401
446
|
}));
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
447
|
+
// ✅ 添加利润交易(使用预先分配的 profitNonce)
|
|
448
|
+
if (needProfitTx && profitNonce !== undefined) {
|
|
449
|
+
const profitTx = await sellers[maxRevenueIndex].signTransaction({
|
|
450
|
+
to: getProfitRecipient(),
|
|
451
|
+
value: totalProfit,
|
|
452
|
+
nonce: profitNonce,
|
|
453
|
+
gasPrice,
|
|
454
|
+
gasLimit: 21000n,
|
|
455
|
+
chainId,
|
|
456
|
+
type: getTxType(config)
|
|
457
|
+
});
|
|
458
|
+
signedTxs.push(profitTx);
|
|
459
|
+
}
|
|
412
460
|
nonceManager.clearTemp();
|
|
413
461
|
return {
|
|
414
462
|
signedTransactions: signedTxs
|
|
@@ -479,15 +527,31 @@ async function buildBuyTransactions({ routeType, params, proxies, wallets, token
|
|
|
479
527
|
throw new Error(`Unsupported routeType: ${routeType}`);
|
|
480
528
|
}
|
|
481
529
|
/**
|
|
482
|
-
* ✅
|
|
530
|
+
* ✅ 修复:明确分配 nonces,避免隐式状态依赖
|
|
483
531
|
*/
|
|
484
532
|
async function allocateProfitAwareNonces(wallets, extractProfit, maxIndex, totalProfit, nonceManager) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
533
|
+
const needProfitTx = extractProfit && totalProfit > 0n && maxIndex >= 0 && maxIndex < wallets.length;
|
|
534
|
+
if (!needProfitTx) {
|
|
535
|
+
// 不需要利润交易,所有钱包各 1 个 nonce
|
|
536
|
+
return await nonceManager.getNextNoncesForWallets(wallets);
|
|
537
|
+
}
|
|
538
|
+
// 需要利润交易:maxIndex 钱包需要 2 个连续 nonce
|
|
539
|
+
const maxIndexNonces = await nonceManager.getNextNonceBatch(wallets[maxIndex], 2);
|
|
540
|
+
// 其他钱包各需要 1 个 nonce
|
|
541
|
+
const otherWallets = wallets.filter((_, i) => i !== maxIndex);
|
|
542
|
+
const otherNonces = otherWallets.length > 0
|
|
543
|
+
? await nonceManager.getNextNoncesForWallets(otherWallets)
|
|
544
|
+
: [];
|
|
545
|
+
// 组装最终的 nonces 数组(保持原顺序)
|
|
546
|
+
const nonces = [];
|
|
547
|
+
let otherIdx = 0;
|
|
548
|
+
for (let i = 0; i < wallets.length; i++) {
|
|
549
|
+
if (i === maxIndex) {
|
|
550
|
+
nonces.push(maxIndexNonces[0]); // 买入交易用第一个 nonce
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
nonces.push(otherNonces[otherIdx++]);
|
|
554
|
+
}
|
|
491
555
|
}
|
|
492
556
|
return nonces;
|
|
493
557
|
}
|
|
@@ -641,36 +705,4 @@ async function buildSellTransactions({ routeType, params, proxies, wallets, toke
|
|
|
641
705
|
}
|
|
642
706
|
throw new Error(`Unsupported routeType: ${routeType}`);
|
|
643
707
|
}
|
|
644
|
-
|
|
645
|
-
if (!extractProfit || quotedOutputs.length === 0) {
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
let totalProfit = 0n;
|
|
649
|
-
let maxRevenueIndex = -1;
|
|
650
|
-
let maxRevenue = 0n;
|
|
651
|
-
for (let i = 0; i < wallets.length; i++) {
|
|
652
|
-
const quoted = quotedOutputs[i];
|
|
653
|
-
if (quoted > 0n) {
|
|
654
|
-
const { profit } = calculateProfit(quoted, config);
|
|
655
|
-
totalProfit += profit;
|
|
656
|
-
if (quoted > maxRevenue) {
|
|
657
|
-
maxRevenue = quoted;
|
|
658
|
-
maxRevenueIndex = i;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
if (totalProfit === 0n || maxRevenueIndex < 0) {
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
const profitNonce = await nonceManager.getNextNonce(wallets[maxRevenueIndex]);
|
|
666
|
-
const profitTx = await wallets[maxRevenueIndex].signTransaction({
|
|
667
|
-
to: getProfitRecipient(),
|
|
668
|
-
value: totalProfit,
|
|
669
|
-
nonce: profitNonce,
|
|
670
|
-
gasPrice,
|
|
671
|
-
gasLimit: 21000n,
|
|
672
|
-
chainId,
|
|
673
|
-
type: getTxType(config)
|
|
674
|
-
});
|
|
675
|
-
signedTxs.push(profitTx);
|
|
676
|
-
}
|
|
708
|
+
// ✅ appendSellProfitTransfer 已内联到 pancakeProxyBatchSellMerkle 中,避免 nonce 竞争问题
|
|
@@ -147,8 +147,8 @@ export async function flapBundleBuyFirstMerkle(params) {
|
|
|
147
147
|
});
|
|
148
148
|
const portalBuyer = new Contract(chainContext.portalAddress, PORTAL_ABI, buyer);
|
|
149
149
|
const portalSeller = new Contract(chainContext.portalAddress, PORTAL_ABI, seller);
|
|
150
|
-
// ✅
|
|
151
|
-
const [buyUnsigned, sellUnsigned,
|
|
150
|
+
// ✅ 第三批并行 - buyUnsigned、sellUnsigned、estimatedSellFunds(不含 nonce 相关操作)
|
|
151
|
+
const [buyUnsigned, sellUnsigned, estimatedSellFunds] = await Promise.all([
|
|
152
152
|
portalBuyer.swapExactInput.populateTransaction({
|
|
153
153
|
inputToken, // ✅ 使用动态输入代币
|
|
154
154
|
outputToken: tokenAddress,
|
|
@@ -164,29 +164,30 @@ export async function flapBundleBuyFirstMerkle(params) {
|
|
|
164
164
|
minOutputAmount: 0,
|
|
165
165
|
permitData: '0x'
|
|
166
166
|
}),
|
|
167
|
-
|
|
168
|
-
tokenAddress,
|
|
169
|
-
provider: chainContext.provider,
|
|
170
|
-
seller,
|
|
171
|
-
decimals: sellerInfo.decimals,
|
|
172
|
-
portalAddress: chainContext.portalAddress,
|
|
173
|
-
chainId: chainContext.chainId,
|
|
174
|
-
config,
|
|
175
|
-
nonceManager,
|
|
176
|
-
gasPrice,
|
|
177
|
-
txType
|
|
178
|
-
}),
|
|
179
|
-
estimateSellFunds(portalSeller, tokenAddress, sellAmountWei, outputToken),
|
|
180
|
-
// 预先规划 nonces(假设可能需要利润转账)
|
|
181
|
-
planNonces({
|
|
182
|
-
buyer,
|
|
183
|
-
seller,
|
|
184
|
-
approvalExists: true, // 保守估计,假设需要授权
|
|
185
|
-
extractProfit: true, // 保守估计,假设需要利润转账
|
|
186
|
-
sameAddress,
|
|
187
|
-
nonceManager
|
|
188
|
-
})
|
|
167
|
+
estimateSellFunds(portalSeller, tokenAddress, sellAmountWei, outputToken)
|
|
189
168
|
]);
|
|
169
|
+
// ✅ 修复:先构建授权交易(会消耗 nonce),再规划其他 nonce
|
|
170
|
+
const approvalTx = await buildApprovalTransaction({
|
|
171
|
+
tokenAddress,
|
|
172
|
+
provider: chainContext.provider,
|
|
173
|
+
seller,
|
|
174
|
+
decimals: sellerInfo.decimals,
|
|
175
|
+
portalAddress: chainContext.portalAddress,
|
|
176
|
+
chainId: chainContext.chainId,
|
|
177
|
+
config,
|
|
178
|
+
nonceManager,
|
|
179
|
+
gasPrice,
|
|
180
|
+
txType
|
|
181
|
+
});
|
|
182
|
+
// ✅ 修复:根据实际的授权情况规划 nonce
|
|
183
|
+
const noncePlan = await planNonces({
|
|
184
|
+
buyer,
|
|
185
|
+
seller,
|
|
186
|
+
approvalExists: approvalTx !== null, // ✅ 使用实际值
|
|
187
|
+
extractProfit: true,
|
|
188
|
+
sameAddress,
|
|
189
|
+
nonceManager
|
|
190
|
+
});
|
|
190
191
|
// ✅ 修复:基于卖出收益估算利润
|
|
191
192
|
const profitBase = estimatedSellFunds > 0n ? estimatedSellFunds : buyerFundsWei;
|
|
192
193
|
const tokenProfitAmount = calculateProfitAmount(profitBase);
|