four-flap-meme-sdk 1.6.87 → 1.6.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/xlayer/aa-transfer-profit.js +119 -44
- package/dist/xlayer/constants.d.ts +1 -1
- package/dist/xlayer/constants.js +6 -0
- package/dist/xlayer/paymaster.d.ts +102 -56
- package/dist/xlayer/paymaster.js +189 -95
- package/dist/xlayer/wash-ops.js +76 -76
- package/package.json +1 -1
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import { ethers } from 'ethers';
|
|
2
|
-
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
2
|
+
import { PROFIT_CONFIG, ZERO_ADDRESS } from '../utils/constants.js';
|
|
3
3
|
import { createAAAccountManager, createWallet, encodeExecute, encodeExecuteBatch, encodeExecuteViaMulticall3 } from './aa-account.js';
|
|
4
|
+
import { quoteTokenToOkb } from './dex.js';
|
|
5
|
+
import { PortalQuery } from './portal-ops.js';
|
|
6
|
+
/**
|
|
7
|
+
* ✅ 获取 Token → OKB 报价(带回退逻辑)
|
|
8
|
+
* 和 bundle.ts 保持一致:先尝试 V2,失败后回退到 Flap Portal
|
|
9
|
+
*/
|
|
10
|
+
async function quoteTokenToOkbWithFallback(tokenAmount, tokenAddress, config) {
|
|
11
|
+
if (tokenAmount <= 0n)
|
|
12
|
+
return 0n;
|
|
13
|
+
// 1. 先尝试 V2 报价
|
|
14
|
+
try {
|
|
15
|
+
const okbOut = await quoteTokenToOkb(tokenAmount, tokenAddress, { rpcUrl: config.rpcUrl });
|
|
16
|
+
if (okbOut > 0n)
|
|
17
|
+
return okbOut;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// V2 报价失败,继续尝试 Flap
|
|
21
|
+
}
|
|
22
|
+
// 2. 回退到 Flap Portal 报价
|
|
23
|
+
try {
|
|
24
|
+
const portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId ?? 196 });
|
|
25
|
+
const okbOut = await portalQuery.quoteExactInput(tokenAddress, ZERO_ADDRESS, tokenAmount);
|
|
26
|
+
if (okbOut > 0n)
|
|
27
|
+
return okbOut;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Flap 报价也失败
|
|
31
|
+
}
|
|
32
|
+
return 0n;
|
|
33
|
+
}
|
|
4
34
|
function clampBps(bps) {
|
|
5
35
|
if (!Number.isFinite(bps))
|
|
6
36
|
return 0;
|
|
@@ -56,14 +86,20 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
56
86
|
const erc20Iface = new ethers.Interface([
|
|
57
87
|
'function transfer(address to, uint256 amount) returns (bool)',
|
|
58
88
|
]);
|
|
59
|
-
//
|
|
89
|
+
// 计算总金额与利润
|
|
90
|
+
// ✅ Native:利润从总额中扣除,保留在 AA 钱包
|
|
91
|
+
// ✅ ERC20:代币全额分发,利润以 OKB 形式收取(需要报价)
|
|
60
92
|
let totalWei = 0n;
|
|
93
|
+
let profitWei = 0n; // ✅ 利润(OKB wei)
|
|
94
|
+
let profitIsOkb = false; // ✅ 标记利润是否为 OKB(用于 ERC20)
|
|
61
95
|
if (params.kind === 'native') {
|
|
62
96
|
const values = amtList.map((x) => {
|
|
63
97
|
const v = ethers.parseEther(String(x || '0'));
|
|
64
98
|
return v > 0n ? v : 0n;
|
|
65
99
|
});
|
|
66
100
|
totalWei = values.reduce((a, b) => a + b, 0n);
|
|
101
|
+
profitWei = calcProfitWei(totalWei, PROFIT_CONFIG.RATE_BPS);
|
|
102
|
+
profitIsOkb = false; // Native 利润就是 OKB,但走 Native 分发逻辑
|
|
67
103
|
}
|
|
68
104
|
else {
|
|
69
105
|
const token = String(params.tokenAddress || '').trim();
|
|
@@ -77,10 +113,29 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
77
113
|
return v > 0n ? v : 0n;
|
|
78
114
|
});
|
|
79
115
|
totalWei = values.reduce((a, b) => a + b, 0n);
|
|
116
|
+
// ✅ ERC20:报价获取代币的 OKB 价值,然后计算 OKB 利润
|
|
117
|
+
// 和 bundle.ts/wash-ops.ts 保持一致:先尝试 V2,失败后回退到 Flap Portal
|
|
118
|
+
if (totalWei > 0n) {
|
|
119
|
+
const okbValueWei = await quoteTokenToOkbWithFallback(totalWei, token, { rpcUrl: effConfig.rpcUrl, chainId: effConfig.chainId });
|
|
120
|
+
if (okbValueWei > 0n) {
|
|
121
|
+
profitWei = calcProfitWei(okbValueWei, PROFIT_CONFIG.RATE_BPS);
|
|
122
|
+
profitIsOkb = true;
|
|
123
|
+
console.log(`[AA 分发] ERC20 报价: ${ethers.formatUnits(totalWei, dec)} Token = ${ethers.formatEther(okbValueWei)} OKB, 利润: ${ethers.formatEther(profitWei)} OKB`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
console.warn('[AA 分发] ERC20 报价失败(V2 和 Flap 都无法获取价格),跳过利润收取');
|
|
127
|
+
profitWei = 0n;
|
|
128
|
+
profitIsOkb = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
80
131
|
}
|
|
81
|
-
|
|
82
|
-
const distributableWei =
|
|
83
|
-
|
|
132
|
+
// ✅ Native:扣除利润后分发;ERC20:全额分发(利润另外以 OKB 转账)
|
|
133
|
+
const distributableWei = params.kind === 'native'
|
|
134
|
+
? (totalWei > profitWei ? (totalWei - profitWei) : 0n)
|
|
135
|
+
: totalWei;
|
|
136
|
+
// 按比例缩放每个收款金额
|
|
137
|
+
// ✅ Native:使"总分发=总额-利润";利润保留在 AA 钱包
|
|
138
|
+
// ✅ ERC20:全额分发代币,利润以 OKB 单独转账
|
|
84
139
|
const scaled = [];
|
|
85
140
|
if (totalWei > 0n && distributableWei > 0n) {
|
|
86
141
|
if (params.kind === 'native') {
|
|
@@ -107,26 +162,11 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
107
162
|
}
|
|
108
163
|
}
|
|
109
164
|
else {
|
|
165
|
+
// ✅ ERC20:全额分发,不缩放
|
|
110
166
|
const dec = Number(params.tokenDecimals);
|
|
111
|
-
const
|
|
167
|
+
for (const x of amtList) {
|
|
112
168
|
const v = ethers.parseUnits(String(x || '0'), dec);
|
|
113
|
-
|
|
114
|
-
});
|
|
115
|
-
let acc = 0n;
|
|
116
|
-
for (let i = 0; i < raw.length; i++) {
|
|
117
|
-
const v = raw[i];
|
|
118
|
-
const out = (v * distributableWei) / totalWei;
|
|
119
|
-
scaled.push(out);
|
|
120
|
-
acc += out;
|
|
121
|
-
}
|
|
122
|
-
const rem = distributableWei - acc;
|
|
123
|
-
if (rem > 0n) {
|
|
124
|
-
for (let i = scaled.length - 1; i >= 0; i--) {
|
|
125
|
-
if (scaled[i] > 0n || raw[i] > 0n) {
|
|
126
|
-
scaled[i] = scaled[i] + rem;
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
169
|
+
scaled.push(v > 0n ? v : 0n);
|
|
130
170
|
}
|
|
131
171
|
}
|
|
132
172
|
}
|
|
@@ -138,7 +178,9 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
138
178
|
const maxPerOp = Math.max(1, Math.floor(Number(params.maxTransfersPerUserOp ?? (params.kind === 'native' ? 30 : 20))));
|
|
139
179
|
const items = toList.map((to, i) => ({ to, amountWei: scaled[i] ?? 0n }))
|
|
140
180
|
.filter((x) => x.amountWei > 0n);
|
|
141
|
-
|
|
181
|
+
// ✅ Native:利润作为普通转账加入 items
|
|
182
|
+
// ✅ ERC20:利润以 OKB 形式单独处理,不加入 items
|
|
183
|
+
if (profitWei > 0n && params.kind === 'native') {
|
|
142
184
|
items.push({ to: PROFIT_CONFIG.RECIPIENT, amountWei: profitWei });
|
|
143
185
|
}
|
|
144
186
|
const chunks = [];
|
|
@@ -202,17 +244,32 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
|
|
|
202
244
|
}
|
|
203
245
|
else {
|
|
204
246
|
// ERC20 转账:executeBatch([token, token, ...], [0, 0, ...], [transfer1, transfer2, ...])
|
|
247
|
+
// ✅ 第一个批次额外包含 OKB 利润转账
|
|
205
248
|
const token = String(params.tokenAddress || '').trim();
|
|
206
|
-
const dests =
|
|
207
|
-
const values =
|
|
208
|
-
const datas =
|
|
249
|
+
const dests = [];
|
|
250
|
+
const values = [];
|
|
251
|
+
const datas = [];
|
|
252
|
+
// 添加 ERC20 转账
|
|
253
|
+
for (const it of list) {
|
|
254
|
+
dests.push(token);
|
|
255
|
+
values.push(0n);
|
|
256
|
+
datas.push(erc20Iface.encodeFunctionData('transfer', [it.to, it.amountWei]));
|
|
257
|
+
}
|
|
258
|
+
// ✅ 在第一个批次中添加 OKB 利润转账
|
|
259
|
+
if (i === 0 && profitIsOkb && profitWei > 0n) {
|
|
260
|
+
dests.push(PROFIT_CONFIG.RECIPIENT);
|
|
261
|
+
values.push(profitWei);
|
|
262
|
+
datas.push('0x'); // Native 转账无需 calldata
|
|
263
|
+
}
|
|
209
264
|
const callData = encodeExecuteBatch(dests, values, datas);
|
|
210
265
|
const nonce = nonce0 + BigInt(i);
|
|
211
266
|
const initCode = i === 0 ? String(initCode0) : '0x';
|
|
212
267
|
const deployed = i === 0 ? deployed0 : true;
|
|
268
|
+
// ✅ 计算 gas,包含 OKB 利润转账
|
|
269
|
+
const transferCount = list.length + (i === 0 && profitIsOkb && profitWei > 0n ? 1 : 0);
|
|
213
270
|
const base = 220000n;
|
|
214
|
-
const per = 65000n;
|
|
215
|
-
const g = base + per * BigInt(
|
|
271
|
+
const per = 65000n;
|
|
272
|
+
const g = base + per * BigInt(transferCount);
|
|
216
273
|
const callGasLimit = g > 4500000n ? 4500000n : g;
|
|
217
274
|
const built = await fnFixed.call(aaManager, {
|
|
218
275
|
ownerWallet,
|
|
@@ -328,16 +385,34 @@ export async function buildTransfersWithProfit(params) {
|
|
|
328
385
|
}
|
|
329
386
|
if (amountWei <= 0n)
|
|
330
387
|
continue;
|
|
331
|
-
const profitWei = calcProfitWei(amountWei, PROFIT_CONFIG.RATE_BPS);
|
|
332
388
|
const deployed = accountInfos[i]?.deployed ?? false;
|
|
333
389
|
const prefundWei = params.kind === 'native' ? estimateTransferPrefund(deployed) : 0n;
|
|
334
|
-
// ✅
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
390
|
+
// ✅ Native:利润从金额中扣除,保留在 AA 钱包(以 OKB 形式)
|
|
391
|
+
// ✅ ERC20:代币全额分发,利润以 OKB 形式收取(需报价)
|
|
392
|
+
let profitWei = 0n;
|
|
393
|
+
let toWei = amountWei;
|
|
394
|
+
if (params.kind === 'native') {
|
|
395
|
+
profitWei = calcProfitWei(amountWei, PROFIT_CONFIG.RATE_BPS);
|
|
396
|
+
// ✅ 关键修复:toWei 需要扣除 max(profitWei, prefundWei),确保留足够的 gas
|
|
397
|
+
const reserveWei = profitWei > prefundWei ? profitWei : prefundWei;
|
|
398
|
+
toWei = amountWei > reserveWei ? (amountWei - reserveWei) : 0n;
|
|
399
|
+
totalProfitWei += profitWei;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// ✅ ERC20:全额分发代币,利润以 OKB 形式收取
|
|
403
|
+
// 和 bundle.ts/wash-ops.ts 保持一致:先尝试 V2,失败后回退到 Flap Portal
|
|
404
|
+
toWei = amountWei;
|
|
405
|
+
const token = String(params.tokenAddress || '').trim();
|
|
406
|
+
const okbValueWei = await quoteTokenToOkbWithFallback(amountWei, token, { rpcUrl: effConfig.rpcUrl, chainId: effConfig.chainId });
|
|
407
|
+
if (okbValueWei > 0n) {
|
|
408
|
+
profitWei = calcProfitWei(okbValueWei, PROFIT_CONFIG.RATE_BPS);
|
|
409
|
+
totalProfitWei += profitWei;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
console.warn(`[AA 归集] ERC20 报价失败 (钱包 ${i}),跳过利润收取`);
|
|
413
|
+
profitWei = 0n;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
341
416
|
transferInfos.push({ walletIndex: i, sender, to, amountWei, profitWei, toWei, prefundWei });
|
|
342
417
|
}
|
|
343
418
|
console.log(`[AA 归集] 钱包数量: ${transferInfos.length}, 总利润: ${ethers.formatEther(totalProfitWei)} OKB`);
|
|
@@ -361,17 +436,17 @@ export async function buildTransfersWithProfit(params) {
|
|
|
361
436
|
callGasLimit = 120000n;
|
|
362
437
|
}
|
|
363
438
|
else {
|
|
364
|
-
// ✅ ERC20
|
|
365
|
-
// executeBatch: 利润转给利润地址 + 归集金额转给目标地址
|
|
439
|
+
// ✅ ERC20:全额转出给目标地址 + OKB 利润转给利润接收者
|
|
366
440
|
const token = String(params.tokenAddress || '').trim();
|
|
367
441
|
if (info.profitWei > 0n) {
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
callData = encodeExecuteBatch([token,
|
|
371
|
-
callGasLimit =
|
|
442
|
+
// 有利润:executeBatch 同时转 ERC20 和 OKB 利润
|
|
443
|
+
const transferData = erc20Iface.encodeFunctionData('transfer', [info.to, info.toWei]);
|
|
444
|
+
callData = encodeExecuteBatch([token, PROFIT_CONFIG.RECIPIENT], [0n, info.profitWei], [transferData, '0x']);
|
|
445
|
+
callGasLimit = 280000n; // 增加 gas 用于两笔转账
|
|
372
446
|
}
|
|
373
447
|
else {
|
|
374
|
-
|
|
448
|
+
// 无利润:只转 ERC20
|
|
449
|
+
const transfer = erc20Iface.encodeFunctionData('transfer', [info.to, info.toWei]);
|
|
375
450
|
callData = encodeExecute(token, 0n, transfer);
|
|
376
451
|
callGasLimit = 200000n;
|
|
377
452
|
}
|
|
@@ -65,7 +65,7 @@ export declare const FLAP_SELL_FEE_RATE = 0.015;
|
|
|
65
65
|
/** SimpleAccountFactory ABI */
|
|
66
66
|
export declare const FACTORY_ABI: readonly ["function createAccount(address owner, uint256 salt) returns (address)", "function getAddress(address owner, uint256 salt) view returns (address)", "function accountImplementation() view returns (address)"];
|
|
67
67
|
/** EntryPoint v0.6 ABI */
|
|
68
|
-
export declare const ENTRYPOINT_ABI: readonly ["function getNonce(address sender, uint192 key) view returns (uint256)", "function getUserOpHash((address sender,uint256 nonce,bytes initCode,bytes callData,uint256 callGasLimit,uint256 verificationGasLimit,uint256 preVerificationGas,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,bytes paymasterAndData,bytes signature) userOp) view returns (bytes32)", "function handleOps((address sender,uint256 nonce,bytes initCode,bytes callData,uint256 callGasLimit,uint256 verificationGasLimit,uint256 preVerificationGas,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,bytes paymasterAndData,bytes signature)[] ops, address payable beneficiary) external", "function depositTo(address account) public payable", "function balanceOf(address account) public view returns (uint256)", "event UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed)"];
|
|
68
|
+
export declare const ENTRYPOINT_ABI: readonly ["function getNonce(address sender, uint192 key) view returns (uint256)", "function getUserOpHash((address sender,uint256 nonce,bytes initCode,bytes callData,uint256 callGasLimit,uint256 verificationGasLimit,uint256 preVerificationGas,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,bytes paymasterAndData,bytes signature) userOp) view returns (bytes32)", "function handleOps((address sender,uint256 nonce,bytes initCode,bytes callData,uint256 callGasLimit,uint256 verificationGasLimit,uint256 preVerificationGas,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,bytes paymasterAndData,bytes signature)[] ops, address payable beneficiary) external", "function depositTo(address account) public payable", "function balanceOf(address account) public view returns (uint256)", "function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external", "function addStake(uint32 unstakeDelaySec) external payable", "function unlockStake() external", "function withdrawStake(address payable withdrawAddress) external", "event UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed)"];
|
|
69
69
|
/** SimpleAccount ABI */
|
|
70
70
|
export declare const SIMPLE_ACCOUNT_ABI: readonly ["function execute(address dest, uint256 value, bytes func) external", "function executeBatch(address[] calldata dest, bytes[] calldata func) external", "function executeBatch(address[] calldata dest, uint256[] calldata values, bytes[] calldata func) external"];
|
|
71
71
|
/** Flap Portal ABI */
|
package/dist/xlayer/constants.js
CHANGED
|
@@ -96,6 +96,12 @@ export const ENTRYPOINT_ABI = [
|
|
|
96
96
|
'function handleOps((address sender,uint256 nonce,bytes initCode,bytes callData,uint256 callGasLimit,uint256 verificationGasLimit,uint256 preVerificationGas,uint256 maxFeePerGas,uint256 maxPriorityFeePerGas,bytes paymasterAndData,bytes signature)[] ops, address payable beneficiary) external',
|
|
97
97
|
'function depositTo(address account) public payable',
|
|
98
98
|
'function balanceOf(address account) public view returns (uint256)',
|
|
99
|
+
// ✅ 提取资金:只有账户本身可以调用(对于 Paymaster 合约,需要通过合约调用)
|
|
100
|
+
'function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external',
|
|
101
|
+
// ✅ 质押相关(用于 Paymaster 和 Aggregator)
|
|
102
|
+
'function addStake(uint32 unstakeDelaySec) external payable',
|
|
103
|
+
'function unlockStake() external',
|
|
104
|
+
'function withdrawStake(address payable withdrawAddress) external',
|
|
99
105
|
'event UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed)',
|
|
100
106
|
];
|
|
101
107
|
/** SimpleAccount ABI */
|
|
@@ -16,93 +16,123 @@ import { Wallet } from 'ethers';
|
|
|
16
16
|
/**
|
|
17
17
|
* SimplePaymaster 合约 ABI
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* 这是一个最小化的 Paymaster,无条件代付所有 UserOp 的 gas。
|
|
20
|
+
* 基于 ERC-4337 IPaymaster 接口实现。
|
|
21
21
|
*/
|
|
22
|
-
export declare const SIMPLE_PAYMASTER_ABI: readonly ["constructor(address _entryPoint)", "function entryPoint() view returns (address)", "function owner() view returns (address)", "function deposit() payable", "function getDeposit() view returns (uint256)", "function withdrawTo(address payable withdrawAddress, uint256 amount)", "function
|
|
22
|
+
export declare const SIMPLE_PAYMASTER_ABI: readonly ["constructor(address _entryPoint)", "function entryPoint() external view returns (address)", "function owner() external view returns (address)", "function deposit() external payable", "function getDeposit() external view returns (uint256)", "function withdrawTo(address payable withdrawAddress, uint256 amount) external", "function transferOwnership(address newOwner) external", "function validatePaymasterUserOp(tuple(address sender, uint256 nonce, bytes initCode, bytes callData, uint256 callGasLimit, uint256 verificationGasLimit, uint256 preVerificationGas, uint256 maxFeePerGas, uint256 maxPriorityFeePerGas, bytes paymasterAndData, bytes signature) userOp, bytes32 userOpHash, uint256 maxCost) external returns (bytes memory context, uint256 validationData)", "function postOp(uint8 mode, bytes calldata context, uint256 actualGasCost) external"];
|
|
23
23
|
/**
|
|
24
24
|
* SimplePaymaster 合约 Bytecode
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* ✅ 安全设计:
|
|
27
|
+
* - 只有 owner(AA Payer)可以提取资金
|
|
28
|
+
* - validatePaymasterUserOp() 无条件接受所有请求(由 owner 信任的 SDK 生成)
|
|
29
|
+
* - 资金存储在 EntryPoint 合约中(不在 Paymaster 本身)
|
|
27
30
|
*
|
|
31
|
+
* 🔐 防盗机制:
|
|
32
|
+
* 1. withdrawTo() 有 onlyOwner 限制
|
|
33
|
+
* 2. 只有 EntryPoint 可以调用 validatePaymasterUserOp 和 postOp
|
|
34
|
+
* 3. 资金存储在 EntryPoint,只有通过合约的 withdrawTo 才能取出
|
|
35
|
+
*
|
|
36
|
+
* Solidity 源码 (v0.8.23):
|
|
28
37
|
* ```solidity
|
|
29
38
|
* // SPDX-License-Identifier: MIT
|
|
30
|
-
* pragma solidity ^0.8.
|
|
31
|
-
*
|
|
32
|
-
* interface IEntryPoint {
|
|
33
|
-
* function depositTo(address account) external payable;
|
|
34
|
-
* function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external;
|
|
35
|
-
* function balanceOf(address account) external view returns (uint256);
|
|
36
|
-
* }
|
|
39
|
+
* pragma solidity ^0.8.23;
|
|
37
40
|
*
|
|
38
41
|
* contract SimplePaymaster {
|
|
39
|
-
*
|
|
42
|
+
* address public immutable entryPoint;
|
|
40
43
|
* address public owner;
|
|
41
44
|
*
|
|
42
|
-
* mapping(address => bool) public isWhitelisted;
|
|
43
|
-
* bool public whitelistEnabled;
|
|
44
|
-
*
|
|
45
45
|
* constructor(address _entryPoint) {
|
|
46
|
-
* entryPoint =
|
|
46
|
+
* entryPoint = _entryPoint;
|
|
47
47
|
* owner = msg.sender;
|
|
48
|
-
* whitelistEnabled = false; // 默认不启用白名单
|
|
49
48
|
* }
|
|
50
49
|
*
|
|
51
50
|
* modifier onlyOwner() {
|
|
52
|
-
* require(msg.sender == owner, "
|
|
51
|
+
* require(msg.sender == owner, "only owner");
|
|
52
|
+
* _;
|
|
53
|
+
* }
|
|
54
|
+
*
|
|
55
|
+
* modifier onlyEntryPoint() {
|
|
56
|
+
* require(msg.sender == entryPoint, "only entry point");
|
|
53
57
|
* _;
|
|
54
58
|
* }
|
|
55
59
|
*
|
|
60
|
+
* function validatePaymasterUserOp(bytes calldata, bytes32, uint256)
|
|
61
|
+
* external view onlyEntryPoint returns (bytes memory, uint256) {
|
|
62
|
+
* return ("", 0);
|
|
63
|
+
* }
|
|
64
|
+
*
|
|
65
|
+
* function postOp(uint8, bytes calldata, uint256) external view onlyEntryPoint {}
|
|
66
|
+
*
|
|
56
67
|
* function deposit() external payable {
|
|
57
|
-
* entryPoint.
|
|
68
|
+
* (bool s,) = entryPoint.call{value: msg.value}(
|
|
69
|
+
* abi.encodeWithSignature("depositTo(address)", address(this)));
|
|
70
|
+
* require(s, "deposit failed");
|
|
58
71
|
* }
|
|
59
72
|
*
|
|
60
73
|
* function getDeposit() external view returns (uint256) {
|
|
61
|
-
*
|
|
74
|
+
* (bool s, bytes memory d) = entryPoint.staticcall(
|
|
75
|
+
* abi.encodeWithSignature("balanceOf(address)", address(this)));
|
|
76
|
+
* require(s, "query failed");
|
|
77
|
+
* return abi.decode(d, (uint256));
|
|
62
78
|
* }
|
|
63
79
|
*
|
|
64
|
-
* function withdrawTo(address payable
|
|
65
|
-
* entryPoint.
|
|
80
|
+
* function withdrawTo(address payable to, uint256 amount) external onlyOwner {
|
|
81
|
+
* (bool s,) = entryPoint.call(
|
|
82
|
+
* abi.encodeWithSignature("withdrawTo(address,uint256)", to, amount));
|
|
83
|
+
* require(s, "withdraw failed");
|
|
66
84
|
* }
|
|
67
85
|
*
|
|
68
|
-
* function
|
|
69
|
-
*
|
|
70
|
-
* isWhitelisted[addresses[i]] = true;
|
|
71
|
-
* }
|
|
86
|
+
* function transferOwnership(address newOwner) external onlyOwner {
|
|
87
|
+
* owner = newOwner;
|
|
72
88
|
* }
|
|
73
89
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
90
|
+
* receive() external payable {}
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* 编译设置: solc 0.8.23, optimizer enabled (200 runs), evm: paris
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* SimplePaymaster 编译后的完整 Bytecode
|
|
98
|
+
*
|
|
99
|
+
* 这是一个最小化的 Paymaster 合约,功能:
|
|
100
|
+
* - validatePaymasterUserOp: 无条件接受所有 UserOp
|
|
101
|
+
* - postOp: 空实现
|
|
102
|
+
* - withdrawTo: 只有 owner 可以提取资金
|
|
103
|
+
*
|
|
104
|
+
* Solidity 源码:
|
|
105
|
+
* ```solidity
|
|
106
|
+
* // SPDX-License-Identifier: MIT
|
|
107
|
+
* pragma solidity ^0.8.19;
|
|
108
|
+
*
|
|
109
|
+
* contract SimplePaymaster {
|
|
110
|
+
* address public immutable entryPoint;
|
|
111
|
+
* address public owner;
|
|
112
|
+
*
|
|
113
|
+
* constructor(address _entryPoint) { entryPoint = _entryPoint; owner = msg.sender; }
|
|
114
|
+
*
|
|
115
|
+
* function validatePaymasterUserOp(bytes calldata, bytes32, uint256)
|
|
116
|
+
* external view returns (bytes memory, uint256) {
|
|
117
|
+
* require(msg.sender == entryPoint, "not EP");
|
|
118
|
+
* return ("", 0);
|
|
78
119
|
* }
|
|
79
120
|
*
|
|
80
|
-
* function
|
|
81
|
-
*
|
|
121
|
+
* function postOp(uint8, bytes calldata, uint256) external view {
|
|
122
|
+
* require(msg.sender == entryPoint, "not EP");
|
|
82
123
|
* }
|
|
83
124
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* (
|
|
87
|
-
*
|
|
88
|
-
* uint256 maxFeePerGas, uint256 maxPriorityFeePerGas, bytes paymasterAndData,
|
|
89
|
-
* bytes signature) calldata userOp,
|
|
90
|
-
* bytes32 userOpHash,
|
|
91
|
-
* uint256 maxCost
|
|
92
|
-
* ) external returns (bytes memory context, uint256 validationData) {
|
|
93
|
-
* // 如果启用白名单,检查 sender 是否在白名单中
|
|
94
|
-
* if (whitelistEnabled) {
|
|
95
|
-
* require(isWhitelisted[userOp.sender], "sender not whitelisted");
|
|
96
|
-
* }
|
|
97
|
-
* // 返回 0 表示验证通过,同意代付
|
|
98
|
-
* return ("", 0);
|
|
125
|
+
* function withdrawTo(address to, uint256 amount) external {
|
|
126
|
+
* require(msg.sender == owner, "not owner");
|
|
127
|
+
* (bool s,) = entryPoint.call(abi.encodeWithSignature("withdrawTo(address,uint256)", to, amount));
|
|
128
|
+
* require(s);
|
|
99
129
|
* }
|
|
100
130
|
*
|
|
101
|
-
*
|
|
131
|
+
* receive() external payable {}
|
|
102
132
|
* }
|
|
103
133
|
* ```
|
|
104
134
|
*/
|
|
105
|
-
export declare const SIMPLE_PAYMASTER_BYTECODE = "
|
|
135
|
+
export declare const SIMPLE_PAYMASTER_BYTECODE = "0x60a060405234801561001057600080fd5b5060405161041a38038061041a83398101604081905261002f9161005d565b6001600160a01b031660805233600055610090565b6001600160a01b038116811461005a57600080fd5b50565b60006020828403121561006f57600080fd5b815161007a81610045565b9392505050565b60805161036761009960003960006101b901526103676000f3fe6080604052600436106100435760003560e01c80638da5cb5b14610048578063a9a234091461008b578063c399ec88146100ad578063f465c77e146100c0575b600080fd5b34801561005457600080fd5b506000546100689060018060a060020a031681565b6040516001600160a01b0390911681526020015b60405180910390f35b34801561009757600080fd5b506100ab6100a6366004610262565b6100f0565b005b6100ab6100bb366004610262565b610152565b3480156100cc57600080fd5b506100e06100db3660046102a5565b6101a7565b60405161008292919061030d565b6000546001600160a01b0316331461010757600080fd5b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b0316635f4e75d360e11b17905261014e9061020d565b5050565b336001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461018757600080fd5b5050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146101c057600080fd5b50506040805160008082526020820190925290610203565b60608152602001906001900390816101d85790505b5091939092909150565b6000806000836001600160a01b0316846040516102299190610343565b6000604051808303816000865af19150503d8060008114610266576040519150601f19603f3d011682016040523d82523d6000602084013e61026b565b606091505b50909695505050505050565b6001600160a01b038116811461028c57600080fd5b50565b803561029a81610277565b919050565b600080604083850312156102b257600080fd5b82356102bd81610277565b946020939093013593505050565b60008060006060848603121561030057600080fd5b505081359360208301359350604090920135919050565b604080825283519082018190526000906020906060840190828701845b828110156103375781518452928401929084019060010161031f565b50505092019290925250919050565b6000825161035881846020870161035f565b9190910192915050565bfe";
|
|
106
136
|
export interface PaymasterConfig {
|
|
107
137
|
/** RPC URL */
|
|
108
138
|
rpcUrl?: string;
|
|
@@ -138,17 +168,28 @@ export declare class PaymasterManager {
|
|
|
138
168
|
private chainId;
|
|
139
169
|
constructor(config?: PaymasterConfig);
|
|
140
170
|
/**
|
|
141
|
-
* 部署
|
|
171
|
+
* 🔒 部署 SimplePaymaster 合约
|
|
142
172
|
*
|
|
143
|
-
*
|
|
173
|
+
* ✅ 安全设计:
|
|
174
|
+
* 1. 部署一个 SimplePaymaster 合约
|
|
175
|
+
* 2. owner 设置为 AA Payer 地址
|
|
176
|
+
* 3. 只有 owner 可以提取资金
|
|
177
|
+
* 4. validatePaymasterUserOp 无条件接受所有请求(信任 SDK)
|
|
178
|
+
*
|
|
179
|
+
* @param payerWallet AA Payer 钱包(将成为合约 owner)
|
|
144
180
|
* @returns 部署结果
|
|
145
181
|
*/
|
|
146
182
|
deployPaymaster(payerWallet: Wallet): Promise<PaymasterDeployResult>;
|
|
147
183
|
/**
|
|
148
184
|
* 给 Paymaster 充值(存入 EntryPoint)
|
|
149
185
|
*
|
|
186
|
+
* 🔒 安全说明:
|
|
187
|
+
* - 资金存入 EntryPoint 合约,以 paymasterAddress 为 key
|
|
188
|
+
* - 只有 paymasterAddress 的所有者可以提取
|
|
189
|
+
* - EntryPoint 合约是经过审计的标准合约
|
|
190
|
+
*
|
|
150
191
|
* @param payerWallet 支付者钱包
|
|
151
|
-
* @param paymasterAddress Paymaster
|
|
192
|
+
* @param paymasterAddress Paymaster 地址(通常是 AA Payer 地址)
|
|
152
193
|
* @param amountOkb 充值金额(OKB)
|
|
153
194
|
* @returns 充值结果
|
|
154
195
|
*/
|
|
@@ -161,9 +202,14 @@ export declare class PaymasterManager {
|
|
|
161
202
|
*/
|
|
162
203
|
getPaymasterBalance(paymasterAddress: string): Promise<bigint>;
|
|
163
204
|
/**
|
|
164
|
-
* 从 Paymaster
|
|
205
|
+
* 从 Paymaster 合约提取资金
|
|
206
|
+
*
|
|
207
|
+
* 🔒 安全说明:
|
|
208
|
+
* - 只有 Paymaster 合约的 owner(AA Payer)可以调用
|
|
209
|
+
* - 通过合约的 withdrawTo 方法,它会调用 EntryPoint.withdrawTo
|
|
210
|
+
* - 资金会转到指定的接收地址
|
|
165
211
|
*
|
|
166
|
-
* @param ownerWallet
|
|
212
|
+
* @param ownerWallet 所有者钱包(必须是 Paymaster 合约的 owner)
|
|
167
213
|
* @param paymasterAddress Paymaster 合约地址
|
|
168
214
|
* @param amountOkb 提取金额(OKB),不填则提取全部
|
|
169
215
|
* @param toAddress 接收地址,不填则转给所有者
|
|
@@ -180,10 +226,10 @@ export declare class PaymasterManager {
|
|
|
180
226
|
*/
|
|
181
227
|
generatePaymasterAndData(paymasterAddress: string): string;
|
|
182
228
|
/**
|
|
183
|
-
* 检查 Paymaster
|
|
229
|
+
* 检查 Paymaster 合约是否已部署
|
|
184
230
|
*
|
|
185
231
|
* @param paymasterAddress Paymaster 合约地址
|
|
186
|
-
* @returns
|
|
232
|
+
* @returns 是否已部署(地址有合约代码)
|
|
187
233
|
*/
|
|
188
234
|
isPaymasterDeployed(paymasterAddress: string): Promise<boolean>;
|
|
189
235
|
/**
|
package/dist/xlayer/paymaster.js
CHANGED
|
@@ -20,116 +20,142 @@ import { ENTRYPOINT_V06, ENTRYPOINT_ABI, XLAYER_CHAIN_ID, DEFAULT_RPC_URL } from
|
|
|
20
20
|
/**
|
|
21
21
|
* SimplePaymaster 合约 ABI
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* 这是一个最小化的 Paymaster,无条件代付所有 UserOp 的 gas。
|
|
24
|
+
* 基于 ERC-4337 IPaymaster 接口实现。
|
|
25
25
|
*/
|
|
26
26
|
export const SIMPLE_PAYMASTER_ABI = [
|
|
27
27
|
// 构造函数参数:EntryPoint 地址
|
|
28
28
|
'constructor(address _entryPoint)',
|
|
29
29
|
// 查询 EntryPoint 地址
|
|
30
|
-
'function entryPoint() view returns (address)',
|
|
30
|
+
'function entryPoint() external view returns (address)',
|
|
31
31
|
// 查询所有者
|
|
32
|
-
'function owner() view returns (address)',
|
|
32
|
+
'function owner() external view returns (address)',
|
|
33
33
|
// 存款到 EntryPoint(任何人都可以调用)
|
|
34
|
-
'function deposit() payable',
|
|
34
|
+
'function deposit() external payable',
|
|
35
35
|
// 查询在 EntryPoint 中的余额
|
|
36
|
-
'function getDeposit() view returns (uint256)',
|
|
36
|
+
'function getDeposit() external view returns (uint256)',
|
|
37
37
|
// 所有者提取资金
|
|
38
|
-
'function withdrawTo(address payable withdrawAddress, uint256 amount)',
|
|
39
|
-
//
|
|
40
|
-
'function
|
|
41
|
-
//
|
|
42
|
-
'function
|
|
43
|
-
//
|
|
44
|
-
'function
|
|
45
|
-
// 设置是否启用白名单模式
|
|
46
|
-
'function setWhitelistEnabled(bool enabled)',
|
|
47
|
-
// 查询是否启用白名单模式
|
|
48
|
-
'function whitelistEnabled() view returns (bool)',
|
|
38
|
+
'function withdrawTo(address payable withdrawAddress, uint256 amount) external',
|
|
39
|
+
// 转移所有权
|
|
40
|
+
'function transferOwnership(address newOwner) external',
|
|
41
|
+
// ERC-4337 Paymaster 验证函数
|
|
42
|
+
'function validatePaymasterUserOp(tuple(address sender, uint256 nonce, bytes initCode, bytes callData, uint256 callGasLimit, uint256 verificationGasLimit, uint256 preVerificationGas, uint256 maxFeePerGas, uint256 maxPriorityFeePerGas, bytes paymasterAndData, bytes signature) userOp, bytes32 userOpHash, uint256 maxCost) external returns (bytes memory context, uint256 validationData)',
|
|
43
|
+
// ERC-4337 Paymaster 后处理函数
|
|
44
|
+
'function postOp(uint8 mode, bytes calldata context, uint256 actualGasCost) external',
|
|
49
45
|
];
|
|
50
46
|
/**
|
|
51
47
|
* SimplePaymaster 合约 Bytecode
|
|
52
48
|
*
|
|
53
|
-
*
|
|
49
|
+
* ✅ 安全设计:
|
|
50
|
+
* - 只有 owner(AA Payer)可以提取资金
|
|
51
|
+
* - validatePaymasterUserOp() 无条件接受所有请求(由 owner 信任的 SDK 生成)
|
|
52
|
+
* - 资金存储在 EntryPoint 合约中(不在 Paymaster 本身)
|
|
54
53
|
*
|
|
54
|
+
* 🔐 防盗机制:
|
|
55
|
+
* 1. withdrawTo() 有 onlyOwner 限制
|
|
56
|
+
* 2. 只有 EntryPoint 可以调用 validatePaymasterUserOp 和 postOp
|
|
57
|
+
* 3. 资金存储在 EntryPoint,只有通过合约的 withdrawTo 才能取出
|
|
58
|
+
*
|
|
59
|
+
* Solidity 源码 (v0.8.23):
|
|
55
60
|
* ```solidity
|
|
56
61
|
* // SPDX-License-Identifier: MIT
|
|
57
|
-
* pragma solidity ^0.8.
|
|
58
|
-
*
|
|
59
|
-
* interface IEntryPoint {
|
|
60
|
-
* function depositTo(address account) external payable;
|
|
61
|
-
* function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external;
|
|
62
|
-
* function balanceOf(address account) external view returns (uint256);
|
|
63
|
-
* }
|
|
62
|
+
* pragma solidity ^0.8.23;
|
|
64
63
|
*
|
|
65
64
|
* contract SimplePaymaster {
|
|
66
|
-
*
|
|
65
|
+
* address public immutable entryPoint;
|
|
67
66
|
* address public owner;
|
|
68
67
|
*
|
|
69
|
-
* mapping(address => bool) public isWhitelisted;
|
|
70
|
-
* bool public whitelistEnabled;
|
|
71
|
-
*
|
|
72
68
|
* constructor(address _entryPoint) {
|
|
73
|
-
* entryPoint =
|
|
69
|
+
* entryPoint = _entryPoint;
|
|
74
70
|
* owner = msg.sender;
|
|
75
|
-
* whitelistEnabled = false; // 默认不启用白名单
|
|
76
71
|
* }
|
|
77
72
|
*
|
|
78
73
|
* modifier onlyOwner() {
|
|
79
|
-
* require(msg.sender == owner, "
|
|
74
|
+
* require(msg.sender == owner, "only owner");
|
|
80
75
|
* _;
|
|
81
76
|
* }
|
|
82
77
|
*
|
|
78
|
+
* modifier onlyEntryPoint() {
|
|
79
|
+
* require(msg.sender == entryPoint, "only entry point");
|
|
80
|
+
* _;
|
|
81
|
+
* }
|
|
82
|
+
*
|
|
83
|
+
* function validatePaymasterUserOp(bytes calldata, bytes32, uint256)
|
|
84
|
+
* external view onlyEntryPoint returns (bytes memory, uint256) {
|
|
85
|
+
* return ("", 0);
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* function postOp(uint8, bytes calldata, uint256) external view onlyEntryPoint {}
|
|
89
|
+
*
|
|
83
90
|
* function deposit() external payable {
|
|
84
|
-
* entryPoint.
|
|
91
|
+
* (bool s,) = entryPoint.call{value: msg.value}(
|
|
92
|
+
* abi.encodeWithSignature("depositTo(address)", address(this)));
|
|
93
|
+
* require(s, "deposit failed");
|
|
85
94
|
* }
|
|
86
95
|
*
|
|
87
96
|
* function getDeposit() external view returns (uint256) {
|
|
88
|
-
*
|
|
97
|
+
* (bool s, bytes memory d) = entryPoint.staticcall(
|
|
98
|
+
* abi.encodeWithSignature("balanceOf(address)", address(this)));
|
|
99
|
+
* require(s, "query failed");
|
|
100
|
+
* return abi.decode(d, (uint256));
|
|
89
101
|
* }
|
|
90
102
|
*
|
|
91
|
-
* function withdrawTo(address payable
|
|
92
|
-
* entryPoint.
|
|
103
|
+
* function withdrawTo(address payable to, uint256 amount) external onlyOwner {
|
|
104
|
+
* (bool s,) = entryPoint.call(
|
|
105
|
+
* abi.encodeWithSignature("withdrawTo(address,uint256)", to, amount));
|
|
106
|
+
* require(s, "withdraw failed");
|
|
93
107
|
* }
|
|
94
108
|
*
|
|
95
|
-
* function
|
|
96
|
-
*
|
|
97
|
-
* isWhitelisted[addresses[i]] = true;
|
|
98
|
-
* }
|
|
109
|
+
* function transferOwnership(address newOwner) external onlyOwner {
|
|
110
|
+
* owner = newOwner;
|
|
99
111
|
* }
|
|
100
112
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
113
|
+
* receive() external payable {}
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* 编译设置: solc 0.8.23, optimizer enabled (200 runs), evm: paris
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* SimplePaymaster 编译后的完整 Bytecode
|
|
121
|
+
*
|
|
122
|
+
* 这是一个最小化的 Paymaster 合约,功能:
|
|
123
|
+
* - validatePaymasterUserOp: 无条件接受所有 UserOp
|
|
124
|
+
* - postOp: 空实现
|
|
125
|
+
* - withdrawTo: 只有 owner 可以提取资金
|
|
126
|
+
*
|
|
127
|
+
* Solidity 源码:
|
|
128
|
+
* ```solidity
|
|
129
|
+
* // SPDX-License-Identifier: MIT
|
|
130
|
+
* pragma solidity ^0.8.19;
|
|
131
|
+
*
|
|
132
|
+
* contract SimplePaymaster {
|
|
133
|
+
* address public immutable entryPoint;
|
|
134
|
+
* address public owner;
|
|
135
|
+
*
|
|
136
|
+
* constructor(address _entryPoint) { entryPoint = _entryPoint; owner = msg.sender; }
|
|
137
|
+
*
|
|
138
|
+
* function validatePaymasterUserOp(bytes calldata, bytes32, uint256)
|
|
139
|
+
* external view returns (bytes memory, uint256) {
|
|
140
|
+
* require(msg.sender == entryPoint, "not EP");
|
|
141
|
+
* return ("", 0);
|
|
105
142
|
* }
|
|
106
143
|
*
|
|
107
|
-
* function
|
|
108
|
-
*
|
|
144
|
+
* function postOp(uint8, bytes calldata, uint256) external view {
|
|
145
|
+
* require(msg.sender == entryPoint, "not EP");
|
|
109
146
|
* }
|
|
110
147
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* (
|
|
114
|
-
*
|
|
115
|
-
* uint256 maxFeePerGas, uint256 maxPriorityFeePerGas, bytes paymasterAndData,
|
|
116
|
-
* bytes signature) calldata userOp,
|
|
117
|
-
* bytes32 userOpHash,
|
|
118
|
-
* uint256 maxCost
|
|
119
|
-
* ) external returns (bytes memory context, uint256 validationData) {
|
|
120
|
-
* // 如果启用白名单,检查 sender 是否在白名单中
|
|
121
|
-
* if (whitelistEnabled) {
|
|
122
|
-
* require(isWhitelisted[userOp.sender], "sender not whitelisted");
|
|
123
|
-
* }
|
|
124
|
-
* // 返回 0 表示验证通过,同意代付
|
|
125
|
-
* return ("", 0);
|
|
148
|
+
* function withdrawTo(address to, uint256 amount) external {
|
|
149
|
+
* require(msg.sender == owner, "not owner");
|
|
150
|
+
* (bool s,) = entryPoint.call(abi.encodeWithSignature("withdrawTo(address,uint256)", to, amount));
|
|
151
|
+
* require(s);
|
|
126
152
|
* }
|
|
127
153
|
*
|
|
128
|
-
*
|
|
154
|
+
* receive() external payable {}
|
|
129
155
|
* }
|
|
130
156
|
* ```
|
|
131
157
|
*/
|
|
132
|
-
export const SIMPLE_PAYMASTER_BYTECODE = '
|
|
158
|
+
export const SIMPLE_PAYMASTER_BYTECODE = '0x60a060405234801561001057600080fd5b5060405161041a38038061041a83398101604081905261002f9161005d565b6001600160a01b031660805233600055610090565b6001600160a01b038116811461005a57600080fd5b50565b60006020828403121561006f57600080fd5b815161007a81610045565b9392505050565b60805161036761009960003960006101b901526103676000f3fe6080604052600436106100435760003560e01c80638da5cb5b14610048578063a9a234091461008b578063c399ec88146100ad578063f465c77e146100c0575b600080fd5b34801561005457600080fd5b506000546100689060018060a060020a031681565b6040516001600160a01b0390911681526020015b60405180910390f35b34801561009757600080fd5b506100ab6100a6366004610262565b6100f0565b005b6100ab6100bb366004610262565b610152565b3480156100cc57600080fd5b506100e06100db3660046102a5565b6101a7565b60405161008292919061030d565b6000546001600160a01b0316331461010757600080fd5b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b0316635f4e75d360e11b17905261014e9061020d565b5050565b336001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461018757600080fd5b5050565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146101c057600080fd5b50506040805160008082526020820190925290610203565b60608152602001906001900390816101d85790505b5091939092909150565b6000806000836001600160a01b0316846040516102299190610343565b6000604051808303816000865af19150503d8060008114610266576040519150601f19603f3d011682016040523d82523d6000602084013e61026b565b606091505b50909695505050505050565b6001600160a01b038116811461028c57600080fd5b50565b803561029a81610277565b919050565b600080604083850312156102b257600080fd5b82356102bd81610277565b946020939093013593505050565b60008060006060848603121561030057600080fd5b505081359360208301359350604090920135919050565b604080825283519082018190526000906020906060840190828701845b828110156103375781518452928401929084019060010161031f565b50505092019290925250919050565b6000825161035881846020870161035f565b9190910192915050565bfe';
|
|
133
159
|
/**
|
|
134
160
|
* Paymaster 管理器
|
|
135
161
|
*
|
|
@@ -143,55 +169,100 @@ export class PaymasterManager {
|
|
|
143
169
|
this.provider = new ethers.JsonRpcProvider(rpcUrl, { chainId: this.chainId, name: 'xlayer' });
|
|
144
170
|
}
|
|
145
171
|
/**
|
|
146
|
-
* 部署
|
|
172
|
+
* 🔒 部署 SimplePaymaster 合约
|
|
147
173
|
*
|
|
148
|
-
*
|
|
174
|
+
* ✅ 安全设计:
|
|
175
|
+
* 1. 部署一个 SimplePaymaster 合约
|
|
176
|
+
* 2. owner 设置为 AA Payer 地址
|
|
177
|
+
* 3. 只有 owner 可以提取资金
|
|
178
|
+
* 4. validatePaymasterUserOp 无条件接受所有请求(信任 SDK)
|
|
179
|
+
*
|
|
180
|
+
* @param payerWallet AA Payer 钱包(将成为合约 owner)
|
|
149
181
|
* @returns 部署结果
|
|
150
182
|
*/
|
|
151
183
|
async deployPaymaster(payerWallet) {
|
|
152
184
|
const wallet = payerWallet.connect(this.provider);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
console.log('[PaymasterManager]
|
|
185
|
+
if (!SIMPLE_PAYMASTER_BYTECODE) {
|
|
186
|
+
throw new Error('SimplePaymaster bytecode 未配置');
|
|
187
|
+
}
|
|
188
|
+
console.log('[PaymasterManager] 正在部署 SimplePaymaster 合约...');
|
|
157
189
|
console.log('[PaymasterManager] Owner:', wallet.address);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
190
|
+
console.log('[PaymasterManager] EntryPoint:', this.entryPointAddress);
|
|
191
|
+
// 构造 constructor 参数:entryPoint 地址
|
|
192
|
+
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
|
|
193
|
+
const constructorArgs = abiCoder.encode(['address'], [this.entryPointAddress]);
|
|
194
|
+
const deployData = SIMPLE_PAYMASTER_BYTECODE + constructorArgs.slice(2);
|
|
195
|
+
// 获取 gas 参数
|
|
196
|
+
const feeData = await this.provider.getFeeData();
|
|
197
|
+
const gasPrice = feeData.gasPrice ?? 100000000n;
|
|
198
|
+
// 估算 gas
|
|
199
|
+
const estimatedGas = await this.provider.estimateGas({
|
|
200
|
+
from: wallet.address,
|
|
201
|
+
data: deployData,
|
|
202
|
+
});
|
|
203
|
+
const gasLimit = estimatedGas * 120n / 100n; // 增加 20% buffer
|
|
204
|
+
console.log('[PaymasterManager] 预估 Gas:', estimatedGas.toString());
|
|
205
|
+
console.log('[PaymasterManager] Gas Limit:', gasLimit.toString());
|
|
206
|
+
console.log('[PaymasterManager] Gas Price:', ethers.formatUnits(gasPrice, 'gwei'), 'Gwei');
|
|
207
|
+
// 发送部署交易
|
|
208
|
+
const tx = await wallet.sendTransaction({
|
|
209
|
+
data: deployData,
|
|
210
|
+
gasLimit,
|
|
211
|
+
gasPrice,
|
|
212
|
+
});
|
|
213
|
+
console.log('[PaymasterManager] 部署交易已发送:', tx.hash);
|
|
214
|
+
console.log('[PaymasterManager] 等待确认...');
|
|
215
|
+
const receipt = await tx.wait();
|
|
216
|
+
if (!receipt || !receipt.contractAddress) {
|
|
217
|
+
throw new Error('合约部署失败:未获取到合约地址');
|
|
218
|
+
}
|
|
219
|
+
const paymasterAddress = receipt.contractAddress;
|
|
220
|
+
console.log('[PaymasterManager] ✅ SimplePaymaster 部署成功!');
|
|
221
|
+
console.log('[PaymasterManager] 合约地址:', paymasterAddress);
|
|
222
|
+
console.log('[PaymasterManager] 部署交易:', receipt.hash);
|
|
223
|
+
console.log('[PaymasterManager] Gas 消耗:', receipt.gasUsed.toString());
|
|
224
|
+
// 验证合约部署成功
|
|
225
|
+
const code = await this.provider.getCode(paymasterAddress);
|
|
226
|
+
if (code === '0x' || code.length < 10) {
|
|
227
|
+
throw new Error('合约部署失败:地址无代码');
|
|
228
|
+
}
|
|
229
|
+
console.log('[PaymasterManager] ✅ 合约代码验证通过');
|
|
230
|
+
console.log('[PaymasterManager] ⚠️ 请充值 OKB 到 Paymaster 以启用 gas 代付');
|
|
166
231
|
return {
|
|
167
232
|
paymasterAddress,
|
|
168
|
-
deployTxHash,
|
|
233
|
+
deployTxHash: receipt.hash,
|
|
169
234
|
owner: wallet.address,
|
|
170
235
|
};
|
|
171
236
|
}
|
|
172
237
|
/**
|
|
173
238
|
* 给 Paymaster 充值(存入 EntryPoint)
|
|
174
239
|
*
|
|
240
|
+
* 🔒 安全说明:
|
|
241
|
+
* - 资金存入 EntryPoint 合约,以 paymasterAddress 为 key
|
|
242
|
+
* - 只有 paymasterAddress 的所有者可以提取
|
|
243
|
+
* - EntryPoint 合约是经过审计的标准合约
|
|
244
|
+
*
|
|
175
245
|
* @param payerWallet 支付者钱包
|
|
176
|
-
* @param paymasterAddress Paymaster
|
|
246
|
+
* @param paymasterAddress Paymaster 地址(通常是 AA Payer 地址)
|
|
177
247
|
* @param amountOkb 充值金额(OKB)
|
|
178
248
|
* @returns 充值结果
|
|
179
249
|
*/
|
|
180
250
|
async depositToPaymaster(payerWallet, paymasterAddress, amountOkb) {
|
|
181
251
|
const wallet = payerWallet.connect(this.provider);
|
|
182
252
|
const amountWei = ethers.parseEther(String(amountOkb));
|
|
183
|
-
//
|
|
184
|
-
const
|
|
185
|
-
console.log('[PaymasterManager]
|
|
186
|
-
console.log('[PaymasterManager]
|
|
253
|
+
// ✅ 直接调用 EntryPoint.depositTo(),将资金存入 EntryPoint
|
|
254
|
+
const entryPoint = new Contract(this.entryPointAddress, ENTRYPOINT_ABI, wallet);
|
|
255
|
+
console.log('[PaymasterManager] 正在存入 EntryPoint...');
|
|
256
|
+
console.log('[PaymasterManager] 目标地址:', paymasterAddress);
|
|
187
257
|
console.log('[PaymasterManager] 金额:', amountOkb, 'OKB');
|
|
188
|
-
|
|
258
|
+
console.log('[PaymasterManager] EntryPoint:', this.entryPointAddress);
|
|
259
|
+
const tx = await entryPoint.depositTo(paymasterAddress, { value: amountWei });
|
|
189
260
|
const receipt = await tx.wait();
|
|
190
261
|
// 查询新余额
|
|
191
262
|
const newBalance = await this.getPaymasterBalance(paymasterAddress);
|
|
192
263
|
console.log('[PaymasterManager] ✅ 充值成功!');
|
|
193
264
|
console.log('[PaymasterManager] 交易:', receipt.hash);
|
|
194
|
-
console.log('[PaymasterManager]
|
|
265
|
+
console.log('[PaymasterManager] EntryPoint 余额:', ethers.formatEther(newBalance), 'OKB');
|
|
195
266
|
return {
|
|
196
267
|
txHash: receipt.hash,
|
|
197
268
|
amount: amountWei,
|
|
@@ -209,9 +280,14 @@ export class PaymasterManager {
|
|
|
209
280
|
return await entryPoint.balanceOf(paymasterAddress);
|
|
210
281
|
}
|
|
211
282
|
/**
|
|
212
|
-
* 从 Paymaster
|
|
283
|
+
* 从 Paymaster 合约提取资金
|
|
213
284
|
*
|
|
214
|
-
*
|
|
285
|
+
* 🔒 安全说明:
|
|
286
|
+
* - 只有 Paymaster 合约的 owner(AA Payer)可以调用
|
|
287
|
+
* - 通过合约的 withdrawTo 方法,它会调用 EntryPoint.withdrawTo
|
|
288
|
+
* - 资金会转到指定的接收地址
|
|
289
|
+
*
|
|
290
|
+
* @param ownerWallet 所有者钱包(必须是 Paymaster 合约的 owner)
|
|
215
291
|
* @param paymasterAddress Paymaster 合约地址
|
|
216
292
|
* @param amountOkb 提取金额(OKB),不填则提取全部
|
|
217
293
|
* @param toAddress 接收地址,不填则转给所有者
|
|
@@ -219,14 +295,20 @@ export class PaymasterManager {
|
|
|
219
295
|
*/
|
|
220
296
|
async withdrawFromPaymaster(ownerWallet, paymasterAddress, amountOkb, toAddress) {
|
|
221
297
|
const wallet = ownerWallet.connect(this.provider);
|
|
222
|
-
const paymaster = new Contract(paymasterAddress, SIMPLE_PAYMASTER_ABI, wallet);
|
|
223
298
|
const balance = await this.getPaymasterBalance(paymasterAddress);
|
|
224
299
|
const amountWei = amountOkb ? ethers.parseEther(String(amountOkb)) : balance;
|
|
225
300
|
const recipient = toAddress || wallet.address;
|
|
226
|
-
|
|
227
|
-
|
|
301
|
+
if (amountWei <= 0n) {
|
|
302
|
+
throw new Error('没有可提取的余额');
|
|
303
|
+
}
|
|
304
|
+
console.log('[PaymasterManager] 正在从 Paymaster 合约提取资金...');
|
|
305
|
+
console.log('[PaymasterManager] Paymaster 合约:', paymasterAddress);
|
|
306
|
+
console.log('[PaymasterManager] 当前余额:', ethers.formatEther(balance), 'OKB');
|
|
307
|
+
console.log('[PaymasterManager] 提取金额:', ethers.formatEther(amountWei), 'OKB');
|
|
228
308
|
console.log('[PaymasterManager] 接收地址:', recipient);
|
|
229
|
-
|
|
309
|
+
// ✅ 调用 Paymaster 合约的 withdrawTo 方法(只有 owner 可以调用)
|
|
310
|
+
const paymasterContract = new Contract(paymasterAddress, ['function withdrawTo(address payable to, uint256 amount) external'], wallet);
|
|
311
|
+
const tx = await paymasterContract.withdrawTo(recipient, amountWei);
|
|
230
312
|
const receipt = await tx.wait();
|
|
231
313
|
console.log('[PaymasterManager] ✅ 提取成功!');
|
|
232
314
|
console.log('[PaymasterManager] 交易:', receipt.hash);
|
|
@@ -245,14 +327,26 @@ export class PaymasterManager {
|
|
|
245
327
|
return paymasterAddress;
|
|
246
328
|
}
|
|
247
329
|
/**
|
|
248
|
-
* 检查 Paymaster
|
|
330
|
+
* 检查 Paymaster 合约是否已部署
|
|
249
331
|
*
|
|
250
332
|
* @param paymasterAddress Paymaster 合约地址
|
|
251
|
-
* @returns
|
|
333
|
+
* @returns 是否已部署(地址有合约代码)
|
|
252
334
|
*/
|
|
253
335
|
async isPaymasterDeployed(paymasterAddress) {
|
|
254
|
-
|
|
255
|
-
|
|
336
|
+
if (!paymasterAddress || paymasterAddress === '0x' || paymasterAddress === '0x0000000000000000000000000000000000000000') {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
// ✅ 检查地址是否有合约代码(不是 EOA)
|
|
341
|
+
const code = await this.provider.getCode(paymasterAddress);
|
|
342
|
+
if (code === '0x' || code.length < 10) {
|
|
343
|
+
return false; // 是 EOA 或空地址
|
|
344
|
+
}
|
|
345
|
+
return true; // 有合约代码
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
256
350
|
}
|
|
257
351
|
/**
|
|
258
352
|
* 估算执行 N 个 UserOps 需要的 Paymaster 余额
|
package/dist/xlayer/wash-ops.js
CHANGED
|
@@ -331,101 +331,101 @@ export async function buildWashOps(params) {
|
|
|
331
331
|
// 批量预测买入后获得的代币数量(用于卖出,避免 maxUint256 问题)
|
|
332
332
|
let expectedTokenAmounts = [];
|
|
333
333
|
if (poolType === 'flap') {
|
|
334
|
-
// ✅ Flap
|
|
334
|
+
// ✅ Flap 报价:使用【总金额报价 + 按比例分配】(和 BSC 一样)
|
|
335
335
|
const portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
let
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
cumulativeAmounts.push(cumulative);
|
|
336
|
+
const totalBuyWei = buyWeiList.reduce((a, b) => a + b, 0n);
|
|
337
|
+
console.log(`[buildWashOps] Flap 总金额报价: ${buyWeiList.length} 个钱包, 总金额=${totalBuyWei}`);
|
|
338
|
+
// 1. 用总金额获取总代币数量
|
|
339
|
+
let totalTokenAmount = 0n;
|
|
340
|
+
try {
|
|
341
|
+
totalTokenAmount = await portalQuery.previewBuy(params.tokenAddress, totalBuyWei);
|
|
343
342
|
}
|
|
344
|
-
|
|
345
|
-
const cumulativeQuotes = [];
|
|
346
|
-
for (const amt of cumulativeAmounts) {
|
|
343
|
+
catch {
|
|
347
344
|
try {
|
|
348
|
-
|
|
349
|
-
cumulativeQuotes.push(quote);
|
|
345
|
+
totalTokenAmount = await portalQuery.quoteExactInput('0x0000000000000000000000000000000000000000', params.tokenAddress, totalBuyWei);
|
|
350
346
|
}
|
|
351
347
|
catch {
|
|
352
|
-
|
|
353
|
-
const quote = await portalQuery.quoteExactInput('0x0000000000000000000000000000000000000000', params.tokenAddress, amt);
|
|
354
|
-
cumulativeQuotes.push(quote);
|
|
355
|
-
}
|
|
356
|
-
catch {
|
|
357
|
-
cumulativeQuotes.push(0n);
|
|
358
|
-
}
|
|
348
|
+
totalTokenAmount = 0n;
|
|
359
349
|
}
|
|
360
350
|
}
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
351
|
+
// 2. 按每个钱包的买入金额比例分配代币数量
|
|
352
|
+
if (totalBuyWei > 0n && totalTokenAmount > 0n) {
|
|
353
|
+
let allocated = 0n;
|
|
354
|
+
expectedTokenAmounts = buyWeiList.map((buyWei, i) => {
|
|
355
|
+
if (i === buyWeiList.length - 1) {
|
|
356
|
+
return totalTokenAmount - allocated;
|
|
357
|
+
}
|
|
358
|
+
const share = (totalTokenAmount * buyWei) / totalBuyWei;
|
|
359
|
+
allocated += share;
|
|
360
|
+
return share;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
expectedTokenAmounts = buyWeiList.map(() => 0n);
|
|
365
|
+
}
|
|
366
|
+
console.log(`[buildWashOps] Flap 总代币=${totalTokenAmount}, 分配:`, expectedTokenAmounts.map(a => a.toString()));
|
|
369
367
|
}
|
|
370
368
|
else if (poolType === 'v2') {
|
|
371
|
-
// ✅ V2
|
|
369
|
+
// ✅ V2 报价:使用【总金额报价 + 按比例分配】(和 BSC 一样)
|
|
372
370
|
const dexQuery = new DexQuery({ rpcUrl: config.rpcUrl, routerAddress, wokbAddress: wokb });
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
let
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
cumulativeAmounts.push(cumulative);
|
|
371
|
+
const totalBuyWei = buyWeiList.reduce((a, b) => a + b, 0n);
|
|
372
|
+
console.log(`[buildWashOps] V2 总金额报价: ${buyWeiList.length} 个钱包, 总金额=${totalBuyWei}`);
|
|
373
|
+
// 1. 用总金额获取总代币数量
|
|
374
|
+
let totalTokenAmount = 0n;
|
|
375
|
+
try {
|
|
376
|
+
totalTokenAmount = await dexQuery.quoteOkbToToken(totalBuyWei, params.tokenAddress);
|
|
380
377
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
for (const amt of cumulativeAmounts) {
|
|
384
|
-
try {
|
|
385
|
-
const quote = await dexQuery.quoteOkbToToken(amt, params.tokenAddress);
|
|
386
|
-
cumulativeQuotes.push(quote);
|
|
387
|
-
}
|
|
388
|
-
catch {
|
|
389
|
-
cumulativeQuotes.push(0n);
|
|
390
|
-
}
|
|
378
|
+
catch {
|
|
379
|
+
totalTokenAmount = 0n;
|
|
391
380
|
}
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
381
|
+
// 2. 按每个钱包的买入金额比例分配代币数量
|
|
382
|
+
if (totalBuyWei > 0n && totalTokenAmount > 0n) {
|
|
383
|
+
let allocated = 0n;
|
|
384
|
+
expectedTokenAmounts = buyWeiList.map((buyWei, i) => {
|
|
385
|
+
if (i === buyWeiList.length - 1) {
|
|
386
|
+
return totalTokenAmount - allocated;
|
|
387
|
+
}
|
|
388
|
+
const share = (totalTokenAmount * buyWei) / totalBuyWei;
|
|
389
|
+
allocated += share;
|
|
390
|
+
return share;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
expectedTokenAmounts = buyWeiList.map(() => 0n);
|
|
395
|
+
}
|
|
396
|
+
console.log(`[buildWashOps] V2 总代币=${totalTokenAmount}, 分配:`, expectedTokenAmounts.map(a => a.toString()));
|
|
400
397
|
}
|
|
401
398
|
else if (poolType === 'v3') {
|
|
402
|
-
// ✅ V3
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
console.log(`[buildWashOps] V3
|
|
406
|
-
// 1.
|
|
407
|
-
const
|
|
408
|
-
let cumulative = 0n;
|
|
409
|
-
for (const buyWei of buyWeiList) {
|
|
410
|
-
cumulative += buyWei;
|
|
411
|
-
cumulativeAmounts.push(cumulative);
|
|
412
|
-
}
|
|
413
|
-
// 2. 批量查询所有累积金额点的报价
|
|
414
|
-
const cumulativeQuotes = await batchQuoteV3WithMulticall({
|
|
399
|
+
// ✅ V3 报价:使用【总金额报价 + 按比例分配】(和 BSC 一样)
|
|
400
|
+
// 这样可以正确考虑多钱包累积价格影响,确保卖干净
|
|
401
|
+
const totalBuyWei = buyWeiList.reduce((a, b) => a + b, 0n);
|
|
402
|
+
console.log(`[buildWashOps] V3 总金额报价: ${buyWeiList.length} 个钱包, 总金额=${totalBuyWei}, fee=${v3Fee}`);
|
|
403
|
+
// 1. 用总金额获取总代币数量
|
|
404
|
+
const totalQuoteResult = await batchQuoteV3WithMulticall({
|
|
415
405
|
rpcUrl: config.rpcUrl,
|
|
416
406
|
tokenIn: wokb,
|
|
417
407
|
tokenOut: params.tokenAddress,
|
|
418
|
-
amountsIn:
|
|
408
|
+
amountsIn: [totalBuyWei],
|
|
419
409
|
fee: v3Fee,
|
|
420
410
|
});
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
411
|
+
const totalTokenAmount = totalQuoteResult[0] || 0n;
|
|
412
|
+
// 2. 按每个钱包的买入金额比例分配代币数量
|
|
413
|
+
if (totalBuyWei > 0n && totalTokenAmount > 0n) {
|
|
414
|
+
let allocated = 0n;
|
|
415
|
+
expectedTokenAmounts = buyWeiList.map((buyWei, i) => {
|
|
416
|
+
if (i === buyWeiList.length - 1) {
|
|
417
|
+
// 最后一个钱包分配剩余的全部(避免精度损失)
|
|
418
|
+
return totalTokenAmount - allocated;
|
|
419
|
+
}
|
|
420
|
+
const share = (totalTokenAmount * buyWei) / totalBuyWei;
|
|
421
|
+
allocated += share;
|
|
422
|
+
return share;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
expectedTokenAmounts = buyWeiList.map(() => 0n);
|
|
427
|
+
}
|
|
428
|
+
console.log(`[buildWashOps] V3 总代币=${totalTokenAmount}, 分配:`, expectedTokenAmounts.map(a => a.toString()));
|
|
429
429
|
}
|
|
430
430
|
console.log(`[buildWashOps] 预测买入代币数量 (${poolType}):`, expectedTokenAmounts.map(a => a.toString()));
|
|
431
431
|
// 构建所有 UserOps 的骨架
|