four-flap-meme-sdk 1.6.35 → 1.6.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/xlayer/aa-account.d.ts +6 -0
- package/dist/xlayer/aa-account.js +34 -0
- package/dist/xlayer/dex-bundle-swap.js +81 -39
- package/dist/xlayer/hop-wallet-manager.d.ts +143 -0
- package/dist/xlayer/hop-wallet-manager.js +526 -0
- package/dist/xlayer/index.d.ts +1 -0
- package/dist/xlayer/index.js +4 -0
- package/dist/xlayer/portal-bundle-swap.js +70 -27
- package/dist/xlayer/types.d.ts +18 -0
- 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
|
@@ -89,6 +89,12 @@ export declare class AAAccountManager {
|
|
|
89
89
|
* 获取完整的 AA 账户信息
|
|
90
90
|
*/
|
|
91
91
|
getAccountInfo(ownerAddress: string, salt?: bigint): Promise<AAAccount>;
|
|
92
|
+
/**
|
|
93
|
+
* 批量获取多个 AA sender 的 nonce
|
|
94
|
+
* @param senderAddresses AA sender 地址列表
|
|
95
|
+
* @returns 对应的 nonce 数组
|
|
96
|
+
*/
|
|
97
|
+
batchGetNonces(senderAddresses: string[]): Promise<bigint[]>;
|
|
92
98
|
/**
|
|
93
99
|
* 生成 initCode(用于在 UserOp 中部署账户)
|
|
94
100
|
*/
|
|
@@ -290,6 +290,40 @@ export class AAAccountManager {
|
|
|
290
290
|
nonce,
|
|
291
291
|
};
|
|
292
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* 批量获取多个 AA sender 的 nonce
|
|
295
|
+
* @param senderAddresses AA sender 地址列表
|
|
296
|
+
* @returns 对应的 nonce 数组
|
|
297
|
+
*/
|
|
298
|
+
async batchGetNonces(senderAddresses) {
|
|
299
|
+
if (senderAddresses.length === 0)
|
|
300
|
+
return [];
|
|
301
|
+
await this.syncEntryPointIfNeeded();
|
|
302
|
+
const epIface = new Interface(ENTRYPOINT_ABI);
|
|
303
|
+
const nonceCalls = senderAddresses.map((sender) => ({
|
|
304
|
+
target: this.entryPointAddress,
|
|
305
|
+
allowFailure: true,
|
|
306
|
+
callData: epIface.encodeFunctionData('getNonce', [sender, 0]),
|
|
307
|
+
}));
|
|
308
|
+
const nonces = new Array(senderAddresses.length).fill(0n);
|
|
309
|
+
const BATCH = 350;
|
|
310
|
+
for (let cursor = 0; cursor < nonceCalls.length; cursor += BATCH) {
|
|
311
|
+
const sliceCalls = nonceCalls.slice(cursor, cursor + BATCH);
|
|
312
|
+
const res = await this.multicallAggregate3({ calls: sliceCalls });
|
|
313
|
+
for (let i = 0; i < res.length; i++) {
|
|
314
|
+
const r = res[i];
|
|
315
|
+
const idx = cursor + i;
|
|
316
|
+
if (!r?.success || !r.returnData || r.returnData === '0x')
|
|
317
|
+
continue;
|
|
318
|
+
try {
|
|
319
|
+
const decoded = epIface.decodeFunctionResult('getNonce', r.returnData);
|
|
320
|
+
nonces[idx] = BigInt(decoded?.[0] ?? 0n);
|
|
321
|
+
}
|
|
322
|
+
catch { /* ignore */ }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return nonces;
|
|
326
|
+
}
|
|
293
327
|
/**
|
|
294
328
|
* 生成 initCode(用于在 UserOp 中部署账户)
|
|
295
329
|
*/
|
|
@@ -426,6 +426,7 @@ export class AADexSwapExecutor {
|
|
|
426
426
|
async bundleBatchSwapSign(params) {
|
|
427
427
|
const { dexKey, routerAddress: routerAddressIn, tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, capitalMode = true, // ✅ 默认资金利用率模式(卖出→分发→买入)
|
|
428
428
|
disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, tradeType, lpFeeProfile = 0, quoteToken, quoteTokenDecimals = 6, // XLayer USDT/USDC/USDT0 都是 6 位精度
|
|
429
|
+
prefundedHopWallets, // ✅ 预充值多跳:已预充值的 hop 钱包列表
|
|
429
430
|
} = params;
|
|
430
431
|
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
431
432
|
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
@@ -581,17 +582,23 @@ export class AADexSwapExecutor {
|
|
|
581
582
|
extractProfit,
|
|
582
583
|
profitWei: ethers.formatEther(profitWei),
|
|
583
584
|
});
|
|
584
|
-
// ✅
|
|
585
|
+
// ✅ 多跳模式判断:
|
|
585
586
|
// ERC-4337 的 handleOps 执行流程是"先验证所有 UserOps,再执行所有 UserOps"
|
|
586
587
|
// hop 钱包是临时生成的(余额=0),在 validation 阶段无法支付 prefund,会导致 AA21 错误
|
|
587
588
|
//
|
|
588
|
-
//
|
|
589
|
-
//
|
|
589
|
+
// 多跳可用的条件:
|
|
590
|
+
// 1. 配置了 Paymaster(prefund = 0n,hop 钱包不需要余额)
|
|
591
|
+
// 2. 传入了预充值好的 hop 钱包(hop 钱包已有余额支付 prefund)
|
|
590
592
|
const hasPaymaster = this.aaManager.hasPaymaster();
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
593
|
+
const hasPrefundedHops = Array.isArray(prefundedHopWallets) && prefundedHopWallets.length > 0;
|
|
594
|
+
const canDoMultiHop = hasPaymaster || hasPrefundedHops;
|
|
595
|
+
const effectiveHopCount = canDoMultiHop ? hopCount : 0;
|
|
596
|
+
if (hopCount > 0 && !canDoMultiHop) {
|
|
597
|
+
console.warn('[AA DEX 批量换手] ⚠️ 无法执行多跳(hop 钱包无法支付 prefund),已自动降级为直接分发模式');
|
|
598
|
+
console.warn('[AA DEX 批量换手] 💡 提示:使用 HopWalletManager.prefundHopWallets() 预充值 hop 钱包,或配置 Paymaster');
|
|
599
|
+
}
|
|
600
|
+
else if (hopCount > 0 && hasPrefundedHops) {
|
|
601
|
+
console.log('[AA DEX 批量换手] ✅ 预充值多跳模式已启用 (hopCount:', hopCount, ', prefundedHops:', prefundedHopWallets.length, ')');
|
|
595
602
|
}
|
|
596
603
|
else if (hopCount > 0 && hasPaymaster) {
|
|
597
604
|
console.log('[AA DEX 批量换手] ✅ Paymaster 模式已启用,支持多跳换手 (hopCount:', hopCount, ')');
|
|
@@ -655,9 +662,13 @@ export class AADexSwapExecutor {
|
|
|
655
662
|
}
|
|
656
663
|
}
|
|
657
664
|
else {
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
|
|
665
|
+
// ✅ 多跳模式:使用预充值的 hop 钱包或 Paymaster 模式
|
|
666
|
+
console.log('[AA DEX 批量换手] 进入多跳模式 (effectiveHopCount > 0):', {
|
|
667
|
+
effectiveHopCount,
|
|
668
|
+
buyerCount: buyerSenders.length,
|
|
669
|
+
usePrefundedHops: hasPrefundedHops,
|
|
670
|
+
usePaymaster: hasPaymaster,
|
|
671
|
+
});
|
|
661
672
|
const feeData = await this.aaManager.getFeeData();
|
|
662
673
|
const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
|
|
663
674
|
// 预估 prefund 的函数(使用全局常量)
|
|
@@ -667,25 +678,59 @@ export class AADexSwapExecutor {
|
|
|
667
678
|
const totalGas = callGas + verifyGas + preVerifyGas;
|
|
668
679
|
return (totalGas * gasPrice * PREFUND_BUFFER_PERCENT) / 100n;
|
|
669
680
|
};
|
|
670
|
-
// ✅
|
|
671
|
-
//
|
|
681
|
+
// ✅ 构建 hop 钱包列表
|
|
682
|
+
// 如果有预充值的 hop 钱包,使用它们;否则生成新钱包(需要 Paymaster)
|
|
672
683
|
const allGeneratedHopWallets = [];
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
684
|
+
if (hasPrefundedHops) {
|
|
685
|
+
// ✅ 预充值模式:使用传入的 hop 钱包
|
|
686
|
+
// 每个买方分配 effectiveHopCount 个 hop 钱包
|
|
687
|
+
const totalHopsNeeded = buyerSenders.length * effectiveHopCount;
|
|
688
|
+
if (prefundedHopWallets.length < totalHopsNeeded) {
|
|
689
|
+
throw new Error(`预充值 hop 钱包数量不足: 需要 ${totalHopsNeeded} 个,实际 ${prefundedHopWallets.length} 个`);
|
|
690
|
+
}
|
|
691
|
+
// ✅ 关键修复:从链上获取所有 hop 钱包的真实 nonce(用户可能重复使用同一批 hop 钱包)
|
|
692
|
+
const hopSenders = prefundedHopWallets.slice(0, totalHopsNeeded).map(h => h.senderAddress);
|
|
693
|
+
const hopNonces = await this.aaManager.batchGetNonces(hopSenders);
|
|
694
|
+
let hopIdx = 0;
|
|
695
|
+
for (let buyerIdx = 0; buyerIdx < buyerSenders.length; buyerIdx++) {
|
|
696
|
+
const chainHops = [];
|
|
697
|
+
for (let h = 0; h < effectiveHopCount; h++) {
|
|
698
|
+
const prefundedHop = prefundedHopWallets[hopIdx];
|
|
699
|
+
const wallet = new ethers.Wallet(prefundedHop.privateKey, provider);
|
|
700
|
+
chainHops.push({
|
|
701
|
+
wallet,
|
|
702
|
+
sender: prefundedHop.senderAddress,
|
|
703
|
+
deployed: prefundedHop.deployed,
|
|
704
|
+
initCode: prefundedHop.initCode,
|
|
705
|
+
});
|
|
706
|
+
// ✅ 使用从链上获取的真实 nonce(而非硬编码 0)
|
|
707
|
+
const realNonce = hopNonces[hopIdx] ?? 0n;
|
|
708
|
+
nonceMap.init(prefundedHop.senderAddress, realNonce);
|
|
709
|
+
hopIdx++;
|
|
710
|
+
}
|
|
711
|
+
allGeneratedHopWallets.push(chainHops);
|
|
685
712
|
}
|
|
686
|
-
|
|
713
|
+
console.log(`[AA DEX 多跳] ✅ 使用预充值 hop 钱包: ${totalHopsNeeded} 个 (${buyerSenders.length} 条链 × ${effectiveHopCount} 跳)`);
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
// Paymaster 模式:生成新钱包(不需要预充值,prefund=0)
|
|
717
|
+
for (let buyerIdx = 0; buyerIdx < buyerSenders.length; buyerIdx++) {
|
|
718
|
+
const chainHops = [];
|
|
719
|
+
for (let h = 0; h < effectiveHopCount; h++) {
|
|
720
|
+
const randomWallet = ethers.Wallet.createRandom();
|
|
721
|
+
const wallet = new ethers.Wallet(randomWallet.privateKey, provider);
|
|
722
|
+
const sender = await this.aaManager.predictSenderAddress(wallet.address);
|
|
723
|
+
const code = await provider.getCode(sender);
|
|
724
|
+
const deployed = code !== null && code !== '0x';
|
|
725
|
+
const initCode = deployed ? '0x' : this.aaManager.generateInitCode(wallet.address);
|
|
726
|
+
chainHops.push({ wallet, sender, deployed, initCode });
|
|
727
|
+
// 初始化 nonce
|
|
728
|
+
nonceMap.init(sender, 0n);
|
|
729
|
+
}
|
|
730
|
+
allGeneratedHopWallets.push(chainHops);
|
|
731
|
+
}
|
|
732
|
+
console.log(`[AA DEX 多跳] 使用 Paymaster 模式生成 hop 钱包: ${buyerSenders.length * effectiveHopCount} 个`);
|
|
687
733
|
}
|
|
688
|
-
console.log(`[AA DEX 多跳] 总 hop 钱包数: ${buyerSenders.length * hopCount} (${buyerSenders.length} 条链 × ${hopCount} 跳)`);
|
|
689
734
|
// ✅ 利润刮取(只在第一笔 seller UserOp 中执行)
|
|
690
735
|
let profitHandled = false;
|
|
691
736
|
// ✅ 构建每条多跳链的 UserOps
|
|
@@ -696,16 +741,14 @@ export class AADexSwapExecutor {
|
|
|
696
741
|
const buyAmount = buyAmountsWei[buyerIdx] ?? 0n;
|
|
697
742
|
if (buyAmount <= 0n)
|
|
698
743
|
continue;
|
|
699
|
-
//
|
|
700
|
-
const hopPrefunds = chainHops.map((hop, idx) => {
|
|
701
|
-
// 最后一个 hop 转账给 buyer,中间 hop 转账给下一个 hop
|
|
702
|
-
return estimatePrefund(HOP_CALL_GAS_LIMIT, hop.deployed);
|
|
703
|
-
});
|
|
704
|
-
const totalHopPrefund = hopPrefunds.reduce((a, b) => a + b, 0n);
|
|
705
|
-
// 计算 buyer 的 prefund(用于买入,使用常量)
|
|
744
|
+
// ✅ 计算 buyer 的 prefund(用于买入,使用常量)
|
|
706
745
|
const buyerPrefund = estimatePrefund(DEX_BUY_CALL_GAS_LIMIT, buyerAi.deployed);
|
|
707
|
-
//
|
|
708
|
-
|
|
746
|
+
// ✅ 计算分发金额:
|
|
747
|
+
// - 预充值模式:hop 已有 prefund,分发金额 = 买入金额 + buyer prefund
|
|
748
|
+
// - Paymaster 模式:prefund = 0,分发金额 = 买入金额 + buyer prefund
|
|
749
|
+
// 注意:无论哪种模式,都不需要在分发中包含 hop 的 prefund
|
|
750
|
+
//(预充值模式下 hop 已有余额,Paymaster 模式下 prefund=0)
|
|
751
|
+
const chainTotalAmount = buyAmount + buyerPrefund;
|
|
709
752
|
// 1) seller → hop0
|
|
710
753
|
const hop0 = chainHops[0];
|
|
711
754
|
let callData0;
|
|
@@ -744,13 +787,12 @@ export class AADexSwapExecutor {
|
|
|
744
787
|
});
|
|
745
788
|
outOps.push(signedSellerToHop0.userOp);
|
|
746
789
|
// 2) hop[j] → hop[j+1] (中间跳)
|
|
747
|
-
|
|
790
|
+
// ✅ 预充值模式下,hop 钱包自己有 prefund,只需要传递买入金额 + buyer prefund
|
|
748
791
|
for (let j = 0; j < chainHops.length - 1; j++) {
|
|
749
792
|
const currentHop = chainHops[j];
|
|
750
793
|
const nextHop = chainHops[j + 1];
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
const amountToTransfer = buyAmount + remainingPrefund + buyerPrefund;
|
|
794
|
+
// 传递金额 = 买入金额 + buyer prefund(hop 自己的 gas 从预充值中支付)
|
|
795
|
+
const amountToTransfer = buyAmount + buyerPrefund;
|
|
754
796
|
let callData;
|
|
755
797
|
if (useNativeToken) {
|
|
756
798
|
callData = encodeExecute(nextHop.sender, amountToTransfer, '0x');
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer AA 多跳钱包管理器
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* 1. 生成临时 Hop 钱包(可导出私钥)
|
|
6
|
+
* 2. 预充值 Hop 钱包(支持取消)
|
|
7
|
+
* 3. 退回预充值资金(原路返回)
|
|
8
|
+
* 4. 状态回调(进度通知)
|
|
9
|
+
*/
|
|
10
|
+
import { Wallet } from 'ethers';
|
|
11
|
+
import { AAAccountManager } from './aa-account.js';
|
|
12
|
+
import { XLayerConfig } from './types.js';
|
|
13
|
+
/** Hop 钱包信息 */
|
|
14
|
+
export interface HopWalletInfo {
|
|
15
|
+
/** 钱包索引 */
|
|
16
|
+
index: number;
|
|
17
|
+
/** Owner 私钥(可导出) */
|
|
18
|
+
privateKey: string;
|
|
19
|
+
/** Owner 地址 */
|
|
20
|
+
ownerAddress: string;
|
|
21
|
+
/** AA Sender 地址 */
|
|
22
|
+
senderAddress: string;
|
|
23
|
+
/** 是否已部署 */
|
|
24
|
+
deployed: boolean;
|
|
25
|
+
/** initCode(未部署时需要) */
|
|
26
|
+
initCode: string;
|
|
27
|
+
/** 预充值金额(wei) */
|
|
28
|
+
prefundWei: bigint;
|
|
29
|
+
/** 预充值状态 */
|
|
30
|
+
prefundStatus: 'pending' | 'funded' | 'refunded' | 'failed';
|
|
31
|
+
/** 预充值交易哈希 */
|
|
32
|
+
prefundTxHash?: string;
|
|
33
|
+
/** 退款交易哈希 */
|
|
34
|
+
refundTxHash?: string;
|
|
35
|
+
}
|
|
36
|
+
/** 预充值进度回调 */
|
|
37
|
+
export interface PrefundProgressCallback {
|
|
38
|
+
(progress: {
|
|
39
|
+
phase: 'generating' | 'prefunding' | 'confirming' | 'ready' | 'cancelled' | 'refunding' | 'refunded' | 'error';
|
|
40
|
+
current: number;
|
|
41
|
+
total: number;
|
|
42
|
+
message: string;
|
|
43
|
+
hopWallets?: HopWalletInfo[];
|
|
44
|
+
error?: Error;
|
|
45
|
+
}): void;
|
|
46
|
+
}
|
|
47
|
+
/** 预充值配置 */
|
|
48
|
+
export interface PrefundConfig {
|
|
49
|
+
/** Hop 数量 */
|
|
50
|
+
hopCount: number;
|
|
51
|
+
/** 每个 Hop 的预估 gas 消耗 */
|
|
52
|
+
hopCallGasLimit?: bigint;
|
|
53
|
+
/** 预留缓冲百分比(默认 150%) */
|
|
54
|
+
bufferPercent?: bigint;
|
|
55
|
+
/** Payer 钱包 */
|
|
56
|
+
payerWallet: Wallet;
|
|
57
|
+
/** 进度回调 */
|
|
58
|
+
onProgress?: PrefundProgressCallback;
|
|
59
|
+
/** 取消信号 */
|
|
60
|
+
abortSignal?: AbortSignal;
|
|
61
|
+
}
|
|
62
|
+
/** 预充值结果 */
|
|
63
|
+
export interface PrefundResult {
|
|
64
|
+
/** 是否成功 */
|
|
65
|
+
success: boolean;
|
|
66
|
+
/** Hop 钱包列表 */
|
|
67
|
+
hopWallets: HopWalletInfo[];
|
|
68
|
+
/** 总预充值金额(wei) */
|
|
69
|
+
totalPrefundWei: bigint;
|
|
70
|
+
/** 预充值交易哈希 */
|
|
71
|
+
prefundTxHash?: string;
|
|
72
|
+
/** 错误信息 */
|
|
73
|
+
error?: Error;
|
|
74
|
+
/** 是否被取消 */
|
|
75
|
+
cancelled?: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** 退款结果 */
|
|
78
|
+
export interface RefundResult {
|
|
79
|
+
/** 是否成功 */
|
|
80
|
+
success: boolean;
|
|
81
|
+
/** 退款总金额(wei) */
|
|
82
|
+
totalRefundWei: bigint;
|
|
83
|
+
/** 退款交易哈希列表 */
|
|
84
|
+
refundTxHashes: string[];
|
|
85
|
+
/** 各 Hop 退款详情 */
|
|
86
|
+
hopRefunds: {
|
|
87
|
+
hopIndex: number;
|
|
88
|
+
refundWei: bigint;
|
|
89
|
+
txHash?: string;
|
|
90
|
+
error?: string;
|
|
91
|
+
}[];
|
|
92
|
+
/** 错误信息 */
|
|
93
|
+
error?: Error;
|
|
94
|
+
}
|
|
95
|
+
export declare class HopWalletManager {
|
|
96
|
+
private aaManager;
|
|
97
|
+
private provider;
|
|
98
|
+
private config;
|
|
99
|
+
constructor(config?: XLayerConfig);
|
|
100
|
+
/**
|
|
101
|
+
* 生成 Hop 钱包列表
|
|
102
|
+
*/
|
|
103
|
+
generateHopWallets(count: number): Promise<HopWalletInfo[]>;
|
|
104
|
+
/**
|
|
105
|
+
* 计算单个 Hop 需要的 prefund
|
|
106
|
+
*/
|
|
107
|
+
calculateHopPrefund(deployed: boolean, callGasLimit?: bigint): Promise<bigint>;
|
|
108
|
+
/**
|
|
109
|
+
* 预充值 Hop 钱包
|
|
110
|
+
*
|
|
111
|
+
* 使用 Payer 发送一笔批量转账交易,给所有 Hop 钱包充值 prefund
|
|
112
|
+
*/
|
|
113
|
+
prefundHopWallets(config: PrefundConfig): Promise<PrefundResult>;
|
|
114
|
+
/**
|
|
115
|
+
* 退回 Hop 钱包的预充值资金
|
|
116
|
+
*
|
|
117
|
+
* 将所有 Hop 钱包的余额转回给指定地址(通常是 Payer)
|
|
118
|
+
*/
|
|
119
|
+
refundHopWallets(hopWallets: HopWalletInfo[], refundTo: string, onProgress?: PrefundProgressCallback): Promise<RefundResult>;
|
|
120
|
+
/**
|
|
121
|
+
* 使用 Payer 批量退回 Hop 钱包资金
|
|
122
|
+
*
|
|
123
|
+
* 通过 handleOps 批量执行所有 Hop 的退款操作
|
|
124
|
+
*/
|
|
125
|
+
refundHopWalletsViaPayer(hopWallets: HopWalletInfo[], payerWallet: Wallet, refundTo: string, onProgress?: PrefundProgressCallback): Promise<RefundResult>;
|
|
126
|
+
/**
|
|
127
|
+
* 导出 Hop 钱包信息(用于备份)
|
|
128
|
+
*/
|
|
129
|
+
exportHopWallets(hopWallets: HopWalletInfo[]): string;
|
|
130
|
+
/**
|
|
131
|
+
* 导入 Hop 钱包信息
|
|
132
|
+
*/
|
|
133
|
+
importHopWallets(jsonStr: string): Promise<HopWalletInfo[]>;
|
|
134
|
+
/**
|
|
135
|
+
* 编码 execute 调用
|
|
136
|
+
*/
|
|
137
|
+
private encodeExecute;
|
|
138
|
+
/**
|
|
139
|
+
* 获取 AAAccountManager
|
|
140
|
+
*/
|
|
141
|
+
getAAManager(): AAAccountManager;
|
|
142
|
+
}
|
|
143
|
+
export declare function createHopWalletManager(config?: XLayerConfig): HopWalletManager;
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer AA 多跳钱包管理器
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* 1. 生成临时 Hop 钱包(可导出私钥)
|
|
6
|
+
* 2. 预充值 Hop 钱包(支持取消)
|
|
7
|
+
* 3. 退回预充值资金(原路返回)
|
|
8
|
+
* 4. 状态回调(进度通知)
|
|
9
|
+
*/
|
|
10
|
+
import { ethers, Wallet } from 'ethers';
|
|
11
|
+
import { AAAccountManager } from './aa-account.js';
|
|
12
|
+
import { VERIFICATION_GAS_LIMIT_DEPLOY, VERIFICATION_GAS_LIMIT_NORMAL, PRE_VERIFICATION_GAS, } from './constants.js';
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// HopWalletManager
|
|
15
|
+
// ============================================================================
|
|
16
|
+
const DEFAULT_HOP_CALL_GAS_LIMIT = 150000n;
|
|
17
|
+
const DEFAULT_BUFFER_PERCENT = 150n; // 1.5x
|
|
18
|
+
export class HopWalletManager {
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.aaManager = new AAAccountManager(config);
|
|
22
|
+
this.provider = this.aaManager.getProvider();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 生成 Hop 钱包列表
|
|
26
|
+
*/
|
|
27
|
+
async generateHopWallets(count) {
|
|
28
|
+
const hopWallets = [];
|
|
29
|
+
for (let i = 0; i < count; i++) {
|
|
30
|
+
const randomWallet = ethers.Wallet.createRandom();
|
|
31
|
+
const wallet = new Wallet(randomWallet.privateKey);
|
|
32
|
+
const ownerAddress = wallet.address;
|
|
33
|
+
const senderAddress = await this.aaManager.predictSenderAddress(ownerAddress);
|
|
34
|
+
const code = await this.provider.getCode(senderAddress);
|
|
35
|
+
const deployed = code !== null && code !== '0x';
|
|
36
|
+
const initCode = deployed ? '0x' : this.aaManager.generateInitCode(ownerAddress);
|
|
37
|
+
hopWallets.push({
|
|
38
|
+
index: i,
|
|
39
|
+
privateKey: randomWallet.privateKey,
|
|
40
|
+
ownerAddress,
|
|
41
|
+
senderAddress,
|
|
42
|
+
deployed,
|
|
43
|
+
initCode,
|
|
44
|
+
prefundWei: 0n,
|
|
45
|
+
prefundStatus: 'pending',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return hopWallets;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 计算单个 Hop 需要的 prefund
|
|
52
|
+
*/
|
|
53
|
+
async calculateHopPrefund(deployed, callGasLimit) {
|
|
54
|
+
const feeData = await this.aaManager.getFeeData();
|
|
55
|
+
const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
|
|
56
|
+
const callGas = callGasLimit ?? DEFAULT_HOP_CALL_GAS_LIMIT;
|
|
57
|
+
const verifyGas = deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
|
|
58
|
+
const preVerifyGas = PRE_VERIFICATION_GAS;
|
|
59
|
+
const totalGas = callGas + verifyGas + preVerifyGas;
|
|
60
|
+
return (totalGas * gasPrice * DEFAULT_BUFFER_PERCENT) / 100n;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 预充值 Hop 钱包
|
|
64
|
+
*
|
|
65
|
+
* 使用 Payer 发送一笔批量转账交易,给所有 Hop 钱包充值 prefund
|
|
66
|
+
*/
|
|
67
|
+
async prefundHopWallets(config) {
|
|
68
|
+
const { hopCount, hopCallGasLimit, bufferPercent, payerWallet, onProgress, abortSignal } = config;
|
|
69
|
+
// 检查取消信号
|
|
70
|
+
const checkAbort = () => {
|
|
71
|
+
if (abortSignal?.aborted) {
|
|
72
|
+
throw new Error('PREFUND_CANCELLED');
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
// 1. 生成 Hop 钱包
|
|
77
|
+
onProgress?.({
|
|
78
|
+
phase: 'generating',
|
|
79
|
+
current: 0,
|
|
80
|
+
total: hopCount,
|
|
81
|
+
message: `正在生成 ${hopCount} 个 Hop 钱包...`,
|
|
82
|
+
});
|
|
83
|
+
checkAbort();
|
|
84
|
+
const hopWallets = await this.generateHopWallets(hopCount);
|
|
85
|
+
onProgress?.({
|
|
86
|
+
phase: 'generating',
|
|
87
|
+
current: hopCount,
|
|
88
|
+
total: hopCount,
|
|
89
|
+
message: `已生成 ${hopCount} 个 Hop 钱包`,
|
|
90
|
+
hopWallets,
|
|
91
|
+
});
|
|
92
|
+
// 2. 计算每个 Hop 需要的 prefund
|
|
93
|
+
checkAbort();
|
|
94
|
+
const feeData = await this.aaManager.getFeeData();
|
|
95
|
+
const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
|
|
96
|
+
const effectiveBufferPercent = bufferPercent ?? DEFAULT_BUFFER_PERCENT;
|
|
97
|
+
for (const hop of hopWallets) {
|
|
98
|
+
const callGas = hopCallGasLimit ?? DEFAULT_HOP_CALL_GAS_LIMIT;
|
|
99
|
+
const verifyGas = hop.deployed ? VERIFICATION_GAS_LIMIT_NORMAL : VERIFICATION_GAS_LIMIT_DEPLOY;
|
|
100
|
+
const preVerifyGas = PRE_VERIFICATION_GAS;
|
|
101
|
+
const totalGas = callGas + verifyGas + preVerifyGas;
|
|
102
|
+
hop.prefundWei = (totalGas * gasPrice * effectiveBufferPercent) / 100n;
|
|
103
|
+
}
|
|
104
|
+
const totalPrefundWei = hopWallets.reduce((sum, h) => sum + h.prefundWei, 0n);
|
|
105
|
+
// 3. 检查 Payer 余额
|
|
106
|
+
const payerAddress = payerWallet.address;
|
|
107
|
+
const payerBalance = await this.provider.getBalance(payerAddress);
|
|
108
|
+
const estimatedGas = 21000n * BigInt(hopCount) + 50000n; // 预估 gas
|
|
109
|
+
const totalNeeded = totalPrefundWei + estimatedGas * gasPrice;
|
|
110
|
+
if (payerBalance < totalNeeded) {
|
|
111
|
+
throw new Error(`Payer 余额不足: 需要 ${ethers.formatEther(totalNeeded)} OKB, 当前 ${ethers.formatEther(payerBalance)} OKB`);
|
|
112
|
+
}
|
|
113
|
+
// 4. 发送预充值交易
|
|
114
|
+
onProgress?.({
|
|
115
|
+
phase: 'prefunding',
|
|
116
|
+
current: 0,
|
|
117
|
+
total: hopCount,
|
|
118
|
+
message: `正在发送预充值交易...`,
|
|
119
|
+
hopWallets,
|
|
120
|
+
});
|
|
121
|
+
checkAbort();
|
|
122
|
+
// 使用批量转账(逐笔发送,XLayer 不支持 Bundle)
|
|
123
|
+
const connectedPayer = payerWallet.connect(this.provider);
|
|
124
|
+
const nonce = await this.provider.getTransactionCount(payerAddress, 'pending');
|
|
125
|
+
const txHashes = [];
|
|
126
|
+
for (let i = 0; i < hopWallets.length; i++) {
|
|
127
|
+
checkAbort();
|
|
128
|
+
const hop = hopWallets[i];
|
|
129
|
+
const tx = await connectedPayer.sendTransaction({
|
|
130
|
+
to: hop.senderAddress,
|
|
131
|
+
value: hop.prefundWei,
|
|
132
|
+
nonce: nonce + i,
|
|
133
|
+
gasPrice,
|
|
134
|
+
gasLimit: 21055n,
|
|
135
|
+
});
|
|
136
|
+
hop.prefundTxHash = tx.hash;
|
|
137
|
+
txHashes.push(tx.hash);
|
|
138
|
+
onProgress?.({
|
|
139
|
+
phase: 'prefunding',
|
|
140
|
+
current: i + 1,
|
|
141
|
+
total: hopCount,
|
|
142
|
+
message: `已发送 ${i + 1}/${hopCount} 笔预充值交易`,
|
|
143
|
+
hopWallets,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// 5. 等待所有交易确认
|
|
147
|
+
onProgress?.({
|
|
148
|
+
phase: 'confirming',
|
|
149
|
+
current: 0,
|
|
150
|
+
total: hopCount,
|
|
151
|
+
message: `正在等待交易确认...`,
|
|
152
|
+
hopWallets,
|
|
153
|
+
});
|
|
154
|
+
for (let i = 0; i < txHashes.length; i++) {
|
|
155
|
+
checkAbort();
|
|
156
|
+
const receipt = await this.provider.waitForTransaction(txHashes[i], 1, 60000);
|
|
157
|
+
if (receipt?.status === 1) {
|
|
158
|
+
hopWallets[i].prefundStatus = 'funded';
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
hopWallets[i].prefundStatus = 'failed';
|
|
162
|
+
}
|
|
163
|
+
onProgress?.({
|
|
164
|
+
phase: 'confirming',
|
|
165
|
+
current: i + 1,
|
|
166
|
+
total: hopCount,
|
|
167
|
+
message: `已确认 ${i + 1}/${hopCount} 笔交易`,
|
|
168
|
+
hopWallets,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// 检查是否全部成功
|
|
172
|
+
const allFunded = hopWallets.every(h => h.prefundStatus === 'funded');
|
|
173
|
+
if (!allFunded) {
|
|
174
|
+
const failedCount = hopWallets.filter(h => h.prefundStatus === 'failed').length;
|
|
175
|
+
throw new Error(`${failedCount} 笔预充值交易失败`);
|
|
176
|
+
}
|
|
177
|
+
onProgress?.({
|
|
178
|
+
phase: 'ready',
|
|
179
|
+
current: hopCount,
|
|
180
|
+
total: hopCount,
|
|
181
|
+
message: `✅ 预充值完成,共 ${ethers.formatEther(totalPrefundWei)} OKB`,
|
|
182
|
+
hopWallets,
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
hopWallets,
|
|
187
|
+
totalPrefundWei,
|
|
188
|
+
prefundTxHash: txHashes[0],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
const err = error;
|
|
193
|
+
if (err.message === 'PREFUND_CANCELLED') {
|
|
194
|
+
onProgress?.({
|
|
195
|
+
phase: 'cancelled',
|
|
196
|
+
current: 0,
|
|
197
|
+
total: hopCount,
|
|
198
|
+
message: '预充值已取消',
|
|
199
|
+
});
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
hopWallets: [],
|
|
203
|
+
totalPrefundWei: 0n,
|
|
204
|
+
cancelled: true,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
onProgress?.({
|
|
208
|
+
phase: 'error',
|
|
209
|
+
current: 0,
|
|
210
|
+
total: hopCount,
|
|
211
|
+
message: `预充值失败: ${err.message}`,
|
|
212
|
+
error: err,
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
hopWallets: [],
|
|
217
|
+
totalPrefundWei: 0n,
|
|
218
|
+
error: err,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 退回 Hop 钱包的预充值资金
|
|
224
|
+
*
|
|
225
|
+
* 将所有 Hop 钱包的余额转回给指定地址(通常是 Payer)
|
|
226
|
+
*/
|
|
227
|
+
async refundHopWallets(hopWallets, refundTo, onProgress) {
|
|
228
|
+
const hopRefunds = [];
|
|
229
|
+
const refundTxHashes = [];
|
|
230
|
+
let totalRefundWei = 0n;
|
|
231
|
+
onProgress?.({
|
|
232
|
+
phase: 'refunding',
|
|
233
|
+
current: 0,
|
|
234
|
+
total: hopWallets.length,
|
|
235
|
+
message: `正在退回 ${hopWallets.length} 个 Hop 钱包的资金...`,
|
|
236
|
+
hopWallets,
|
|
237
|
+
});
|
|
238
|
+
const feeData = await this.aaManager.getFeeData();
|
|
239
|
+
const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
|
|
240
|
+
const gasLimit = 21055n;
|
|
241
|
+
const gasCost = gasLimit * gasPrice;
|
|
242
|
+
for (let i = 0; i < hopWallets.length; i++) {
|
|
243
|
+
const hop = hopWallets[i];
|
|
244
|
+
try {
|
|
245
|
+
// 检查 Hop sender 余额
|
|
246
|
+
const balance = await this.provider.getBalance(hop.senderAddress);
|
|
247
|
+
if (balance <= gasCost) {
|
|
248
|
+
// 余额不足以支付 gas,跳过
|
|
249
|
+
hopRefunds.push({
|
|
250
|
+
hopIndex: hop.index,
|
|
251
|
+
refundWei: 0n,
|
|
252
|
+
error: '余额不足以支付 gas',
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const refundAmount = balance - gasCost;
|
|
257
|
+
// 使用 AA 账户转账(通过 UserOp)
|
|
258
|
+
const hopWallet = new Wallet(hop.privateKey, this.provider);
|
|
259
|
+
// 构建简单的 native 转账 UserOp
|
|
260
|
+
const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
261
|
+
ownerWallet: hopWallet,
|
|
262
|
+
sender: hop.senderAddress,
|
|
263
|
+
nonce: 0n,
|
|
264
|
+
initCode: hop.initCode,
|
|
265
|
+
callData: this.encodeExecute(refundTo, refundAmount, '0x'),
|
|
266
|
+
deployed: hop.deployed,
|
|
267
|
+
});
|
|
268
|
+
// 签名(buildUserOpWithFixedGas 不签名,需要手动签名)
|
|
269
|
+
const signedOp = await this.aaManager.signUserOp(userOp, hopWallet);
|
|
270
|
+
// 发送 handleOps
|
|
271
|
+
const entryPoint = this.aaManager.getEntryPoint();
|
|
272
|
+
const connectedEntryPoint = entryPoint.connect(new Wallet(hop.privateKey, this.provider));
|
|
273
|
+
// 由于 Hop 没有足够的 gas 发送 handleOps,需要用 Payer 来发送
|
|
274
|
+
// 这里我们返回 UserOp,让调用者处理
|
|
275
|
+
hopRefunds.push({
|
|
276
|
+
hopIndex: hop.index,
|
|
277
|
+
refundWei: refundAmount,
|
|
278
|
+
error: '需要外部 Payer 发送 handleOps(Hop 余额仅够 prefund)',
|
|
279
|
+
});
|
|
280
|
+
totalRefundWei += refundAmount;
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
hopRefunds.push({
|
|
284
|
+
hopIndex: hop.index,
|
|
285
|
+
refundWei: 0n,
|
|
286
|
+
error: error.message,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
onProgress?.({
|
|
290
|
+
phase: 'refunding',
|
|
291
|
+
current: i + 1,
|
|
292
|
+
total: hopWallets.length,
|
|
293
|
+
message: `已处理 ${i + 1}/${hopWallets.length} 个 Hop 钱包`,
|
|
294
|
+
hopWallets,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
onProgress?.({
|
|
298
|
+
phase: 'refunded',
|
|
299
|
+
current: hopWallets.length,
|
|
300
|
+
total: hopWallets.length,
|
|
301
|
+
message: `退款完成,共 ${ethers.formatEther(totalRefundWei)} OKB`,
|
|
302
|
+
hopWallets,
|
|
303
|
+
});
|
|
304
|
+
return {
|
|
305
|
+
success: true,
|
|
306
|
+
totalRefundWei,
|
|
307
|
+
refundTxHashes,
|
|
308
|
+
hopRefunds,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 使用 Payer 批量退回 Hop 钱包资金
|
|
313
|
+
*
|
|
314
|
+
* 通过 handleOps 批量执行所有 Hop 的退款操作
|
|
315
|
+
*/
|
|
316
|
+
async refundHopWalletsViaPayer(hopWallets, payerWallet, refundTo, onProgress) {
|
|
317
|
+
const hopRefunds = [];
|
|
318
|
+
const refundTxHashes = [];
|
|
319
|
+
let totalRefundWei = 0n;
|
|
320
|
+
onProgress?.({
|
|
321
|
+
phase: 'refunding',
|
|
322
|
+
current: 0,
|
|
323
|
+
total: hopWallets.length,
|
|
324
|
+
message: `正在构建退款 UserOps...`,
|
|
325
|
+
hopWallets,
|
|
326
|
+
});
|
|
327
|
+
const feeData = await this.aaManager.getFeeData();
|
|
328
|
+
const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
|
|
329
|
+
// 构建所有退款 UserOps
|
|
330
|
+
const userOps = [];
|
|
331
|
+
const hopWalletsToRefund = [];
|
|
332
|
+
// ✅ 关键修复:从链上批量获取所有 hop 钱包的真实 nonce
|
|
333
|
+
const hopSenders = hopWallets.map(h => h.senderAddress);
|
|
334
|
+
const hopNonces = await this.aaManager.batchGetNonces(hopSenders);
|
|
335
|
+
for (let i = 0; i < hopWallets.length; i++) {
|
|
336
|
+
const hop = hopWallets[i];
|
|
337
|
+
try {
|
|
338
|
+
const balance = await this.provider.getBalance(hop.senderAddress);
|
|
339
|
+
// 估算 UserOp 执行需要的 gas
|
|
340
|
+
const estimatedGas = 150000n * gasPrice;
|
|
341
|
+
if (balance <= estimatedGas) {
|
|
342
|
+
hopRefunds.push({
|
|
343
|
+
hopIndex: hop.index,
|
|
344
|
+
refundWei: 0n,
|
|
345
|
+
error: '余额不足',
|
|
346
|
+
});
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const refundAmount = balance - estimatedGas;
|
|
350
|
+
// 构建 UserOp
|
|
351
|
+
const hopWallet = new Wallet(hop.privateKey, this.provider);
|
|
352
|
+
// ✅ 使用从链上获取的真实 nonce
|
|
353
|
+
const realNonce = hopNonces[i] ?? 0n;
|
|
354
|
+
// ✅ 检查 hop 钱包是否已部署(根据 nonce 判断,nonce > 0 说明已用过,肯定已部署)
|
|
355
|
+
const isDeployed = realNonce > 0n || hop.deployed;
|
|
356
|
+
const initCode = isDeployed ? '0x' : hop.initCode;
|
|
357
|
+
const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
358
|
+
ownerWallet: hopWallet,
|
|
359
|
+
sender: hop.senderAddress,
|
|
360
|
+
nonce: realNonce,
|
|
361
|
+
initCode,
|
|
362
|
+
callData: this.encodeExecute(refundTo, refundAmount, '0x'),
|
|
363
|
+
deployed: isDeployed,
|
|
364
|
+
});
|
|
365
|
+
// 签名
|
|
366
|
+
const signedOp = await this.aaManager.signUserOp(userOp, hopWallet);
|
|
367
|
+
userOps.push(signedOp.userOp);
|
|
368
|
+
hopWalletsToRefund.push(hop);
|
|
369
|
+
totalRefundWei += refundAmount;
|
|
370
|
+
hopRefunds.push({
|
|
371
|
+
hopIndex: hop.index,
|
|
372
|
+
refundWei: refundAmount,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
hopRefunds.push({
|
|
377
|
+
hopIndex: hop.index,
|
|
378
|
+
refundWei: 0n,
|
|
379
|
+
error: error.message,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (userOps.length === 0) {
|
|
384
|
+
onProgress?.({
|
|
385
|
+
phase: 'refunded',
|
|
386
|
+
current: hopWallets.length,
|
|
387
|
+
total: hopWallets.length,
|
|
388
|
+
message: '没有可退款的 Hop 钱包',
|
|
389
|
+
hopWallets,
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
success: true,
|
|
393
|
+
totalRefundWei: 0n,
|
|
394
|
+
refundTxHashes: [],
|
|
395
|
+
hopRefunds,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
onProgress?.({
|
|
399
|
+
phase: 'refunding',
|
|
400
|
+
current: 0,
|
|
401
|
+
total: userOps.length,
|
|
402
|
+
message: `正在发送 handleOps 退款交易 (${userOps.length} 个 UserOps)...`,
|
|
403
|
+
hopWallets,
|
|
404
|
+
});
|
|
405
|
+
// 发送 handleOps
|
|
406
|
+
try {
|
|
407
|
+
const connectedPayer = payerWallet.connect(this.provider);
|
|
408
|
+
const entryPoint = this.aaManager.getEntryPoint();
|
|
409
|
+
const entryPointWithSigner = entryPoint.connect(connectedPayer);
|
|
410
|
+
const tx = await entryPointWithSigner.handleOps(userOps, payerWallet.address, {
|
|
411
|
+
gasPrice,
|
|
412
|
+
gasLimit: 500000n * BigInt(userOps.length),
|
|
413
|
+
});
|
|
414
|
+
refundTxHashes.push(tx.hash);
|
|
415
|
+
onProgress?.({
|
|
416
|
+
phase: 'confirming',
|
|
417
|
+
current: 0,
|
|
418
|
+
total: 1,
|
|
419
|
+
message: `等待 handleOps 交易确认...`,
|
|
420
|
+
hopWallets,
|
|
421
|
+
});
|
|
422
|
+
const receipt = await tx.wait();
|
|
423
|
+
if (receipt?.status === 1) {
|
|
424
|
+
for (const hop of hopWalletsToRefund) {
|
|
425
|
+
hop.prefundStatus = 'refunded';
|
|
426
|
+
hop.refundTxHash = tx.hash;
|
|
427
|
+
}
|
|
428
|
+
onProgress?.({
|
|
429
|
+
phase: 'refunded',
|
|
430
|
+
current: hopWallets.length,
|
|
431
|
+
total: hopWallets.length,
|
|
432
|
+
message: `✅ 退款完成,共 ${ethers.formatEther(totalRefundWei)} OKB`,
|
|
433
|
+
hopWallets,
|
|
434
|
+
});
|
|
435
|
+
return {
|
|
436
|
+
success: true,
|
|
437
|
+
totalRefundWei,
|
|
438
|
+
refundTxHashes,
|
|
439
|
+
hopRefunds,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
throw new Error('handleOps 交易失败');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
onProgress?.({
|
|
448
|
+
phase: 'error',
|
|
449
|
+
current: 0,
|
|
450
|
+
total: hopWallets.length,
|
|
451
|
+
message: `退款失败: ${error.message}`,
|
|
452
|
+
error: error,
|
|
453
|
+
hopWallets,
|
|
454
|
+
});
|
|
455
|
+
return {
|
|
456
|
+
success: false,
|
|
457
|
+
totalRefundWei: 0n,
|
|
458
|
+
refundTxHashes,
|
|
459
|
+
hopRefunds,
|
|
460
|
+
error: error,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* 导出 Hop 钱包信息(用于备份)
|
|
466
|
+
*/
|
|
467
|
+
exportHopWallets(hopWallets) {
|
|
468
|
+
const exportData = hopWallets.map(h => ({
|
|
469
|
+
index: h.index,
|
|
470
|
+
privateKey: h.privateKey,
|
|
471
|
+
ownerAddress: h.ownerAddress,
|
|
472
|
+
senderAddress: h.senderAddress,
|
|
473
|
+
prefundWei: h.prefundWei.toString(),
|
|
474
|
+
prefundStatus: h.prefundStatus,
|
|
475
|
+
prefundTxHash: h.prefundTxHash,
|
|
476
|
+
}));
|
|
477
|
+
return JSON.stringify(exportData, null, 2);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* 导入 Hop 钱包信息
|
|
481
|
+
*/
|
|
482
|
+
async importHopWallets(jsonStr) {
|
|
483
|
+
const data = JSON.parse(jsonStr);
|
|
484
|
+
const hopWallets = [];
|
|
485
|
+
for (const item of data) {
|
|
486
|
+
const wallet = new Wallet(item.privateKey);
|
|
487
|
+
const senderAddress = await this.aaManager.predictSenderAddress(wallet.address);
|
|
488
|
+
const code = await this.provider.getCode(senderAddress);
|
|
489
|
+
const deployed = code !== null && code !== '0x';
|
|
490
|
+
const initCode = deployed ? '0x' : this.aaManager.generateInitCode(wallet.address);
|
|
491
|
+
hopWallets.push({
|
|
492
|
+
index: item.index,
|
|
493
|
+
privateKey: item.privateKey,
|
|
494
|
+
ownerAddress: item.ownerAddress,
|
|
495
|
+
senderAddress,
|
|
496
|
+
deployed,
|
|
497
|
+
initCode,
|
|
498
|
+
prefundWei: BigInt(item.prefundWei || '0'),
|
|
499
|
+
prefundStatus: item.prefundStatus || 'pending',
|
|
500
|
+
prefundTxHash: item.prefundTxHash,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return hopWallets;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* 编码 execute 调用
|
|
507
|
+
*/
|
|
508
|
+
encodeExecute(to, value, data) {
|
|
509
|
+
const iface = new ethers.Interface([
|
|
510
|
+
'function execute(address dest, uint256 value, bytes func) external',
|
|
511
|
+
]);
|
|
512
|
+
return iface.encodeFunctionData('execute', [to, value, data]);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* 获取 AAAccountManager
|
|
516
|
+
*/
|
|
517
|
+
getAAManager() {
|
|
518
|
+
return this.aaManager;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// ============================================================================
|
|
522
|
+
// 工厂函数
|
|
523
|
+
// ============================================================================
|
|
524
|
+
export function createHopWalletManager(config = {}) {
|
|
525
|
+
return new HopWalletManager(config);
|
|
526
|
+
}
|
package/dist/xlayer/index.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export { BundleExecutor, createBundleExecutor, bundleBuy, bundleSell, bundleBuyS
|
|
|
66
66
|
export { DexBundleExecutor, createDexBundleExecutor, dexBundleBuySell, type DexBundleConfig, type DexBundleBuySellParams, } from './dex-bundle.js';
|
|
67
67
|
export { AAPortalSwapExecutor, } from './portal-bundle-swap.js';
|
|
68
68
|
export { AADexSwapExecutor, } from './dex-bundle-swap.js';
|
|
69
|
+
export { HopWalletManager, createHopWalletManager, type HopWalletInfo, type PrefundProgressCallback, type PrefundConfig, type PrefundResult, type RefundResult, } from './hop-wallet-manager.js';
|
|
69
70
|
export { AAPortalBuyFirstExecutor, createAAPortalBuyFirstExecutor, } from './portal-buy-first.js';
|
|
70
71
|
export { AADexBuyFirstExecutor, createAADexBuyFirstExecutor, } from './dex-buy-first.js';
|
|
71
72
|
export { BuyFirstVolumeExecutor, createBuyFirstVolumeExecutor, makeBuyFirstVolume, } from './buy-first-volume.js';
|
package/dist/xlayer/index.js
CHANGED
|
@@ -94,6 +94,10 @@ export { DexBundleExecutor, createDexBundleExecutor, dexBundleBuySell, } from '.
|
|
|
94
94
|
export { AAPortalSwapExecutor, } from './portal-bundle-swap.js';
|
|
95
95
|
export { AADexSwapExecutor, } from './dex-bundle-swap.js';
|
|
96
96
|
// ============================================================================
|
|
97
|
+
// Hop 钱包管理器(多跳预充值)
|
|
98
|
+
// ============================================================================
|
|
99
|
+
export { HopWalletManager, createHopWalletManager, } from './hop-wallet-manager.js';
|
|
100
|
+
// ============================================================================
|
|
97
101
|
// AA Buy-First(刷量专用)
|
|
98
102
|
// ============================================================================
|
|
99
103
|
export { AAPortalBuyFirstExecutor, createAAPortalBuyFirstExecutor, } from './portal-buy-first.js';
|
|
@@ -277,7 +277,8 @@ export class AAPortalSwapExecutor {
|
|
|
277
277
|
*/
|
|
278
278
|
async bundleBatchSwapSign(params) {
|
|
279
279
|
const { tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, capitalMode = true, // ✅ 默认资金利用率模式(卖出→分发→买入)
|
|
280
|
-
disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, quoteToken, quoteTokenDecimals = 6,
|
|
280
|
+
disperseHopCount: disperseHopCountIn, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, quoteToken, quoteTokenDecimals = 6, prefundedHopWallets, // ✅ 预充值多跳:已预充值的 hop 钱包列表
|
|
281
|
+
} = params;
|
|
281
282
|
const effectiveConfig = { ...(this.config ?? {}), ...(params.config ?? {}) };
|
|
282
283
|
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effectiveConfig);
|
|
283
284
|
// ✅ ERC20 稳定币支持
|
|
@@ -390,17 +391,23 @@ export class AAPortalSwapExecutor {
|
|
|
390
391
|
extractProfit,
|
|
391
392
|
profitWei: ethers.formatEther(profitWei),
|
|
392
393
|
});
|
|
393
|
-
// ✅
|
|
394
|
+
// ✅ 多跳模式判断:
|
|
394
395
|
// ERC-4337 的 handleOps 执行流程是"先验证所有 UserOps,再执行所有 UserOps"
|
|
395
396
|
// hop 钱包是临时生成的(余额=0),在 validation 阶段无法支付 prefund,会导致 AA21 错误
|
|
396
397
|
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
398
|
+
// 多跳可用的条件:
|
|
399
|
+
// 1. 配置了 Paymaster(prefund = 0n,hop 钱包不需要余额)
|
|
400
|
+
// 2. 传入了预充值好的 hop 钱包(hop 钱包已有余额支付 prefund)
|
|
399
401
|
const hasPaymaster = this.aaManager.hasPaymaster();
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
402
|
+
const hasPrefundedHops = Array.isArray(prefundedHopWallets) && prefundedHopWallets.length > 0;
|
|
403
|
+
const canDoMultiHop = hasPaymaster || hasPrefundedHops;
|
|
404
|
+
const effectiveHopCount = canDoMultiHop ? hopCount : 0;
|
|
405
|
+
if (hopCount > 0 && !canDoMultiHop) {
|
|
406
|
+
console.warn('[AA Portal 批量换手] ⚠️ 无法执行多跳(hop 钱包无法支付 prefund),已自动降级为直接分发模式');
|
|
407
|
+
console.warn('[AA Portal 批量换手] 💡 提示:使用 HopWalletManager.prefundHopWallets() 预充值 hop 钱包,或配置 Paymaster');
|
|
408
|
+
}
|
|
409
|
+
else if (hopCount > 0 && hasPrefundedHops) {
|
|
410
|
+
console.log('[AA Portal 批量换手] ✅ 预充值多跳模式已启用 (hopCount:', hopCount, ', prefundedHops:', prefundedHopWallets.length, ')');
|
|
404
411
|
}
|
|
405
412
|
else if (hopCount > 0 && hasPaymaster) {
|
|
406
413
|
console.log('[AA Portal 批量换手] ✅ Paymaster 模式已启用,支持多跳换手 (hopCount:', hopCount, ')');
|
|
@@ -462,9 +469,13 @@ export class AAPortalSwapExecutor {
|
|
|
462
469
|
}
|
|
463
470
|
}
|
|
464
471
|
else {
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
472
|
+
// ✅ 多跳模式:使用预充值的 hop 钱包或 Paymaster 模式
|
|
473
|
+
console.log('[AA Portal 批量换手] 进入多跳模式 (effectiveHopCount > 0):', {
|
|
474
|
+
effectiveHopCount,
|
|
475
|
+
buyerCount: buyerSenders.length,
|
|
476
|
+
usePrefundedHops: hasPrefundedHops,
|
|
477
|
+
usePaymaster: hasPaymaster,
|
|
478
|
+
});
|
|
468
479
|
const feeData = await this.aaManager.getFeeData();
|
|
469
480
|
const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
|
|
470
481
|
const provider = this.aaManager.getProvider();
|
|
@@ -475,25 +486,57 @@ export class AAPortalSwapExecutor {
|
|
|
475
486
|
const totalGas = callGas + verifyGas + preVerifyGas;
|
|
476
487
|
return (totalGas * gasPrice * PREFUND_BUFFER_PERCENT) / 100n;
|
|
477
488
|
};
|
|
478
|
-
// ✅
|
|
479
|
-
//
|
|
489
|
+
// ✅ 构建 hop 钱包列表
|
|
490
|
+
// 如果有预充值的 hop 钱包,使用它们;否则生成新钱包(需要 Paymaster)
|
|
480
491
|
const allGeneratedHopWallets = [];
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
492
|
+
if (hasPrefundedHops) {
|
|
493
|
+
// ✅ 预充值模式:使用传入的 hop 钱包
|
|
494
|
+
const totalHopsNeeded = buyerSenders.length * effectiveHopCount;
|
|
495
|
+
if (prefundedHopWallets.length < totalHopsNeeded) {
|
|
496
|
+
throw new Error(`预充值 hop 钱包数量不足: 需要 ${totalHopsNeeded} 个,实际 ${prefundedHopWallets.length} 个`);
|
|
497
|
+
}
|
|
498
|
+
// ✅ 关键修复:从链上获取所有 hop 钱包的真实 nonce(用户可能重复使用同一批 hop 钱包)
|
|
499
|
+
const hopSenders = prefundedHopWallets.slice(0, totalHopsNeeded).map(h => h.senderAddress);
|
|
500
|
+
const hopNonces = await this.aaManager.batchGetNonces(hopSenders);
|
|
501
|
+
let hopIdx = 0;
|
|
502
|
+
for (let buyerIdx = 0; buyerIdx < buyerSenders.length; buyerIdx++) {
|
|
503
|
+
const chainHops = [];
|
|
504
|
+
for (let h = 0; h < effectiveHopCount; h++) {
|
|
505
|
+
const prefundedHop = prefundedHopWallets[hopIdx];
|
|
506
|
+
const wallet = new ethers.Wallet(prefundedHop.privateKey, provider);
|
|
507
|
+
chainHops.push({
|
|
508
|
+
wallet,
|
|
509
|
+
sender: prefundedHop.senderAddress,
|
|
510
|
+
deployed: prefundedHop.deployed,
|
|
511
|
+
initCode: prefundedHop.initCode,
|
|
512
|
+
});
|
|
513
|
+
// ✅ 使用从链上获取的真实 nonce(而非硬编码 0)
|
|
514
|
+
const realNonce = hopNonces[hopIdx] ?? 0n;
|
|
515
|
+
nonceMap.init(prefundedHop.senderAddress, realNonce);
|
|
516
|
+
hopIdx++;
|
|
517
|
+
}
|
|
518
|
+
allGeneratedHopWallets.push(chainHops);
|
|
519
|
+
}
|
|
520
|
+
console.log(`[AA Portal 多跳] ✅ 使用预充值 hop 钱包: ${totalHopsNeeded} 个`);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
// Paymaster 模式:生成新钱包
|
|
524
|
+
for (let buyerIdx = 0; buyerIdx < buyerSenders.length; buyerIdx++) {
|
|
525
|
+
const chainHops = [];
|
|
526
|
+
for (let h = 0; h < effectiveHopCount; h++) {
|
|
527
|
+
const randomWallet = ethers.Wallet.createRandom();
|
|
528
|
+
const wallet = new ethers.Wallet(randomWallet.privateKey, provider);
|
|
529
|
+
const sender = await this.aaManager.predictSenderAddress(wallet.address);
|
|
530
|
+
const code = await provider.getCode(sender);
|
|
531
|
+
const deployed = code !== null && code !== '0x';
|
|
532
|
+
const initCode = deployed ? '0x' : this.aaManager.generateInitCode(wallet.address);
|
|
533
|
+
chainHops.push({ wallet, sender, deployed, initCode });
|
|
534
|
+
nonceMap.init(sender, 0n);
|
|
535
|
+
}
|
|
536
|
+
allGeneratedHopWallets.push(chainHops);
|
|
493
537
|
}
|
|
494
|
-
|
|
538
|
+
console.log(`[AA Portal 多跳] 使用 Paymaster 模式生成 hop 钱包`);
|
|
495
539
|
}
|
|
496
|
-
console.log(`[AA 多跳] 总 hop 钱包数: ${buyerSenders.length * hopCount} (${buyerSenders.length} 条链 × ${hopCount} 跳)`);
|
|
497
540
|
// ✅ 利润刮取(只在第一笔 seller UserOp 中执行)
|
|
498
541
|
let profitHandled = false;
|
|
499
542
|
// ✅ 构建每条多跳链的 UserOps
|
package/dist/xlayer/types.d.ts
CHANGED
|
@@ -552,6 +552,24 @@ export interface BundleBatchSwapSignParams extends BundleBatchSwapParams {
|
|
|
552
552
|
payerStartNonce?: number;
|
|
553
553
|
handleOpsGasLimit?: bigint;
|
|
554
554
|
routeAddress?: string;
|
|
555
|
+
/**
|
|
556
|
+
* ✅ 预充值多跳:传入已预充值好的 Hop 钱包列表
|
|
557
|
+
*
|
|
558
|
+
* 如果提供此参数,将使用这些 hop 钱包进行多跳转账,
|
|
559
|
+
* 而不是自动生成新钱包(需要 Paymaster 才能工作的模式)
|
|
560
|
+
*
|
|
561
|
+
* 使用流程:
|
|
562
|
+
* 1. 调用 HopWalletManager.prefundHopWallets() 预充值
|
|
563
|
+
* 2. 将返回的 hopWallets 传入此参数
|
|
564
|
+
* 3. 多跳将使用这些已有余额的 hop 钱包
|
|
565
|
+
*/
|
|
566
|
+
prefundedHopWallets?: Array<{
|
|
567
|
+
privateKey: string;
|
|
568
|
+
ownerAddress: string;
|
|
569
|
+
senderAddress: string;
|
|
570
|
+
deployed: boolean;
|
|
571
|
+
initCode: string;
|
|
572
|
+
}>;
|
|
555
573
|
}
|
|
556
574
|
export interface BundleBatchSwapSignResult {
|
|
557
575
|
signedTransactions: string[];
|
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
|
-
}
|