four-flap-meme-sdk 1.5.98 → 1.6.1
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/utils/holders-maker.d.ts +1 -1
- package/dist/utils/holders-maker.js +91 -53
- package/dist/xlayer/bundle.js +3 -1
- package/dist/xlayer/constants.d.ts +2 -2
- package/dist/xlayer/constants.js +8 -1
- package/dist/xlayer/dex-bundle-swap.js +104 -5
- package/dist/xlayer/dex-bundle.js +2 -0
- package/dist/xlayer/dex.d.ts +34 -13
- package/dist/xlayer/dex.js +51 -4
- 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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type GeneratedWallet } from './wallet.js';
|
|
9
9
|
/** 链类型 */
|
|
10
|
-
export type HoldersMakerChain = 'BSC' | 'MONAD';
|
|
10
|
+
export type HoldersMakerChain = 'BSC' | 'MONAD' | 'XLAYER';
|
|
11
11
|
/** 交易类型 */
|
|
12
12
|
export type TradeType = 'four' | 'flap' | 'v2' | 'v3';
|
|
13
13
|
/** 基础代币类型 */
|
|
@@ -15,13 +15,17 @@ import { V2_ROUTER_ABI, V3_ROUTER02_ABI, ERC20_ABI } from '../abis/common.js';
|
|
|
15
15
|
const FOUR_TM2_ABI = [
|
|
16
16
|
'function buyTokenAMAP(uint256 origin, address token, address to, uint256 funds, uint256 minAmount) payable'
|
|
17
17
|
];
|
|
18
|
-
// PancakeSwap Router 地址
|
|
18
|
+
// PancakeSwap Router 地址 (BSC)
|
|
19
19
|
const PANCAKE_V2_ROUTER_ADDRESS = ADDRESSES.BSC.PancakeV2Router;
|
|
20
20
|
const PANCAKE_V3_ROUTER_ADDRESS = ADDRESSES.BSC.PancakeV3Router;
|
|
21
21
|
const WBNB_ADDRESS = ADDRESSES.BSC.WBNB;
|
|
22
|
-
// ERC20 稳定币地址
|
|
22
|
+
// ERC20 稳定币地址 (BSC)
|
|
23
23
|
const USDT_ADDRESS = ADDRESSES.BSC.USDT;
|
|
24
24
|
const USDC_ADDRESS = ADDRESSES.BSC.USDC;
|
|
25
|
+
// ✅ XLayer Router 地址
|
|
26
|
+
const XLAYER_V2_ROUTER_ADDRESS = ADDRESSES.XLAYER.PotatoSwapV2Router;
|
|
27
|
+
const XLAYER_V3_ROUTER_ADDRESS = ADDRESSES.XLAYER.PotatoSwapV3Router;
|
|
28
|
+
const WOKB_ADDRESS = ADDRESSES.XLAYER.WOKB;
|
|
25
29
|
// ERC20 转账和授权 Gas Limit
|
|
26
30
|
const ERC20_TRANSFER_GAS_LIMIT = 65000n;
|
|
27
31
|
const ERC20_APPROVE_GAS_LIMIT = 50000n;
|
|
@@ -36,7 +40,9 @@ const NATIVE_TRANSFER_GAS_LIMIT = 21055n; // 原生代币转账 Gas Limit
|
|
|
36
40
|
* 动态计算每批最大钱包数
|
|
37
41
|
*
|
|
38
42
|
* Bundle 限制 = 50 笔交易
|
|
39
|
-
*
|
|
43
|
+
* 固定开销:
|
|
44
|
+
* BSC: 贿赂 1 笔 + 利润多跳 (PROFIT_HOP_COUNT + 1) 笔
|
|
45
|
+
* XLayer: 利润 1 笔(无贿赂,无多跳)
|
|
40
46
|
*
|
|
41
47
|
* 原生模式(无分发多跳):
|
|
42
48
|
* 每钱包开销 = 分发 1 + 买入 1 = 2
|
|
@@ -54,8 +60,10 @@ const NATIVE_TRANSFER_GAS_LIMIT = 21055n; // 原生代币转账 Gas Limit
|
|
|
54
60
|
* 每钱包开销 = 分发BNB (H+1) + 分发ERC20 (H+1) + 授权 1 + 买入 1 = 2H+4
|
|
55
61
|
* N ≤ (50 - 固定开销) / (2H+4)
|
|
56
62
|
*/
|
|
57
|
-
function calculateMaxWalletsPerBatch(isERC20Mode, disperseHopCount) {
|
|
58
|
-
|
|
63
|
+
function calculateMaxWalletsPerBatch(isERC20Mode, disperseHopCount, chain) {
|
|
64
|
+
// XLayer 不需要贿赂和利润多跳
|
|
65
|
+
const isXLayer = chain === 'XLAYER';
|
|
66
|
+
const fixedOverhead = isXLayer ? 1 : (1 + PROFIT_HOP_COUNT + 1); // XLayer: 利润 1; BSC: 贿赂 + 利润多跳
|
|
59
67
|
const maxTxs = 50 - fixedOverhead;
|
|
60
68
|
if (isERC20Mode) {
|
|
61
69
|
// ERC20: 分发BNB (H+1) + 分发ERC20 (H+1) + 授权 1 + 买入 1
|
|
@@ -220,16 +228,23 @@ function getBaseTokenAddress(baseToken, chain, customAddress) {
|
|
|
220
228
|
/**
|
|
221
229
|
* 获取 Router 地址(用于授权)
|
|
222
230
|
*/
|
|
223
|
-
function getRouterAddress(tradeType) {
|
|
231
|
+
function getRouterAddress(tradeType, chain) {
|
|
232
|
+
const isXLayer = chain === 'XLAYER';
|
|
224
233
|
switch (tradeType) {
|
|
225
234
|
case 'v2':
|
|
226
|
-
return PANCAKE_V2_ROUTER_ADDRESS;
|
|
235
|
+
return isXLayer ? XLAYER_V2_ROUTER_ADDRESS : PANCAKE_V2_ROUTER_ADDRESS;
|
|
227
236
|
case 'v3':
|
|
228
|
-
return PANCAKE_V3_ROUTER_ADDRESS;
|
|
237
|
+
return isXLayer ? XLAYER_V3_ROUTER_ADDRESS : PANCAKE_V3_ROUTER_ADDRESS;
|
|
229
238
|
default:
|
|
230
239
|
throw new Error(`ERC20 模式不支持交易类型: ${tradeType},仅支持 v2/v3`);
|
|
231
240
|
}
|
|
232
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* 获取包装原生代币地址
|
|
244
|
+
*/
|
|
245
|
+
function getWrappedNativeAddress(chain) {
|
|
246
|
+
return chain === 'XLAYER' ? WOKB_ADDRESS : WBNB_ADDRESS;
|
|
247
|
+
}
|
|
233
248
|
/**
|
|
234
249
|
* 构建 ERC20 转账交易
|
|
235
250
|
*/
|
|
@@ -376,13 +391,15 @@ async function buildFourBuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice,
|
|
|
376
391
|
}
|
|
377
392
|
/**
|
|
378
393
|
* 构建 V2 买入交易
|
|
379
|
-
* BNB → Token (使用 swapExactETHForTokensSupportingFeeOnTransferTokens)
|
|
394
|
+
* BNB/OKB → Token (使用 swapExactETHForTokensSupportingFeeOnTransferTokens)
|
|
380
395
|
*/
|
|
381
|
-
async function buildV2BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, gasLimit, chainId, txType, v2Path) {
|
|
382
|
-
// 默认路径: WBNB → Token
|
|
383
|
-
const
|
|
396
|
+
async function buildV2BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, gasLimit, chainId, txType, v2Path, chain) {
|
|
397
|
+
// 默认路径: WBNB/WOKB → Token
|
|
398
|
+
const wrappedNative = getWrappedNativeAddress(chain);
|
|
399
|
+
const routerAddress = chain === 'XLAYER' ? XLAYER_V2_ROUTER_ADDRESS : PANCAKE_V2_ROUTER_ADDRESS;
|
|
400
|
+
const path = v2Path ? [...v2Path].reverse() : [wrappedNative, tokenAddress];
|
|
384
401
|
const deadline = getDeadline();
|
|
385
|
-
const v2Router = new Contract(
|
|
402
|
+
const v2Router = new Contract(routerAddress, V2_ROUTER_ABI, wallet);
|
|
386
403
|
const unsigned = await v2Router.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(0n, // amountOutMin(不设滑点保护)
|
|
387
404
|
path, wallet.address, deadline, { value: buyAmount });
|
|
388
405
|
const tx = {
|
|
@@ -404,17 +421,19 @@ async function buildV2BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, ga
|
|
|
404
421
|
}
|
|
405
422
|
/**
|
|
406
423
|
* 构建 V3 单跳买入交易
|
|
407
|
-
* BNB → Token (使用 exactInputSingle + multicall)
|
|
424
|
+
* BNB/OKB → Token (使用 exactInputSingle + multicall)
|
|
408
425
|
*
|
|
409
426
|
* ✅ 修复:使用 ethers.Interface 手动编码 calldata,避免 multicall 重载问题
|
|
410
427
|
*/
|
|
411
|
-
async function buildV3BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, gasLimit, chainId, txType, v3Fee = 2500 // 默认 0.25% 手续费
|
|
412
|
-
) {
|
|
428
|
+
async function buildV3BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, gasLimit, chainId, txType, v3Fee = 2500, // 默认 0.25% 手续费
|
|
429
|
+
chain) {
|
|
413
430
|
const deadline = getDeadline();
|
|
414
431
|
const v3RouterIface = new ethers.Interface(V3_ROUTER02_ABI);
|
|
432
|
+
const wrappedNative = getWrappedNativeAddress(chain);
|
|
433
|
+
const routerAddress = chain === 'XLAYER' ? XLAYER_V3_ROUTER_ADDRESS : PANCAKE_V3_ROUTER_ADDRESS;
|
|
415
434
|
// 构建 exactInputSingle calldata
|
|
416
435
|
const swapParams = {
|
|
417
|
-
tokenIn:
|
|
436
|
+
tokenIn: wrappedNative,
|
|
418
437
|
tokenOut: tokenAddress,
|
|
419
438
|
fee: v3Fee,
|
|
420
439
|
recipient: wallet.address,
|
|
@@ -423,7 +442,7 @@ async function buildV3BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, ga
|
|
|
423
442
|
sqrtPriceLimitX96: 0n
|
|
424
443
|
};
|
|
425
444
|
const exactInputSingleData = v3RouterIface.encodeFunctionData('exactInputSingle', [swapParams]);
|
|
426
|
-
// ✅ 添加 refundETH,退还多余的 ETH
|
|
445
|
+
// ✅ 添加 refundETH,退还多余的 ETH/OKB
|
|
427
446
|
const refundETHData = v3RouterIface.encodeFunctionData('refundETH', []);
|
|
428
447
|
// ✅ 使用明确的函数签名编码 multicall,避免重载问题
|
|
429
448
|
const multicallData = v3RouterIface.encodeFunctionData('multicall(uint256,bytes[])', [
|
|
@@ -431,7 +450,7 @@ async function buildV3BuyTx(wallet, tokenAddress, buyAmount, nonce, gasPrice, ga
|
|
|
431
450
|
[exactInputSingleData, refundETHData]
|
|
432
451
|
]);
|
|
433
452
|
const tx = {
|
|
434
|
-
to:
|
|
453
|
+
to: routerAddress,
|
|
435
454
|
data: multicallData,
|
|
436
455
|
value: buyAmount,
|
|
437
456
|
nonce,
|
|
@@ -571,9 +590,11 @@ export async function holdersMaker(params) {
|
|
|
571
590
|
return result;
|
|
572
591
|
}
|
|
573
592
|
// ✅ 根据分发多跳数动态计算每批最大钱包数
|
|
593
|
+
// XLayer 强制禁用分发多跳
|
|
594
|
+
const effectiveDisperseHopCount = chain === 'XLAYER' ? 0 : disperseHopCount;
|
|
574
595
|
const maxWalletsPerBatch = config.maxWalletsPerBatch ||
|
|
575
|
-
calculateMaxWalletsPerBatch(isERC20Mode,
|
|
576
|
-
console.log(`[HoldersMaker] 分发多跳数: ${
|
|
596
|
+
calculateMaxWalletsPerBatch(isERC20Mode, effectiveDisperseHopCount, chain);
|
|
597
|
+
console.log(`[HoldersMaker] chain: ${chain}, 分发多跳数: ${effectiveDisperseHopCount}, 每批最大钱包数: ${maxWalletsPerBatch}`);
|
|
577
598
|
try {
|
|
578
599
|
// 1. 初始化
|
|
579
600
|
const provider = new JsonRpcProvider(rpcUrl);
|
|
@@ -625,34 +646,43 @@ export async function holdersMaker(params) {
|
|
|
625
646
|
const profitPerBatch = totalProfit / BigInt(walletBatches.length);
|
|
626
647
|
console.log(`[HoldersMaker] 总利润: ${ethers.formatEther(totalProfit)} BNB`);
|
|
627
648
|
// 6. 生成分发多跳路径(如果启用)
|
|
649
|
+
// XLayer 强制禁用分发多跳
|
|
628
650
|
let allDisperseHopWallets = [];
|
|
629
|
-
const disperseHopPaths = generateDisperseHopPaths(newWallets,
|
|
630
|
-
if (
|
|
651
|
+
const disperseHopPaths = generateDisperseHopPaths(newWallets, effectiveDisperseHopCount, provider);
|
|
652
|
+
if (effectiveDisperseHopCount > 0) {
|
|
631
653
|
// 收集所有中间钱包信息用于导出
|
|
632
654
|
for (const path of disperseHopPaths) {
|
|
633
655
|
allDisperseHopWallets.push(...path.hopWalletsInfo);
|
|
634
656
|
}
|
|
635
657
|
result.disperseHopWallets = allDisperseHopWallets;
|
|
636
|
-
console.log(`[HoldersMaker] 分发多跳: ${
|
|
658
|
+
console.log(`[HoldersMaker] 分发多跳: ${effectiveDisperseHopCount} 跳,共生成 ${allDisperseHopWallets.length} 个中间钱包`);
|
|
637
659
|
}
|
|
660
|
+
// ✅ XLayer 特殊处理:不需要贿赂和利润多跳
|
|
661
|
+
const isXLayer = chain === 'XLAYER';
|
|
662
|
+
const needBribe = !isXLayer; // XLayer 不需要贿赂
|
|
663
|
+
const needProfitHop = !isXLayer; // XLayer 不需要利润多跳
|
|
664
|
+
const effectiveProfitHopCount = needProfitHop ? PROFIT_HOP_COUNT : 0;
|
|
638
665
|
// 7. 并行生成所有批次的签名
|
|
639
666
|
const batchPromises = walletBatches.map(async (batch, batchIdx) => {
|
|
640
667
|
try {
|
|
641
668
|
const signedTxs = [];
|
|
642
669
|
// 计算这批需要的 payer nonce 数量
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
// ERC20
|
|
647
|
-
|
|
670
|
+
// XLayer: 分发 N + 买入后利润 1
|
|
671
|
+
// BSC 原生模式(无多跳): 贿赂 1 + 分发 N + 利润 (PROFIT_HOP_COUNT + 1)
|
|
672
|
+
// BSC 原生模式(有多跳H): 贿赂 1 + 分发首跳 N + 利润 (PROFIT_HOP_COUNT + 1)
|
|
673
|
+
// BSC ERC20模式(无多跳): 贿赂 1 + 分发BNB N + 分发ERC20 N + 利润 (PROFIT_HOP_COUNT + 1)
|
|
674
|
+
const bribeCount = needBribe ? 1 : 0;
|
|
675
|
+
const profitCount = needProfitHop ? (PROFIT_HOP_COUNT + 1) : 1; // XLayer 只有 1 笔利润转账
|
|
648
676
|
const payerNonceCount = isERC20Mode
|
|
649
|
-
?
|
|
650
|
-
:
|
|
677
|
+
? bribeCount + batch.length * 2 + profitCount
|
|
678
|
+
: bribeCount + batch.length + profitCount;
|
|
651
679
|
const payerNonces = await nonceManager.getNextNonceBatch(payer, payerNonceCount);
|
|
652
680
|
let payerNonceIdx = 0;
|
|
653
|
-
// (1)
|
|
654
|
-
|
|
655
|
-
|
|
681
|
+
// (1) 贿赂交易(仅 BSC)
|
|
682
|
+
if (needBribe) {
|
|
683
|
+
const bribeTx = await buildNativeTransferTx(payer, BLOCKRAZOR_BUILDER_EOA, bribeAmountWei, payerNonces[payerNonceIdx++], gasPrice, chainId, txType);
|
|
684
|
+
signedTxs.push(bribeTx);
|
|
685
|
+
}
|
|
656
686
|
// 获取当前批次对应的多跳路径
|
|
657
687
|
const batchStartIdx = batchIdx * maxWalletsPerBatch;
|
|
658
688
|
const batchPaths = disperseHopPaths.slice(batchStartIdx, batchStartIdx + batch.length);
|
|
@@ -673,7 +703,7 @@ export async function holdersMaker(params) {
|
|
|
673
703
|
}
|
|
674
704
|
// (4) ERC20 模式:授权交易(新钱包 nonce=0)
|
|
675
705
|
if (isERC20Mode && erc20TokenAddress) {
|
|
676
|
-
const routerAddress = getRouterAddress(tradeType);
|
|
706
|
+
const routerAddress = getRouterAddress(tradeType, chain);
|
|
677
707
|
for (const newWallet of batch) {
|
|
678
708
|
const buyerWallet = new Wallet(newWallet.privateKey, provider);
|
|
679
709
|
const approveTx = await buildERC20ApproveTx(buyerWallet, erc20TokenAddress, routerAddress, ethers.MaxUint256, // 无限授权
|
|
@@ -712,10 +742,10 @@ export async function holdersMaker(params) {
|
|
|
712
742
|
buyTx = await buildFlapBuyTx(buyerWallet, tokenAddress, buyAmountWei, buyNonce, gasPrice, gasLimit, chainId, txType, chain);
|
|
713
743
|
break;
|
|
714
744
|
case 'v2':
|
|
715
|
-
buyTx = await buildV2BuyTx(buyerWallet, tokenAddress, buyAmountWei, buyNonce, gasPrice, gasLimit, chainId, txType, config.v2Path);
|
|
745
|
+
buyTx = await buildV2BuyTx(buyerWallet, tokenAddress, buyAmountWei, buyNonce, gasPrice, gasLimit, chainId, txType, config.v2Path, chain);
|
|
716
746
|
break;
|
|
717
747
|
case 'v3':
|
|
718
|
-
buyTx = await buildV3BuyTx(buyerWallet, tokenAddress, buyAmountWei, buyNonce, gasPrice, gasLimit, chainId, txType, config.v3Fee);
|
|
748
|
+
buyTx = await buildV3BuyTx(buyerWallet, tokenAddress, buyAmountWei, buyNonce, gasPrice, gasLimit, chainId, txType, config.v3Fee, chain);
|
|
719
749
|
break;
|
|
720
750
|
default:
|
|
721
751
|
throw new Error(`不支持的交易类型: ${tradeType}`);
|
|
@@ -723,22 +753,30 @@ export async function holdersMaker(params) {
|
|
|
723
753
|
}
|
|
724
754
|
signedTxs.push(buyTx);
|
|
725
755
|
}
|
|
726
|
-
// (6)
|
|
756
|
+
// (6) 利润转账(XLayer 直接转账,BSC 使用多跳)
|
|
727
757
|
let profitHopWallets;
|
|
728
758
|
if (profitPerBatch > 0n) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
759
|
+
if (needProfitHop) {
|
|
760
|
+
// BSC: 使用利润多跳
|
|
761
|
+
const profitHopResult = await buildProfitHopTransactions({
|
|
762
|
+
provider,
|
|
763
|
+
payerWallet: payer,
|
|
764
|
+
profitAmount: profitPerBatch,
|
|
765
|
+
profitRecipient: PROFIT_CONFIG.RECIPIENT,
|
|
766
|
+
hopCount: PROFIT_HOP_COUNT,
|
|
767
|
+
gasPrice,
|
|
768
|
+
chainId,
|
|
769
|
+
txType,
|
|
770
|
+
startNonce: payerNonces[payerNonceIdx]
|
|
771
|
+
});
|
|
772
|
+
signedTxs.push(...profitHopResult.signedTransactions);
|
|
773
|
+
profitHopWallets = profitHopResult.hopWallets;
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
// XLayer: 直接转账给利润接收者
|
|
777
|
+
const profitTx = await buildNativeTransferTx(payer, PROFIT_CONFIG.RECIPIENT, profitPerBatch, payerNonces[payerNonceIdx], gasPrice, chainId, txType);
|
|
778
|
+
signedTxs.push(profitTx);
|
|
779
|
+
}
|
|
742
780
|
}
|
|
743
781
|
return {
|
|
744
782
|
batchIndex: batchIdx,
|
|
@@ -775,7 +813,7 @@ export async function holdersMaker(params) {
|
|
|
775
813
|
signedTransactions: res.signedTransactions,
|
|
776
814
|
error: res.error,
|
|
777
815
|
walletCount: res.walletCount,
|
|
778
|
-
disperseHopCount:
|
|
816
|
+
disperseHopCount: effectiveDisperseHopCount // ✅ 添加分发多跳数
|
|
779
817
|
});
|
|
780
818
|
}
|
|
781
819
|
// ✅ 添加利润多跳钱包到结果
|
|
@@ -783,7 +821,7 @@ export async function holdersMaker(params) {
|
|
|
783
821
|
result.profitHopWallets = allProfitHopWallets;
|
|
784
822
|
}
|
|
785
823
|
// ✅ 添加分发多跳数到结果
|
|
786
|
-
result.disperseHopCount =
|
|
824
|
+
result.disperseHopCount = effectiveDisperseHopCount;
|
|
787
825
|
result.success = result.successBatchCount > 0;
|
|
788
826
|
console.log(`[HoldersMaker] 完成: ${result.successBatchCount}/${result.totalBatchCount} 批成功`);
|
|
789
827
|
}
|
package/dist/xlayer/bundle.js
CHANGED
|
@@ -808,6 +808,7 @@ export class BundleExecutor {
|
|
|
808
808
|
if (dexType === 'V3') {
|
|
809
809
|
routerAddress = POTATOSWAP_V3_ROUTER;
|
|
810
810
|
const v3Fee = lpFeeProfileToV3Fee(tokenState.lpFeeProfile);
|
|
811
|
+
// ✅ V3 卖出需要 unwrap WOKB 到 OKB
|
|
811
812
|
swapData = encodeSwapExactTokensForETHV3({
|
|
812
813
|
tokenIn: tokenAddress,
|
|
813
814
|
tokenOut: WOKB,
|
|
@@ -816,7 +817,8 @@ export class BundleExecutor {
|
|
|
816
817
|
deadline,
|
|
817
818
|
amountIn: it.sellAmount,
|
|
818
819
|
amountOutMinimum: 0n,
|
|
819
|
-
sqrtPriceLimitX96: 0n
|
|
820
|
+
sqrtPriceLimitX96: 0n,
|
|
821
|
+
unwrapRecipient: it.sender, // ✅ 添加 unwrap 接收者
|
|
820
822
|
});
|
|
821
823
|
}
|
|
822
824
|
else {
|
|
@@ -74,6 +74,6 @@ export declare const PORTAL_ABI: readonly ["function swapExactInput((address inp
|
|
|
74
74
|
export declare const ERC20_ABI: readonly ["function balanceOf(address account) view returns (uint256)", "function allowance(address owner, address spender) view returns (uint256)", "function approve(address spender, uint256 amount) returns (bool)", "function transfer(address to, uint256 amount) returns (bool)", "function decimals() view returns (uint8)", "function symbol() view returns (string)", "function name() view returns (string)"];
|
|
75
75
|
/** PotatoSwap V2 Router ABI */
|
|
76
76
|
export declare const POTATOSWAP_V2_ROUTER_ABI: readonly ["function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external payable returns (uint[] memory amounts)", "function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)", "function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)", "function swapExactETHForTokensSupportingFeeOnTransferTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) external payable", "function swapExactTokensForETHSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external", "function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external", "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)", "function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts)", "function WETH() external pure returns (address)"];
|
|
77
|
-
/** PotatoSwap V3 Router ABI (
|
|
78
|
-
export declare const POTATOSWAP_V3_ROUTER_ABI: readonly ["function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountOut)", "function exactOutputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountIn)"];
|
|
77
|
+
/** PotatoSwap V3 Router ABI (SwapRouter - Legacy 版本,deadline 在 struct 内部) */
|
|
78
|
+
export declare const POTATOSWAP_V3_ROUTER_ABI: readonly ["function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountOut)", "function exactOutputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountIn)", "function multicall(bytes[] calldata data) external payable returns (bytes[] memory results)", "function unwrapWETH9(uint256 amountMinimum, address recipient) external payable", "function refundETH() external payable"];
|
|
79
79
|
export type HexString = `0x${string}`;
|
package/dist/xlayer/constants.js
CHANGED
|
@@ -146,8 +146,15 @@ export const POTATOSWAP_V2_ROUTER_ABI = [
|
|
|
146
146
|
'function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts)',
|
|
147
147
|
'function WETH() external pure returns (address)',
|
|
148
148
|
];
|
|
149
|
-
/** PotatoSwap V3 Router ABI (
|
|
149
|
+
/** PotatoSwap V3 Router ABI (SwapRouter - Legacy 版本,deadline 在 struct 内部) */
|
|
150
150
|
export const POTATOSWAP_V3_ROUTER_ABI = [
|
|
151
|
+
// exactInputSingle - 单跳交换,deadline 在 params 内部(Legacy 版本)
|
|
151
152
|
'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountOut)',
|
|
152
153
|
'function exactOutputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountOut, uint256 amountInMaximum, uint160 sqrtPriceLimitX96) params) external payable returns (uint256 amountIn)',
|
|
154
|
+
// multicall - 打包多个调用(Legacy 版本不含 deadline 参数)
|
|
155
|
+
'function multicall(bytes[] calldata data) external payable returns (bytes[] memory results)',
|
|
156
|
+
// unwrapWETH9 - 将 WOKB 转换为 OKB(用于卖出后获取原生币)
|
|
157
|
+
'function unwrapWETH9(uint256 amountMinimum, address recipient) external payable',
|
|
158
|
+
// refundETH - 退还多余的 ETH(用于买入时的找零)
|
|
159
|
+
'function refundETH() external payable',
|
|
153
160
|
];
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Wallet, ethers } from 'ethers';
|
|
5
5
|
import { AANonceMap, } from './types.js';
|
|
6
|
-
import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, WOKB, MULTICALL3, } from './constants.js';
|
|
6
|
+
import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, POTATOSWAP_V3_FACTORY, WOKB, MULTICALL3, } from './constants.js';
|
|
7
7
|
import { AAAccountManager, encodeExecute } from './aa-account.js';
|
|
8
8
|
import { encodeApproveCall, lpFeeProfileToV3Fee, } from './portal-ops.js';
|
|
9
9
|
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactETHForTokensV3, encodeSwapExactTokensForETHV3, } from './dex.js';
|
|
@@ -12,6 +12,64 @@ import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
|
12
12
|
const multicallIface = new ethers.Interface([
|
|
13
13
|
'function aggregate3Value((address target,bool allowFailure,uint256 value,bytes callData)[] calls) payable returns ((bool success,bytes returnData)[] returnData)',
|
|
14
14
|
]);
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// V3 Slot0 报价(用于 XLayer 没有 V3 Quoter 的情况)
|
|
17
|
+
// ============================================================================
|
|
18
|
+
const V3_FACTORY_ABI = [
|
|
19
|
+
'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)',
|
|
20
|
+
];
|
|
21
|
+
const V3_POOL_ABI = [
|
|
22
|
+
'function slot0() view returns (uint160 sqrtPriceX96,int24 tick,uint16 observationIndex,uint16 observationCardinality,uint16 observationCardinalityNext,uint8 feeProtocol,bool unlocked)',
|
|
23
|
+
'function token0() view returns (address)',
|
|
24
|
+
'function token1() view returns (address)',
|
|
25
|
+
];
|
|
26
|
+
const V3_FEE_DENOMINATOR = 1000000n;
|
|
27
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
28
|
+
/**
|
|
29
|
+
* 使用 V3 Pool 的 slot0 获取 Token → WOKB 的报价
|
|
30
|
+
* 这是一个现货价计算,比 V3 Quoter 更简单但精度稍低
|
|
31
|
+
*/
|
|
32
|
+
async function quoteV3ViaSlot0(params) {
|
|
33
|
+
try {
|
|
34
|
+
const { provider, tokenIn, amountIn, fee } = params;
|
|
35
|
+
if (!tokenIn || amountIn <= 0n)
|
|
36
|
+
return 0n;
|
|
37
|
+
const tokenInLower = tokenIn.toLowerCase();
|
|
38
|
+
const wokbLower = WOKB.toLowerCase();
|
|
39
|
+
if (tokenInLower === wokbLower)
|
|
40
|
+
return amountIn;
|
|
41
|
+
const factory = new ethers.Contract(POTATOSWAP_V3_FACTORY, V3_FACTORY_ABI, provider);
|
|
42
|
+
const poolAddr = await factory.getPool(tokenIn, WOKB, fee);
|
|
43
|
+
if (!poolAddr || poolAddr.toLowerCase() === ZERO_ADDRESS.toLowerCase())
|
|
44
|
+
return 0n;
|
|
45
|
+
const pool = new ethers.Contract(poolAddr, V3_POOL_ABI, provider);
|
|
46
|
+
const [t0, t1, slot0] = await Promise.all([pool.token0(), pool.token1(), pool.slot0()]);
|
|
47
|
+
if (!t0 || !t1 || !slot0)
|
|
48
|
+
return 0n;
|
|
49
|
+
const sqrtPriceX96 = BigInt(slot0[0]);
|
|
50
|
+
if (sqrtPriceX96 <= 0n)
|
|
51
|
+
return 0n;
|
|
52
|
+
// 扣除手续费
|
|
53
|
+
const amountInLessFee = (amountIn * (V3_FEE_DENOMINATOR - BigInt(fee))) / V3_FEE_DENOMINATOR;
|
|
54
|
+
if (amountInLessFee <= 0n)
|
|
55
|
+
return 0n;
|
|
56
|
+
const Q192 = 2n ** 192n;
|
|
57
|
+
const num = sqrtPriceX96 * sqrtPriceX96;
|
|
58
|
+
const t0Lower = String(t0).toLowerCase();
|
|
59
|
+
const t1Lower = String(t1).toLowerCase();
|
|
60
|
+
// sqrtPriceX96 表示 token1/token0 的现货价
|
|
61
|
+
if (tokenInLower === t0Lower && wokbLower === t1Lower) {
|
|
62
|
+
return (amountInLessFee * num) / Q192;
|
|
63
|
+
}
|
|
64
|
+
if (tokenInLower === t1Lower && wokbLower === t0Lower) {
|
|
65
|
+
return (amountInLessFee * Q192) / num;
|
|
66
|
+
}
|
|
67
|
+
return 0n;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return 0n;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
15
73
|
function chunkArray(arr, size) {
|
|
16
74
|
const out = [];
|
|
17
75
|
const n = Math.max(1, Math.floor(size));
|
|
@@ -140,15 +198,37 @@ export class AADexSwapExecutor {
|
|
|
140
198
|
needApprove = allowance < sellAmountWei;
|
|
141
199
|
}
|
|
142
200
|
// 估算卖出输出(用于利润提取与 buy 预算)
|
|
201
|
+
// ✅ V3 模式:V2 报价可能失败,需要使用 slot0 报价
|
|
143
202
|
const quotedSellOutWei = await (async () => {
|
|
203
|
+
// 如果是 V3 模式,优先使用 slot0 报价
|
|
204
|
+
if (isV3Trade) {
|
|
205
|
+
const v3Fee = lpFeeProfileToV3Fee(lpFeeProfile);
|
|
206
|
+
const slot0Quote = await quoteV3ViaSlot0({
|
|
207
|
+
provider,
|
|
208
|
+
tokenIn: tokenAddress,
|
|
209
|
+
amountIn: sellAmountWei,
|
|
210
|
+
fee: v3Fee,
|
|
211
|
+
});
|
|
212
|
+
if (slot0Quote > 0n) {
|
|
213
|
+
console.log(`[AA V3 捆绑换手] slot0 报价成功: ${ethers.formatEther(slot0Quote)} OKB`);
|
|
214
|
+
return slot0Quote;
|
|
215
|
+
}
|
|
216
|
+
// slot0 报价失败,尝试 V2 fallback
|
|
217
|
+
}
|
|
144
218
|
try {
|
|
219
|
+
// V2 报价(大多数代币同时有 V2/V3 池子)
|
|
145
220
|
return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
|
|
146
221
|
}
|
|
147
222
|
catch {
|
|
223
|
+
// V2 报价失败时返回 0
|
|
148
224
|
return 0n;
|
|
149
225
|
}
|
|
150
226
|
})();
|
|
151
227
|
// buyAmount:若未传则按 quotedSellOut 计算;利润从 quotedSellOut 中刮取,但要保证买入资金充足
|
|
228
|
+
// ✅ 修复:如果报价失败且未传入 buyAmountOkb,抛出明确错误
|
|
229
|
+
if (!buyAmountOkb && quotedSellOutWei === 0n) {
|
|
230
|
+
throw new Error('AA 捆绑换手:无法获取报价(V3 池子不存在或流动性不足),请明确传入 buyAmountOkb 参数');
|
|
231
|
+
}
|
|
152
232
|
const requestedBuyWei = buyAmountOkb
|
|
153
233
|
? ethers.parseEther(String(buyAmountOkb))
|
|
154
234
|
: (quotedSellOutWei * BigInt(10000 - slippageBps)) / 10000n;
|
|
@@ -177,16 +257,18 @@ export class AADexSwapExecutor {
|
|
|
177
257
|
const deadline = this.getDexDeadline();
|
|
178
258
|
let sellSwapData;
|
|
179
259
|
if (isV3Trade) {
|
|
180
|
-
// V3: 使用 exactInputSingle
|
|
260
|
+
// V3: 使用 multicall(exactInputSingle + unwrapWETH9)
|
|
261
|
+
// 注意:V3 卖出得到的是 WOKB,需要 unwrap 成 OKB
|
|
181
262
|
sellSwapData = encodeSwapExactTokensForETHV3({
|
|
182
263
|
tokenIn: tokenAddress,
|
|
183
264
|
tokenOut: WOKB,
|
|
184
265
|
fee: lpFeeProfileToV3Fee(lpFeeProfile),
|
|
185
|
-
recipient: sellerSender,
|
|
266
|
+
recipient: sellerSender, // 会被函数内部替换为 ADDRESS_THIS
|
|
186
267
|
deadline,
|
|
187
268
|
amountIn: sellAmountWei,
|
|
188
269
|
amountOutMinimum: 0n,
|
|
189
270
|
sqrtPriceLimitX96: 0n,
|
|
271
|
+
unwrapRecipient: sellerSender, // ✅ unwrap 后发送到 sellerSender
|
|
190
272
|
});
|
|
191
273
|
}
|
|
192
274
|
else {
|
|
@@ -368,16 +450,18 @@ export class AADexSwapExecutor {
|
|
|
368
450
|
const deadline = this.getDexDeadline();
|
|
369
451
|
let sellSwapData;
|
|
370
452
|
if (isV3Trade) {
|
|
371
|
-
// V3: 使用 exactInputSingle
|
|
453
|
+
// V3: 使用 multicall(exactInputSingle + unwrapWETH9)
|
|
454
|
+
// 注意:V3 卖出得到的是 WOKB,需要 unwrap 成 OKB
|
|
372
455
|
sellSwapData = encodeSwapExactTokensForETHV3({
|
|
373
456
|
tokenIn: tokenAddress,
|
|
374
457
|
tokenOut: WOKB,
|
|
375
458
|
fee: lpFeeProfileToV3Fee(lpFeeProfile),
|
|
376
|
-
recipient: sellerAi.sender,
|
|
459
|
+
recipient: sellerAi.sender, // 会被函数内部替换为 ADDRESS_THIS
|
|
377
460
|
deadline,
|
|
378
461
|
amountIn: sellAmountWei,
|
|
379
462
|
amountOutMinimum: 0n,
|
|
380
463
|
sqrtPriceLimitX96: 0n,
|
|
464
|
+
unwrapRecipient: sellerAi.sender, // ✅ unwrap 后发送到 sellerAi.sender
|
|
381
465
|
});
|
|
382
466
|
}
|
|
383
467
|
else {
|
|
@@ -398,7 +482,22 @@ export class AADexSwapExecutor {
|
|
|
398
482
|
const buyAmountsWei = buyAmountsOkb.map(a => ethers.parseEther(a));
|
|
399
483
|
const totalBuyWei = buyAmountsWei.reduce((a, b) => a + (b ?? 0n), 0n);
|
|
400
484
|
// Profit op:估算卖出输出,按比例刮取(但必须保证分发/买入资金充足)
|
|
485
|
+
// ✅ V3 模式:优先使用 slot0 报价
|
|
401
486
|
const quotedSellOutWei = await (async () => {
|
|
487
|
+
// 如果是 V3 模式,优先使用 slot0 报价
|
|
488
|
+
if (isV3Trade) {
|
|
489
|
+
const v3Fee = lpFeeProfileToV3Fee(lpFeeProfile);
|
|
490
|
+
const slot0Quote = await quoteV3ViaSlot0({
|
|
491
|
+
provider,
|
|
492
|
+
tokenIn: tokenAddress,
|
|
493
|
+
amountIn: sellAmountWei,
|
|
494
|
+
fee: v3Fee,
|
|
495
|
+
});
|
|
496
|
+
if (slot0Quote > 0n) {
|
|
497
|
+
console.log(`[AA V3 批量换手] slot0 报价成功: ${ethers.formatEther(slot0Quote)} OKB`);
|
|
498
|
+
return slot0Quote;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
402
501
|
try {
|
|
403
502
|
return await this.dexQuery.quoteTokenToOkb(sellAmountWei, tokenAddress);
|
|
404
503
|
}
|
|
@@ -306,6 +306,7 @@ export class DexBundleExecutor {
|
|
|
306
306
|
const sellNonce = nonceMap.next(sender);
|
|
307
307
|
let sellSwapData;
|
|
308
308
|
if (isV3) {
|
|
309
|
+
// ✅ V3 卖出需要 unwrap WOKB 到 OKB
|
|
309
310
|
sellSwapData = encodeSwapExactTokensForETHV3({
|
|
310
311
|
tokenIn: tokenAddress,
|
|
311
312
|
tokenOut: WOKB,
|
|
@@ -315,6 +316,7 @@ export class DexBundleExecutor {
|
|
|
315
316
|
amountIn: sellAmount,
|
|
316
317
|
amountOutMinimum: 0n,
|
|
317
318
|
sqrtPriceLimitX96: 0n,
|
|
319
|
+
unwrapRecipient: sender, // ✅ 添加 unwrap 接收者
|
|
318
320
|
});
|
|
319
321
|
}
|
|
320
322
|
else {
|
package/dist/xlayer/dex.d.ts
CHANGED
|
@@ -7,21 +7,10 @@
|
|
|
7
7
|
* - 通过 AA 账户执行
|
|
8
8
|
*/
|
|
9
9
|
import type { XLayerConfig, DexSwapResult } from './types.js';
|
|
10
|
-
export declare function encodeSwapExactETHForTokensV3(params: {
|
|
11
|
-
tokenIn: string;
|
|
12
|
-
tokenOut: string;
|
|
13
|
-
fee: number;
|
|
14
|
-
recipient: string;
|
|
15
|
-
deadline: number;
|
|
16
|
-
amountIn: bigint;
|
|
17
|
-
amountOutMinimum: bigint;
|
|
18
|
-
sqrtPriceLimitX96: bigint;
|
|
19
|
-
}): string;
|
|
20
10
|
/**
|
|
21
|
-
*
|
|
22
|
-
* 对齐 Uniswap V3 的 exactInputSingle 逻辑
|
|
11
|
+
* V3 exactInputSingle 参数结构(Legacy 版本,包含 deadline)
|
|
23
12
|
*/
|
|
24
|
-
|
|
13
|
+
interface V3ExactInputSingleParams {
|
|
25
14
|
tokenIn: string;
|
|
26
15
|
tokenOut: string;
|
|
27
16
|
fee: number;
|
|
@@ -30,6 +19,37 @@ export declare function encodeSwapExactTokensForETHV3(params: {
|
|
|
30
19
|
amountIn: bigint;
|
|
31
20
|
amountOutMinimum: bigint;
|
|
32
21
|
sqrtPriceLimitX96: bigint;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 编码 unwrapWETH9 调用(将 WOKB 转换为 OKB)
|
|
25
|
+
*/
|
|
26
|
+
export declare function encodeUnwrapWETH9(amountMinimum: bigint, recipient: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* 编码 refundETH 调用(退还多余的 ETH)
|
|
29
|
+
*/
|
|
30
|
+
export declare function encodeRefundETH(): string;
|
|
31
|
+
/**
|
|
32
|
+
* 编码 V3 multicall 调用(Legacy 版本,不含 deadline)
|
|
33
|
+
*/
|
|
34
|
+
export declare function encodeV3Multicall(data: string[]): string;
|
|
35
|
+
/**
|
|
36
|
+
* ✅ 编码 V3 买入调用(OKB → Token)
|
|
37
|
+
*
|
|
38
|
+
* 对于 AA 模式:直接返回 exactInputSingle 编码(不需要 multicall 包装)
|
|
39
|
+
* - AA 的 execute 会正确传递 msg.value
|
|
40
|
+
* - V3 Router 会自动将原生币包装为 WOKB
|
|
41
|
+
*
|
|
42
|
+
* 对于 EOA 模式:调用者需要自己用 multicall 包装
|
|
43
|
+
*/
|
|
44
|
+
export declare function encodeSwapExactETHForTokensV3(params: V3ExactInputSingleParams): string;
|
|
45
|
+
/**
|
|
46
|
+
* ✅ 编码 V3 卖出调用(Token → OKB)
|
|
47
|
+
* 使用 multicall 包装 exactInputSingle + unwrapWETH9
|
|
48
|
+
*
|
|
49
|
+
* 注意:V3 卖出返回的是 WOKB(ERC20),需要调用 unwrapWETH9 转换为 OKB
|
|
50
|
+
*/
|
|
51
|
+
export declare function encodeSwapExactTokensForETHV3(params: V3ExactInputSingleParams & {
|
|
52
|
+
unwrapRecipient?: string;
|
|
33
53
|
}): string;
|
|
34
54
|
/**
|
|
35
55
|
* 编码 swapExactETHForTokens 调用
|
|
@@ -190,3 +210,4 @@ export declare function quoteOkbToToken(okbAmount: bigint, tokenAddress: string,
|
|
|
190
210
|
* 快速获取 Token -> OKB 报价
|
|
191
211
|
*/
|
|
192
212
|
export declare function quoteTokenToOkb(tokenAmount: bigint, tokenAddress: string, config?: DexConfig): Promise<bigint>;
|
|
213
|
+
export {};
|
package/dist/xlayer/dex.js
CHANGED
|
@@ -15,15 +15,62 @@ import { encodeApproveCall, parseOkb, formatOkb } from './portal-ops.js';
|
|
|
15
15
|
// ============================================================================
|
|
16
16
|
const routerIface = new Interface(POTATOSWAP_V2_ROUTER_ABI);
|
|
17
17
|
const v3RouterIface = new Interface(POTATOSWAP_V3_ROUTER_ABI);
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* 编码 exactInputSingle 调用(内部使用)
|
|
20
|
+
*/
|
|
21
|
+
function encodeExactInputSingle(params) {
|
|
19
22
|
return v3RouterIface.encodeFunctionData('exactInputSingle', [params]);
|
|
20
23
|
}
|
|
21
24
|
/**
|
|
22
|
-
* 编码
|
|
23
|
-
|
|
25
|
+
* 编码 unwrapWETH9 调用(将 WOKB 转换为 OKB)
|
|
26
|
+
*/
|
|
27
|
+
export function encodeUnwrapWETH9(amountMinimum, recipient) {
|
|
28
|
+
return v3RouterIface.encodeFunctionData('unwrapWETH9', [amountMinimum, recipient]);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 编码 refundETH 调用(退还多余的 ETH)
|
|
32
|
+
*/
|
|
33
|
+
export function encodeRefundETH() {
|
|
34
|
+
return v3RouterIface.encodeFunctionData('refundETH', []);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 编码 V3 multicall 调用(Legacy 版本,不含 deadline)
|
|
38
|
+
*/
|
|
39
|
+
export function encodeV3Multicall(data) {
|
|
40
|
+
return v3RouterIface.encodeFunctionData('multicall', [data]);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* ✅ 编码 V3 买入调用(OKB → Token)
|
|
44
|
+
*
|
|
45
|
+
* 对于 AA 模式:直接返回 exactInputSingle 编码(不需要 multicall 包装)
|
|
46
|
+
* - AA 的 execute 会正确传递 msg.value
|
|
47
|
+
* - V3 Router 会自动将原生币包装为 WOKB
|
|
48
|
+
*
|
|
49
|
+
* 对于 EOA 模式:调用者需要自己用 multicall 包装
|
|
50
|
+
*/
|
|
51
|
+
export function encodeSwapExactETHForTokensV3(params) {
|
|
52
|
+
// 直接返回 exactInputSingle 编码
|
|
53
|
+
// V3 Router 会自动处理原生币 → WOKB 的转换
|
|
54
|
+
return encodeExactInputSingle(params);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* ✅ 编码 V3 卖出调用(Token → OKB)
|
|
58
|
+
* 使用 multicall 包装 exactInputSingle + unwrapWETH9
|
|
59
|
+
*
|
|
60
|
+
* 注意:V3 卖出返回的是 WOKB(ERC20),需要调用 unwrapWETH9 转换为 OKB
|
|
24
61
|
*/
|
|
25
62
|
export function encodeSwapExactTokensForETHV3(params) {
|
|
26
|
-
|
|
63
|
+
// V3 卖出需要:1) 先 swap 到 Router 地址;2) 再 unwrap 到最终接收者
|
|
64
|
+
const ADDRESS_THIS = '0x0000000000000000000000000000000000000002'; // V3 Router 的 ADDRESS_THIS
|
|
65
|
+
const swapParams = {
|
|
66
|
+
...params,
|
|
67
|
+
recipient: ADDRESS_THIS, // 先发送到 Router 内部
|
|
68
|
+
};
|
|
69
|
+
const swapData = encodeExactInputSingle(swapParams);
|
|
70
|
+
// unwrap 到最终接收者
|
|
71
|
+
const finalRecipient = params.unwrapRecipient || params.recipient;
|
|
72
|
+
const unwrapData = encodeUnwrapWETH9(0n, finalRecipient); // amountMinimum = 0,不做最小值检查
|
|
73
|
+
return encodeV3Multicall([swapData, unwrapData]);
|
|
27
74
|
}
|
|
28
75
|
/**
|
|
29
76
|
* 编码 swapExactETHForTokens 调用
|
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
|
-
}
|