four-flap-meme-sdk 1.3.88 → 1.3.89
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/clients/blockrazor.js +0 -1
- package/dist/contracts/tm-bundle-merkle/core.js +6 -3
- package/dist/contracts/tm-bundle-merkle/pancake-proxy.js +30 -38
- package/dist/contracts/tm-bundle-merkle/swap-buy-first.d.ts +0 -1
- package/dist/contracts/tm-bundle-merkle/swap-buy-first.js +4 -3
- package/dist/contracts/tm-bundle-merkle/swap.d.ts +0 -3
- package/dist/contracts/tm-bundle-merkle/swap.js +2 -2
- package/dist/flap/portal-bundle-merkle/core.js +2 -6
- 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 +55 -35
- package/dist/flap/portal-bundle-merkle/swap-buy-first.d.ts +0 -2
- package/dist/flap/portal-bundle-merkle/swap-buy-first.js +6 -11
- package/dist/flap/portal-bundle-merkle/swap.d.ts +0 -2
- package/dist/flap/portal-bundle-merkle/swap.js +10 -22
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/pancake/bundle-buy-first.d.ts +0 -1
- package/dist/pancake/bundle-buy-first.js +4 -9
- package/dist/pancake/bundle-swap.d.ts +0 -4
- package/dist/pancake/bundle-swap.js +7 -14
- package/dist/utils/erc20.d.ts +108 -2
- package/dist/utils/erc20.js +65 -17
- package/package.json +4 -39
- package/dist/sol/constants.d.ts +0 -126
- package/dist/sol/constants.js +0 -145
- package/dist/sol/dex/index.d.ts +0 -8
- package/dist/sol/dex/index.js +0 -12
- package/dist/sol/dex/meteora/client.d.ts +0 -76
- package/dist/sol/dex/meteora/client.js +0 -219
- package/dist/sol/dex/meteora/damm-v1-bundle.d.ts +0 -61
- package/dist/sol/dex/meteora/damm-v1-bundle.js +0 -112
- package/dist/sol/dex/meteora/damm-v1.d.ts +0 -118
- package/dist/sol/dex/meteora/damm-v1.js +0 -315
- package/dist/sol/dex/meteora/damm-v2-bundle.d.ts +0 -82
- package/dist/sol/dex/meteora/damm-v2-bundle.js +0 -242
- package/dist/sol/dex/meteora/damm-v2.d.ts +0 -172
- package/dist/sol/dex/meteora/damm-v2.js +0 -632
- package/dist/sol/dex/meteora/dbc-bundle.d.ts +0 -123
- package/dist/sol/dex/meteora/dbc-bundle.js +0 -304
- package/dist/sol/dex/meteora/dbc.d.ts +0 -192
- package/dist/sol/dex/meteora/dbc.js +0 -619
- package/dist/sol/dex/meteora/dlmm-bundle.d.ts +0 -39
- package/dist/sol/dex/meteora/dlmm-bundle.js +0 -189
- package/dist/sol/dex/meteora/dlmm.d.ts +0 -146
- package/dist/sol/dex/meteora/dlmm.js +0 -593
- package/dist/sol/dex/meteora/index.d.ts +0 -25
- package/dist/sol/dex/meteora/index.js +0 -65
- package/dist/sol/dex/meteora/types.d.ts +0 -787
- package/dist/sol/dex/meteora/types.js +0 -110
- package/dist/sol/dex/orca/index.d.ts +0 -10
- package/dist/sol/dex/orca/index.js +0 -16
- package/dist/sol/dex/orca/orca-bundle.d.ts +0 -41
- package/dist/sol/dex/orca/orca-bundle.js +0 -173
- package/dist/sol/dex/orca/orca.d.ts +0 -65
- package/dist/sol/dex/orca/orca.js +0 -474
- package/dist/sol/dex/orca/types.d.ts +0 -263
- package/dist/sol/dex/orca/types.js +0 -38
- package/dist/sol/dex/orca/wavebreak-bundle.d.ts +0 -34
- package/dist/sol/dex/orca/wavebreak-bundle.js +0 -198
- package/dist/sol/dex/orca/wavebreak-types.d.ts +0 -227
- package/dist/sol/dex/orca/wavebreak-types.js +0 -23
- package/dist/sol/dex/orca/wavebreak.d.ts +0 -78
- package/dist/sol/dex/orca/wavebreak.js +0 -486
- package/dist/sol/dex/pump/index.d.ts +0 -9
- package/dist/sol/dex/pump/index.js +0 -14
- package/dist/sol/dex/pump/pump-bundle.d.ts +0 -92
- package/dist/sol/dex/pump/pump-bundle.js +0 -383
- package/dist/sol/dex/pump/pump-swap-bundle.d.ts +0 -103
- package/dist/sol/dex/pump/pump-swap-bundle.js +0 -380
- package/dist/sol/dex/pump/pump-swap.d.ts +0 -46
- package/dist/sol/dex/pump/pump-swap.js +0 -199
- package/dist/sol/dex/pump/pump.d.ts +0 -35
- package/dist/sol/dex/pump/pump.js +0 -352
- package/dist/sol/dex/pump/types.d.ts +0 -215
- package/dist/sol/dex/pump/types.js +0 -5
- package/dist/sol/dex/raydium/index.d.ts +0 -8
- package/dist/sol/dex/raydium/index.js +0 -12
- package/dist/sol/dex/raydium/launchlab.d.ts +0 -68
- package/dist/sol/dex/raydium/launchlab.js +0 -210
- package/dist/sol/dex/raydium/raydium-bundle.d.ts +0 -64
- package/dist/sol/dex/raydium/raydium-bundle.js +0 -324
- package/dist/sol/dex/raydium/raydium.d.ts +0 -40
- package/dist/sol/dex/raydium/raydium.js +0 -366
- package/dist/sol/dex/raydium/types.d.ts +0 -240
- package/dist/sol/dex/raydium/types.js +0 -5
- package/dist/sol/index.d.ts +0 -10
- package/dist/sol/index.js +0 -16
- package/dist/sol/jito/bundle.d.ts +0 -90
- package/dist/sol/jito/bundle.js +0 -263
- package/dist/sol/jito/index.d.ts +0 -7
- package/dist/sol/jito/index.js +0 -7
- package/dist/sol/jito/tip.d.ts +0 -51
- package/dist/sol/jito/tip.js +0 -83
- package/dist/sol/jito/types.d.ts +0 -100
- package/dist/sol/jito/types.js +0 -5
- package/dist/sol/token/create-complete.d.ts +0 -115
- package/dist/sol/token/create-complete.js +0 -235
- package/dist/sol/token/create-token.d.ts +0 -57
- package/dist/sol/token/create-token.js +0 -230
- package/dist/sol/token/index.d.ts +0 -9
- package/dist/sol/token/index.js +0 -14
- package/dist/sol/token/metadata-upload.d.ts +0 -86
- package/dist/sol/token/metadata-upload.js +0 -173
- package/dist/sol/token/metadata.d.ts +0 -92
- package/dist/sol/token/metadata.js +0 -274
- package/dist/sol/token/types.d.ts +0 -153
- package/dist/sol/token/types.js +0 -5
- package/dist/sol/types.d.ts +0 -176
- package/dist/sol/types.js +0 -7
- package/dist/sol/utils/balance.d.ts +0 -160
- package/dist/sol/utils/balance.js +0 -638
- package/dist/sol/utils/connection.d.ts +0 -78
- package/dist/sol/utils/connection.js +0 -168
- package/dist/sol/utils/index.d.ts +0 -9
- package/dist/sol/utils/index.js +0 -9
- package/dist/sol/utils/lp-inspect.d.ts +0 -129
- package/dist/sol/utils/lp-inspect.js +0 -521
- package/dist/sol/utils/transfer.d.ts +0 -125
- package/dist/sol/utils/transfer.js +0 -220
- package/dist/sol/utils/wallet.d.ts +0 -107
- package/dist/sol/utils/wallet.js +0 -210
|
@@ -151,7 +151,6 @@ export class BlockRazorClient {
|
|
|
151
151
|
const gasPrice = tx.gasPrice || 0n;
|
|
152
152
|
const minGasPrice = ethers.parseUnits(String(MIN_GAS_PRICE_GWEI), 'gwei');
|
|
153
153
|
if (gasPrice < minGasPrice) {
|
|
154
|
-
console.warn(`⚠️ 交易 Gas Price (${ethers.formatUnits(gasPrice, 'gwei')} Gwei) 低于最低要求 (${MIN_GAS_PRICE_GWEI} Gwei)`);
|
|
155
154
|
}
|
|
156
155
|
}
|
|
157
156
|
catch {
|
|
@@ -412,7 +412,8 @@ async function resolveSellOutputs(params) {
|
|
|
412
412
|
try {
|
|
413
413
|
const result = await trySell('BSC', rpcUrl, tokenAddress, amountsWei[0]);
|
|
414
414
|
const quotedOutputs = [result.funds];
|
|
415
|
-
|
|
415
|
+
// ✅ 已移除滑点保护:minOuts 固定为 0
|
|
416
|
+
const minOuts = [0n];
|
|
416
417
|
return { minOuts, quotedOutputs };
|
|
417
418
|
}
|
|
418
419
|
catch {
|
|
@@ -445,7 +446,8 @@ async function resolveSellOutputs(params) {
|
|
|
445
446
|
}
|
|
446
447
|
return 0n;
|
|
447
448
|
});
|
|
448
|
-
|
|
449
|
+
// ✅ 已移除滑点保护:minOuts 固定为 0
|
|
450
|
+
const minOuts = quotedOutputs.map(() => 0n);
|
|
449
451
|
return { minOuts, quotedOutputs };
|
|
450
452
|
}
|
|
451
453
|
catch {
|
|
@@ -459,7 +461,8 @@ async function resolveSellOutputs(params) {
|
|
|
459
461
|
return 0n;
|
|
460
462
|
}
|
|
461
463
|
}));
|
|
462
|
-
|
|
464
|
+
// ✅ 已移除滑点保护:minOuts 固定为 0
|
|
465
|
+
const minOuts = quotedOutputs.map(() => 0n);
|
|
463
466
|
return { minOuts, quotedOutputs };
|
|
464
467
|
}
|
|
465
468
|
}
|
|
@@ -429,48 +429,40 @@ export async function fourPancakeProxyBatchSellMerkle(params) {
|
|
|
429
429
|
if (needApprovalIndexes.length > 0) {
|
|
430
430
|
throw new Error(`需要授权: ${needApprovalIndexes.length} 个钱包尚未授权。请先完成授权后再卖出。`);
|
|
431
431
|
}
|
|
432
|
-
// ✅
|
|
433
|
-
let minOuts;
|
|
432
|
+
// ✅ 获取报价(用于计算利润),已移除滑点保护
|
|
434
433
|
let quotedOutputs;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
434
|
+
// ✅ 使用 Multicall3 批量获取报价(仅 V2 路由支持)
|
|
435
|
+
if (routeType === 'v2' && params.v2Path && params.v2Path.length >= 2) {
|
|
436
|
+
quotedOutputs = await batchGetV2Quotes(provider, amountsWei, params.v2Path);
|
|
437
|
+
}
|
|
438
|
+
else if (routeType === 'v3-single') {
|
|
439
|
+
// V3 单跳:并行调用(V3 Quoter 是 non-view 函数,不支持 Multicall3)
|
|
440
|
+
quotedOutputs = await Promise.all(amountsWei.map(async (amount) => {
|
|
441
|
+
try {
|
|
442
|
+
const quoter = new Contract(PANCAKE_V3_QUOTER_ADDRESS, PANCAKE_V3_QUOTER_ABI, provider);
|
|
443
|
+
const result = await quoter.quoteExactInputSingle.staticCall({
|
|
444
|
+
tokenIn: tokenAddress,
|
|
445
|
+
tokenOut: params.v3TokenOut,
|
|
446
|
+
amountIn: amount,
|
|
447
|
+
fee: params.v3Fee,
|
|
448
|
+
sqrtPriceLimitX96: 0
|
|
449
|
+
});
|
|
450
|
+
return result[0];
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return 0n;
|
|
454
|
+
}
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
else if (routeType === 'v3-multi' && params.v2Path && params.v2Path.length >= 2) {
|
|
458
|
+
// V3 多跳:使用 V2 备选路由
|
|
459
|
+
quotedOutputs = await batchGetV2Quotes(provider, amountsWei, params.v2Path);
|
|
439
460
|
}
|
|
440
461
|
else {
|
|
441
|
-
|
|
442
|
-
if (routeType === 'v2' && params.v2Path && params.v2Path.length >= 2) {
|
|
443
|
-
quotedOutputs = await batchGetV2Quotes(provider, amountsWei, params.v2Path);
|
|
444
|
-
}
|
|
445
|
-
else if (routeType === 'v3-single') {
|
|
446
|
-
// V3 单跳:并行调用(V3 Quoter 是 non-view 函数,不支持 Multicall3)
|
|
447
|
-
quotedOutputs = await Promise.all(amountsWei.map(async (amount) => {
|
|
448
|
-
try {
|
|
449
|
-
const quoter = new Contract(PANCAKE_V3_QUOTER_ADDRESS, PANCAKE_V3_QUOTER_ABI, provider);
|
|
450
|
-
const result = await quoter.quoteExactInputSingle.staticCall({
|
|
451
|
-
tokenIn: tokenAddress,
|
|
452
|
-
tokenOut: params.v3TokenOut,
|
|
453
|
-
amountIn: amount,
|
|
454
|
-
fee: params.v3Fee,
|
|
455
|
-
sqrtPriceLimitX96: 0
|
|
456
|
-
});
|
|
457
|
-
return result[0];
|
|
458
|
-
}
|
|
459
|
-
catch {
|
|
460
|
-
return 0n;
|
|
461
|
-
}
|
|
462
|
-
}));
|
|
463
|
-
}
|
|
464
|
-
else if (routeType === 'v3-multi' && params.v2Path && params.v2Path.length >= 2) {
|
|
465
|
-
// V3 多跳:使用 V2 备选路由
|
|
466
|
-
quotedOutputs = await batchGetV2Quotes(provider, amountsWei, params.v2Path);
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
quotedOutputs = new Array(amountsWei.length).fill(0n);
|
|
470
|
-
}
|
|
471
|
-
// ✅ minOuts = 0,不设置滑点限制(大额交易更稳定)
|
|
472
|
-
minOuts = quotedOutputs.map(() => 0n);
|
|
462
|
+
quotedOutputs = new Array(amountsWei.length).fill(0n);
|
|
473
463
|
}
|
|
464
|
+
// ✅ 已移除滑点保护:minOuts 固定为 0
|
|
465
|
+
const minOuts = new Array(sellers.length).fill(0n);
|
|
474
466
|
// ✅ 计算利润并找出收益最多的钱包
|
|
475
467
|
let totalProfit = 0n;
|
|
476
468
|
let maxRevenueIndex = 0;
|
|
@@ -7,7 +7,6 @@ import { CommonBundleConfig } from '../../utils/bundle-helpers.js';
|
|
|
7
7
|
import { FourSignConfig } from './types.js';
|
|
8
8
|
export interface FourBuyFirstSignConfig extends FourSignConfig {
|
|
9
9
|
reserveGasBNB?: number;
|
|
10
|
-
slippageBps?: number;
|
|
11
10
|
}
|
|
12
11
|
export interface FourBuyFirstConfig extends CommonBundleConfig {
|
|
13
12
|
apiKey: string;
|
|
@@ -82,8 +82,8 @@ export async function fourBundleBuyFirstMerkle(params) {
|
|
|
82
82
|
// ✅ 优化:第三批并行 - trySell、构建交易、获取 nonces
|
|
83
83
|
const tmBuyer = new Contract(TM_ADDRESS, TM_ABI, buyer);
|
|
84
84
|
const tmSeller = new Contract(TM_ADDRESS, TM_ABI, seller);
|
|
85
|
-
|
|
86
|
-
const minBuyAmount =
|
|
85
|
+
// ✅ 已移除滑点保护:minBuyAmount 固定为 0
|
|
86
|
+
const minBuyAmount = 0n;
|
|
87
87
|
// 预先规划 nonces
|
|
88
88
|
const extractProfit = true;
|
|
89
89
|
const profitRateBps = getProfitRateBps();
|
|
@@ -121,7 +121,8 @@ export async function fourBundleBuyFirstMerkle(params) {
|
|
|
121
121
|
]);
|
|
122
122
|
const { buyerNonces, sellerNonces } = noncesResult;
|
|
123
123
|
const estimatedSellFunds = sellResult.funds;
|
|
124
|
-
|
|
124
|
+
// ✅ 已移除滑点保护:minSellFunds 固定为 0
|
|
125
|
+
const minSellFunds = 0n;
|
|
125
126
|
const profitAmount = extractProfit ? (estimatedSellFunds * BigInt(profitRateBps)) / 10000n : 0n;
|
|
126
127
|
// 更新卖出交易的 minSellFunds
|
|
127
128
|
sellUnsigned.data = tmSeller.interface.encodeFunctionData('sellToken', [
|
|
@@ -29,7 +29,6 @@ export interface FourBundleSwapSignParams {
|
|
|
29
29
|
sellPercentage?: number;
|
|
30
30
|
buyerPrivateKey: string;
|
|
31
31
|
tokenAddress: string;
|
|
32
|
-
slippageTolerance?: number;
|
|
33
32
|
config: FourSwapSignConfig;
|
|
34
33
|
}
|
|
35
34
|
export interface FourBundleSwapParams {
|
|
@@ -38,7 +37,6 @@ export interface FourBundleSwapParams {
|
|
|
38
37
|
sellPercentage?: number;
|
|
39
38
|
buyerPrivateKey: string;
|
|
40
39
|
tokenAddress: string;
|
|
41
|
-
slippageTolerance?: number;
|
|
42
40
|
config: FourSwapConfig;
|
|
43
41
|
}
|
|
44
42
|
/**
|
|
@@ -69,7 +67,6 @@ export interface FourBatchSwapSignParams {
|
|
|
69
67
|
buyerPrivateKeys: string[];
|
|
70
68
|
buyerRatios?: number[];
|
|
71
69
|
tokenAddress: string;
|
|
72
|
-
slippageTolerance?: number;
|
|
73
70
|
config: FourSwapSignConfig;
|
|
74
71
|
}
|
|
75
72
|
/**
|
|
@@ -12,7 +12,7 @@ import { TM_ABI, HELPER3_ABI, TM_ADDRESS } from './swap-internal.js';
|
|
|
12
12
|
* Four内盘捆绑换手
|
|
13
13
|
*/
|
|
14
14
|
export async function fourBundleSwapMerkle(params) {
|
|
15
|
-
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKey, tokenAddress,
|
|
15
|
+
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKey, tokenAddress, config } = params;
|
|
16
16
|
const chainIdNum = config.chainId ?? 56;
|
|
17
17
|
const provider = new ethers.JsonRpcProvider(config.rpcUrl, {
|
|
18
18
|
chainId: chainIdNum,
|
|
@@ -185,7 +185,7 @@ export async function fourBundleSwapMerkle(params) {
|
|
|
185
185
|
* 限制:最多 24 个买方(服务器限制 25 笔交易,包含 1 笔利润交易)
|
|
186
186
|
*/
|
|
187
187
|
export async function fourBatchSwapMerkle(params) {
|
|
188
|
-
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, tokenAddress,
|
|
188
|
+
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, tokenAddress, config } = params;
|
|
189
189
|
// ✅ 校验买方数量(最多 24 个)
|
|
190
190
|
const MAX_BUYERS = 24;
|
|
191
191
|
if (buyerPrivateKeys.length === 0) {
|
|
@@ -628,12 +628,8 @@ async function quoteSellOutputsWithQuote(portal, tokenAddress, amountsWei, outpu
|
|
|
628
628
|
}));
|
|
629
629
|
}
|
|
630
630
|
}
|
|
631
|
-
function resolveMinOutputs(
|
|
632
|
-
|
|
633
|
-
return provided.map(m => typeof m === 'string' ? ethers.parseEther(m) : BigInt(m));
|
|
634
|
-
}
|
|
635
|
-
// ✅ 默认 minOutput = 0,不设置滑点限制
|
|
636
|
-
// 原因:大额交易时 5% 滑点可能不够,导致交易失败
|
|
631
|
+
function resolveMinOutputs(_provided, walletCount, _quotedOutputs) {
|
|
632
|
+
// ✅ 已移除滑点保护:minOutput 固定为 0
|
|
637
633
|
return Array(walletCount).fill(0n);
|
|
638
634
|
}
|
|
639
635
|
// ✅ appendSellProfitTransaction 已内联到 batchSellWithBundleMerkle 中,避免 nonce 竞争问题
|
|
@@ -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
|
+
}
|
|
@@ -56,16 +56,27 @@ function getGasLimit(config, defaultGas = DEFAULT_GAS_LIMIT) {
|
|
|
56
56
|
return BigInt(calculatedGas);
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* 查询代币 decimals
|
|
59
|
+
* 查询代币 decimals(带缓存)
|
|
60
|
+
* ✅ 代币精度不会变化,缓存后永久有效
|
|
60
61
|
*/
|
|
61
62
|
async function getTokenDecimals(tokenAddress, provider) {
|
|
63
|
+
const cacheKey = tokenAddress.toLowerCase();
|
|
64
|
+
// ✅ 检查缓存
|
|
65
|
+
const cached = tokenDecimalsCache.get(cacheKey);
|
|
66
|
+
if (cached !== undefined) {
|
|
67
|
+
return cached;
|
|
68
|
+
}
|
|
62
69
|
try {
|
|
63
70
|
const token = new Contract(tokenAddress, ERC20_ABI, provider);
|
|
64
71
|
const decimals = await token.decimals();
|
|
65
|
-
|
|
72
|
+
const result = Number(decimals);
|
|
73
|
+
// ✅ 缓存结果
|
|
74
|
+
tokenDecimalsCache.set(cacheKey, result);
|
|
75
|
+
return result;
|
|
66
76
|
}
|
|
67
77
|
catch {
|
|
68
|
-
// 默认返回 18,兼容大部分 ERC20
|
|
78
|
+
// 默认返回 18,兼容大部分 ERC20(也缓存)
|
|
79
|
+
tokenDecimalsCache.set(cacheKey, 18);
|
|
69
80
|
return 18;
|
|
70
81
|
}
|
|
71
82
|
}
|
|
@@ -281,11 +292,21 @@ export async function pancakeProxyBatchBuyMerkle(params) {
|
|
|
281
292
|
const divisor = BigInt(10 ** decimalsDiff);
|
|
282
293
|
actualAmountsWei = remainingAmounts.map(amount => amount / divisor);
|
|
283
294
|
}
|
|
284
|
-
// ✅
|
|
295
|
+
// ✅ 优化:如果前端传入了 gasPrice 和 nonces,跳过 RPC 调用
|
|
296
|
+
const presetGasPrice = config.gasPrice;
|
|
297
|
+
const presetNonces = config.nonces;
|
|
298
|
+
// ✅ 只获取必需的数据(跳过已有的)
|
|
285
299
|
const [gasPrice, tokenDecimals, nonces] = await Promise.all([
|
|
286
|
-
|
|
300
|
+
// gasPrice:优先使用前端传入的
|
|
301
|
+
presetGasPrice !== undefined
|
|
302
|
+
? Promise.resolve(presetGasPrice)
|
|
303
|
+
: getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
304
|
+
// tokenDecimals:有缓存
|
|
287
305
|
getTokenDecimals(tokenAddress, provider),
|
|
288
|
-
|
|
306
|
+
// nonces:优先使用前端传入的
|
|
307
|
+
presetNonces && presetNonces.length === buyers.length
|
|
308
|
+
? Promise.resolve(presetNonces)
|
|
309
|
+
: allocateProfitAwareNonces(buyers, shouldExtractProfitForBuy, maxFundsIndex, nativeProfitAmount, nonceManager)
|
|
289
310
|
]);
|
|
290
311
|
const minOuts = resolveBuyMinOutputs(params, buyers.length, tokenDecimals);
|
|
291
312
|
const needBNB = needSendBNB(routeType, params, useNativeToken);
|
|
@@ -350,10 +371,18 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
350
371
|
const finalGasLimit = getGasLimit(config);
|
|
351
372
|
const extractProfit = shouldExtractProfit(config);
|
|
352
373
|
const nonceManager = new NonceManager(provider);
|
|
374
|
+
// ✅ 优化:如果前端传入了 gasPrice 和 nonces,跳过 RPC 调用
|
|
375
|
+
const presetGasPrice = config.gasPrice;
|
|
376
|
+
const presetNonces = config.nonces;
|
|
353
377
|
// ✅ 优化:第一批并行 - gasPrice、tokenDecimals、allowances
|
|
354
378
|
const [gasPrice, tokenDecimals] = await Promise.all([
|
|
355
|
-
|
|
379
|
+
// gasPrice:优先使用前端传入的
|
|
380
|
+
presetGasPrice !== undefined
|
|
381
|
+
? Promise.resolve(presetGasPrice)
|
|
382
|
+
: getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
383
|
+
// tokenDecimals:有缓存
|
|
356
384
|
getTokenDecimals(tokenAddress, provider),
|
|
385
|
+
// allowances:必须检查
|
|
357
386
|
ensureAllowances({
|
|
358
387
|
provider,
|
|
359
388
|
tokenAddress,
|
|
@@ -362,8 +391,8 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
362
391
|
})
|
|
363
392
|
]);
|
|
364
393
|
const amountsWei = sellAmounts.map(amount => ethers.parseUnits(amount, tokenDecimals));
|
|
365
|
-
// ✅ 优化:第二批并行 - resolveSellOutputs 和 buildSellTransactions
|
|
366
394
|
const proxies = createPancakeProxies(sellers, ADDRESSES.BSC.PancakeProxy);
|
|
395
|
+
// 获取报价(用于计算利润)
|
|
367
396
|
const { minOuts, quotedOutputs } = await resolveSellOutputs({
|
|
368
397
|
params,
|
|
369
398
|
provider,
|
|
@@ -392,7 +421,12 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
392
421
|
const needProfitTx = extractProfit && totalProfit > 0n && maxRevenueIndex >= 0;
|
|
393
422
|
let nonces;
|
|
394
423
|
let profitNonce;
|
|
395
|
-
|
|
424
|
+
// ✅ 优化:如果前端传入了 nonces,直接使用
|
|
425
|
+
if (presetNonces && presetNonces.length === sellers.length) {
|
|
426
|
+
nonces = presetNonces;
|
|
427
|
+
profitNonce = needProfitTx ? presetNonces[maxRevenueIndex] + 1 : undefined;
|
|
428
|
+
}
|
|
429
|
+
else if (needProfitTx) {
|
|
396
430
|
// maxRevenueIndex 钱包需要 2 个连续 nonce(卖出 + 利润)
|
|
397
431
|
const maxRevenueNonces = await nonceManager.getNextNonceBatch(sellers[maxRevenueIndex], 2);
|
|
398
432
|
// 其他钱包各需要 1 个 nonce
|
|
@@ -461,6 +495,8 @@ export async function pancakeProxyBatchSellMerkle(params) {
|
|
|
461
495
|
// ✅ Provider 缓存(复用连接,减少初始化开销)
|
|
462
496
|
const providerCache = new Map();
|
|
463
497
|
const PROVIDER_CACHE_TTL_MS = 60 * 1000; // 60秒缓存
|
|
498
|
+
// ✅ Token Decimals 缓存(代币精度不会变化)
|
|
499
|
+
const tokenDecimalsCache = new Map();
|
|
464
500
|
function createChainContext(chain, rpcUrl) {
|
|
465
501
|
const chainId = CHAIN_ID_MAP[chain];
|
|
466
502
|
const cacheKey = `${chain}-${rpcUrl}`;
|
|
@@ -492,10 +528,8 @@ function findMaxAmountIndex(amounts) {
|
|
|
492
528
|
}
|
|
493
529
|
return maxIndex;
|
|
494
530
|
}
|
|
495
|
-
function resolveBuyMinOutputs(
|
|
496
|
-
|
|
497
|
-
return params.minOutputAmounts.map(m => typeof m === 'string' ? ethers.parseUnits(m, tokenDecimals) : m);
|
|
498
|
-
}
|
|
531
|
+
function resolveBuyMinOutputs(_params, walletCount, _tokenDecimals) {
|
|
532
|
+
// ✅ 已移除滑点保护:minOutput 固定为 0
|
|
499
533
|
return new Array(walletCount).fill(0n);
|
|
500
534
|
}
|
|
501
535
|
function createPancakeProxies(wallets, proxyAddress) {
|
|
@@ -576,26 +610,18 @@ async function ensureAllowances({ provider, tokenAddress, owners, spender }) {
|
|
|
576
610
|
}
|
|
577
611
|
}
|
|
578
612
|
/**
|
|
579
|
-
* ✅
|
|
613
|
+
* ✅ 获取卖出报价(用于计算利润,不用于滑点保护)
|
|
614
|
+
* ✅ 已移除滑点保护:minOutput 固定为 0
|
|
580
615
|
*/
|
|
581
616
|
async function resolveSellOutputs({ params, provider, tokenAddress, routeType, amountsWei }) {
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
const minOuts = params.minOutputAmounts.map(m => typeof m === 'string' ? ethers.parseEther(m) : m);
|
|
585
|
-
return {
|
|
586
|
-
minOuts,
|
|
587
|
-
quotedOutputs: minOuts.map(m => m * 100n / 95n)
|
|
588
|
-
};
|
|
589
|
-
}
|
|
617
|
+
// ✅ 已移除滑点保护:minOutput 固定为 0
|
|
618
|
+
const minOuts = new Array(amountsWei.length).fill(0n);
|
|
590
619
|
// 如果只有 1 个,直接调用(避免 multicall 开销)
|
|
591
620
|
if (amountsWei.length === 1) {
|
|
592
621
|
const quotedOutput = await getSingleQuote(params, provider, tokenAddress, routeType, amountsWei[0]);
|
|
593
|
-
return {
|
|
594
|
-
quotedOutputs: [quotedOutput],
|
|
595
|
-
minOuts: [0n] // ✅ minOutput = 0,不设置滑点限制
|
|
596
|
-
};
|
|
622
|
+
return { quotedOutputs: [quotedOutput], minOuts };
|
|
597
623
|
}
|
|
598
|
-
// ✅ 使用 Multicall3 批量获取报价(仅 V2
|
|
624
|
+
// ✅ 使用 Multicall3 批量获取报价(仅 V2 路由支持,用于计算利润)
|
|
599
625
|
if (routeType === 'v2' && params.v2Path && params.v2Path.length >= 2) {
|
|
600
626
|
try {
|
|
601
627
|
const v2RouterIface = new Interface(PANCAKE_V2_ROUTER_ABI);
|
|
@@ -619,10 +645,7 @@ async function resolveSellOutputs({ params, provider, tokenAddress, routeType, a
|
|
|
619
645
|
}
|
|
620
646
|
return 0n;
|
|
621
647
|
});
|
|
622
|
-
return {
|
|
623
|
-
quotedOutputs,
|
|
624
|
-
minOuts: quotedOutputs.map(() => 0n) // ✅ minOutput = 0,不设置滑点限制
|
|
625
|
-
};
|
|
648
|
+
return { quotedOutputs, minOuts };
|
|
626
649
|
}
|
|
627
650
|
catch {
|
|
628
651
|
// Multicall 失败,回退到并行调用
|
|
@@ -630,10 +653,7 @@ async function resolveSellOutputs({ params, provider, tokenAddress, routeType, a
|
|
|
630
653
|
}
|
|
631
654
|
// 回退:并行调用(V3 路由或 Multicall 失败时)
|
|
632
655
|
const quotedOutputs = await Promise.all(amountsWei.map(amount => getSingleQuote(params, provider, tokenAddress, routeType, amount)));
|
|
633
|
-
return {
|
|
634
|
-
quotedOutputs,
|
|
635
|
-
minOuts: quotedOutputs.map(() => 0n) // ✅ minOutput = 0,不设置滑点限制
|
|
636
|
-
};
|
|
656
|
+
return { quotedOutputs, minOuts };
|
|
637
657
|
}
|
|
638
658
|
/**
|
|
639
659
|
* 获取单个报价
|
|
@@ -8,7 +8,6 @@ import { FlapSignConfig } from './config.js';
|
|
|
8
8
|
export type FlapChain = 'bsc' | 'xlayer' | 'base';
|
|
9
9
|
export interface FlapBuyFirstSignConfig extends FlapSignConfig {
|
|
10
10
|
reserveGasETH?: number;
|
|
11
|
-
slippageBps?: number;
|
|
12
11
|
skipQuoteOnError?: boolean;
|
|
13
12
|
}
|
|
14
13
|
export interface FlapBuyFirstConfig extends CommonBundleConfig {
|
|
@@ -16,7 +15,6 @@ export interface FlapBuyFirstConfig extends CommonBundleConfig {
|
|
|
16
15
|
customRpcUrl?: string;
|
|
17
16
|
bundleBlockOffset?: number;
|
|
18
17
|
reserveGasETH?: number;
|
|
19
|
-
slippageBps?: number;
|
|
20
18
|
skipQuoteOnError?: boolean;
|
|
21
19
|
waitForConfirmation?: boolean;
|
|
22
20
|
waitTimeoutMs?: number;
|
|
@@ -124,12 +124,11 @@ export async function flapBundleBuyFirstMerkle(params) {
|
|
|
124
124
|
]);
|
|
125
125
|
const { buyerFundsWei, buyerBalance } = buyerFundsResult;
|
|
126
126
|
const priorityFee = gasPrice / 10n === 0n ? 1n : gasPrice / 10n;
|
|
127
|
-
// ✅
|
|
127
|
+
// ✅ 获取报价(已移除滑点保护)
|
|
128
128
|
const quoteResult = await quoteBuyerOutput({
|
|
129
129
|
portalAddress: chainContext.portalAddress,
|
|
130
130
|
tokenAddress,
|
|
131
131
|
buyerFundsWei,
|
|
132
|
-
slippageBps: config.slippageBps,
|
|
133
132
|
provider: chainContext.provider,
|
|
134
133
|
skipQuoteOnError: config.skipQuoteOnError,
|
|
135
134
|
inputToken // ✅ 传递输入代币
|
|
@@ -300,35 +299,31 @@ async function calculateBuyerFunds({ buyer, buyerFunds, buyerFundsPercentage, re
|
|
|
300
299
|
}
|
|
301
300
|
return { buyerFundsWei, buyerBalance };
|
|
302
301
|
}
|
|
303
|
-
async function quoteBuyerOutput({ portalAddress, tokenAddress, buyerFundsWei,
|
|
302
|
+
async function quoteBuyerOutput({ portalAddress, tokenAddress, buyerFundsWei, provider, skipQuoteOnError, inputToken = ZERO_ADDRESS // ✅ 默认使用原生代币
|
|
304
303
|
}) {
|
|
305
304
|
let quotedToken = 0n;
|
|
306
|
-
let minOutToken = 0n;
|
|
307
305
|
const portal = new Contract(portalAddress, PORTAL_ABI, provider);
|
|
308
|
-
const safeSlippage = Math.max(0, Math.min(5000, slippageBps ?? 100));
|
|
309
306
|
try {
|
|
310
307
|
quotedToken = await portal.quoteExactInput.staticCall({
|
|
311
308
|
inputToken, // ✅ 使用动态输入代币
|
|
312
309
|
outputToken: tokenAddress,
|
|
313
310
|
inputAmount: buyerFundsWei
|
|
314
311
|
});
|
|
315
|
-
const keep = BigInt(10000 - safeSlippage);
|
|
316
|
-
minOutToken = (quotedToken * keep) / 10000n;
|
|
317
312
|
}
|
|
318
313
|
catch (error) {
|
|
319
314
|
if (skipQuoteOnError ?? true) {
|
|
320
315
|
quotedToken = 0n;
|
|
321
|
-
minOutToken = 0n;
|
|
322
316
|
}
|
|
323
317
|
else {
|
|
324
318
|
throw new Error(`买入报价失败: ${error}`);
|
|
325
319
|
}
|
|
326
320
|
}
|
|
327
|
-
|
|
321
|
+
// ✅ 已移除滑点保护:minOutToken 固定为 0
|
|
322
|
+
const sellAmountWei = quotedToken;
|
|
328
323
|
if (sellAmountWei <= 0n) {
|
|
329
|
-
throw new Error('卖方卖出数量为 0
|
|
324
|
+
throw new Error('卖方卖出数量为 0:报价失败');
|
|
330
325
|
}
|
|
331
|
-
return { quotedToken, minOutToken, sellAmountWei };
|
|
326
|
+
return { quotedToken, minOutToken: 0n, sellAmountWei };
|
|
332
327
|
}
|
|
333
328
|
async function ensureSellerBalance({ tokenAddress, provider, seller, sellAmountWei, skipBalanceCheck }) {
|
|
334
329
|
const erc20 = new Contract(tokenAddress, ERC20_BALANCE_ABI, provider);
|
|
@@ -13,7 +13,6 @@ export interface FlapSwapSignConfig {
|
|
|
13
13
|
txType?: 0 | 2;
|
|
14
14
|
chainId?: number;
|
|
15
15
|
reserveGasETH?: number;
|
|
16
|
-
slippageBps?: number;
|
|
17
16
|
skipQuoteOnError?: boolean;
|
|
18
17
|
skipApprovalCheck?: boolean;
|
|
19
18
|
}
|
|
@@ -23,7 +22,6 @@ export interface FlapSwapConfig extends CommonBundleConfig {
|
|
|
23
22
|
customRpcUrl?: string;
|
|
24
23
|
bundleBlockOffset?: number;
|
|
25
24
|
reserveGasETH?: number;
|
|
26
|
-
slippageBps?: number;
|
|
27
25
|
skipQuoteOnError?: boolean;
|
|
28
26
|
waitForConfirmation?: boolean;
|
|
29
27
|
waitTimeoutMs?: number;
|