@aastar/enduser 0.16.11
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/LICENSE +21 -0
- package/__tests__/CommunityClient.test.ts +205 -0
- package/__tests__/UserClient.test.ts +294 -0
- package/__tests__/index.test.ts +16 -0
- package/__tests__/mocks/client.ts +22 -0
- package/coverage/CommunityClient.ts.html +790 -0
- package/coverage/UserClient.ts.html +1423 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +3 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/CommunityClient.d.ts +65 -0
- package/dist/CommunityClient.js +188 -0
- package/dist/UserClient.d.ts +87 -0
- package/dist/UserClient.js +395 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/testAccountManager.d.ts +142 -0
- package/dist/testAccountManager.js +267 -0
- package/package.json +26 -0
- package/src/CommunityClient.ts +235 -0
- package/src/UserClient.ts +447 -0
- package/src/index.ts +2 -0
- package/src/testAccountManager.ts +374 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { BaseClient } from '@aastar/core';
|
|
2
|
+
import { accountActions, sbtActions, tokenActions, entryPointActions, stakingActions, registryActions } from '@aastar/core';
|
|
3
|
+
import { bundlerActions, getUserOperationHash } from 'viem/account-abstraction';
|
|
4
|
+
import { encodeFunctionData } from 'viem';
|
|
5
|
+
export class UserClient extends BaseClient {
|
|
6
|
+
accountAddress;
|
|
7
|
+
sbtAddress;
|
|
8
|
+
entryPointAddress;
|
|
9
|
+
gTokenStakingAddress;
|
|
10
|
+
registryAddress;
|
|
11
|
+
gTokenAddress;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
super(config);
|
|
14
|
+
this.accountAddress = config.accountAddress;
|
|
15
|
+
this.sbtAddress = config.sbtAddress;
|
|
16
|
+
this.entryPointAddress = config.entryPointAddress;
|
|
17
|
+
this.gTokenStakingAddress = config.gTokenStakingAddress;
|
|
18
|
+
this.registryAddress = config.registryAddress;
|
|
19
|
+
this.gTokenAddress = config.gTokenAddress;
|
|
20
|
+
}
|
|
21
|
+
// ========================================
|
|
22
|
+
// 1. 账户基本操作 (基于 L1 simpleAccountActions)
|
|
23
|
+
// ========================================
|
|
24
|
+
/**
|
|
25
|
+
* Get the nonce of the account from EntryPoint (more reliable for 4337)
|
|
26
|
+
*/
|
|
27
|
+
async getNonce(key = 0n) {
|
|
28
|
+
try {
|
|
29
|
+
if (!this.entryPointAddress) {
|
|
30
|
+
throw new Error('EntryPoint address required for this client');
|
|
31
|
+
}
|
|
32
|
+
const entryPoint = entryPointActions(this.entryPointAddress);
|
|
33
|
+
return await entryPoint(this.getStartPublicClient()).getNonce({
|
|
34
|
+
sender: this.accountAddress,
|
|
35
|
+
key
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the owner of the AA account
|
|
44
|
+
*/
|
|
45
|
+
async getOwner() {
|
|
46
|
+
try {
|
|
47
|
+
const account = accountActions(this.accountAddress);
|
|
48
|
+
return await account(this.getStartPublicClient()).owner();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Execute a transaction from the AA account
|
|
56
|
+
*/
|
|
57
|
+
async execute(target, value, data, options) {
|
|
58
|
+
try {
|
|
59
|
+
const account = accountActions(this.accountAddress);
|
|
60
|
+
// Use standard AA execute
|
|
61
|
+
return await account(this.client).execute({
|
|
62
|
+
dest: target,
|
|
63
|
+
value,
|
|
64
|
+
func: data,
|
|
65
|
+
account: options?.account
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Execute a batch of transactions
|
|
74
|
+
*/
|
|
75
|
+
async executeBatch(targets, values, datas, options) {
|
|
76
|
+
try {
|
|
77
|
+
const account = accountActions(this.accountAddress);
|
|
78
|
+
return await account(this.client).executeBatch({
|
|
79
|
+
dest: targets,
|
|
80
|
+
value: values,
|
|
81
|
+
func: datas,
|
|
82
|
+
account: options?.account
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ========================================
|
|
90
|
+
// identity 与 SBT (基于 L1 sbtActions)
|
|
91
|
+
// ========================================
|
|
92
|
+
/**
|
|
93
|
+
* Get user's SBT balance
|
|
94
|
+
*/
|
|
95
|
+
async getSBTBalance() {
|
|
96
|
+
try {
|
|
97
|
+
if (!this.sbtAddress)
|
|
98
|
+
throw new Error('SBT address required for this client');
|
|
99
|
+
const sbt = sbtActions(this.sbtAddress);
|
|
100
|
+
return await sbt(this.getStartPublicClient()).balanceOf({
|
|
101
|
+
owner: this.accountAddress
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Self-mint SBT for a role (user self-service)
|
|
110
|
+
*/
|
|
111
|
+
async mintSBT(roleId, options) {
|
|
112
|
+
try {
|
|
113
|
+
if (!this.sbtAddress)
|
|
114
|
+
throw new Error('SBT address required for this client');
|
|
115
|
+
const sbt = sbtActions(this.sbtAddress);
|
|
116
|
+
return await sbt(this.client).mintForRole({
|
|
117
|
+
user: this.accountAddress,
|
|
118
|
+
roleId,
|
|
119
|
+
roleData: '0x',
|
|
120
|
+
account: options?.account
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ========================================
|
|
128
|
+
// 3. 资产管理 (基于 L1 tokenActions)
|
|
129
|
+
// ========================================
|
|
130
|
+
/**
|
|
131
|
+
* Transfer GToken or any ERC20
|
|
132
|
+
*/
|
|
133
|
+
async transferToken(token, to, amount, options) {
|
|
134
|
+
try {
|
|
135
|
+
const tokens = tokenActions()(this.client);
|
|
136
|
+
return await tokens.transfer({
|
|
137
|
+
token,
|
|
138
|
+
to,
|
|
139
|
+
amount,
|
|
140
|
+
account: options?.account
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get Token Balance
|
|
149
|
+
*/
|
|
150
|
+
async getTokenBalance(token) {
|
|
151
|
+
try {
|
|
152
|
+
const tokens = tokenActions()(this.getStartPublicClient());
|
|
153
|
+
return await tokens.balanceOf({
|
|
154
|
+
token,
|
|
155
|
+
account: this.accountAddress
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ========================================
|
|
163
|
+
// 4. 委托与质押 (Delegation & Staking)
|
|
164
|
+
// ========================================
|
|
165
|
+
/**
|
|
166
|
+
* Delegate stake to a role (Delegate to an operator/community)
|
|
167
|
+
*/
|
|
168
|
+
async stakeForRole(roleId, amount, options) {
|
|
169
|
+
try {
|
|
170
|
+
if (!this.gTokenStakingAddress)
|
|
171
|
+
throw new Error('GTokenStaking address required for this client');
|
|
172
|
+
const staking = stakingActions(this.gTokenStakingAddress);
|
|
173
|
+
return await staking(this.client).lockStake({
|
|
174
|
+
user: this.accountAddress,
|
|
175
|
+
roleId,
|
|
176
|
+
stakeAmount: amount,
|
|
177
|
+
entryBurn: 0n,
|
|
178
|
+
payer: this.accountAddress,
|
|
179
|
+
account: options?.account
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Unstake from a role
|
|
188
|
+
*/
|
|
189
|
+
async unstakeFromRole(roleId, options) {
|
|
190
|
+
try {
|
|
191
|
+
if (!this.gTokenStakingAddress)
|
|
192
|
+
throw new Error('GTokenStaking address required for this client');
|
|
193
|
+
const staking = stakingActions(this.gTokenStakingAddress);
|
|
194
|
+
return await staking(this.client).unlockAndTransfer({
|
|
195
|
+
user: this.accountAddress,
|
|
196
|
+
roleId,
|
|
197
|
+
account: options?.account
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get staked balance for a specific role
|
|
206
|
+
*/
|
|
207
|
+
async getStakedBalance(roleId) {
|
|
208
|
+
try {
|
|
209
|
+
if (!this.gTokenStakingAddress)
|
|
210
|
+
throw new Error('GTokenStaking address required for this client');
|
|
211
|
+
const staking = stakingActions(this.gTokenStakingAddress);
|
|
212
|
+
return await staking(this.getStartPublicClient()).getLockedStake({
|
|
213
|
+
user: this.accountAddress,
|
|
214
|
+
roleId
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// ========================================
|
|
222
|
+
// 5. 生命周期管理 (Lifecycle)
|
|
223
|
+
// ========================================
|
|
224
|
+
/**
|
|
225
|
+
* Exit a specific role (Cleanup registry status)
|
|
226
|
+
*/
|
|
227
|
+
async exitRole(roleId, options) {
|
|
228
|
+
try {
|
|
229
|
+
if (!this.registryAddress)
|
|
230
|
+
throw new Error('Registry address required for this client');
|
|
231
|
+
const registry = registryActions(this.registryAddress);
|
|
232
|
+
return await registry(this.client).exitRole({
|
|
233
|
+
roleId,
|
|
234
|
+
account: options?.account
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Leave a community (Burn SBT and clean up)
|
|
243
|
+
*/
|
|
244
|
+
async leaveCommunity(community, options) {
|
|
245
|
+
try {
|
|
246
|
+
if (!this.sbtAddress)
|
|
247
|
+
throw new Error('SBT address required for this client');
|
|
248
|
+
const sbt = sbtActions(this.sbtAddress);
|
|
249
|
+
return await sbt(this.client).leaveCommunity({
|
|
250
|
+
community,
|
|
251
|
+
account: options?.account
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Register as EndUser (One-click: Approve + Register)
|
|
260
|
+
* Handles GToken approval to Staking contract and Role registration.
|
|
261
|
+
*/
|
|
262
|
+
async registerAsEndUser(communityAddress, stakeAmount, options) {
|
|
263
|
+
try {
|
|
264
|
+
if (!this.registryAddress)
|
|
265
|
+
throw new Error('Registry address required for this client');
|
|
266
|
+
if (!this.gTokenStakingAddress)
|
|
267
|
+
throw new Error('GTokenStaking address required for this client');
|
|
268
|
+
if (!this.gTokenAddress)
|
|
269
|
+
throw new Error('GToken address required for this client');
|
|
270
|
+
const { encodeAbiParameters, keccak256, toBytes, parseEther } = await import('viem');
|
|
271
|
+
const ROLE_ENDUSER = keccak256(toBytes("ENDUSER"));
|
|
272
|
+
// Correct mapping for Registry Actions
|
|
273
|
+
const registry = registryActions(this.registryAddress);
|
|
274
|
+
const tokens = tokenActions()(this.getStartPublicClient());
|
|
275
|
+
// 1. Check Allowance
|
|
276
|
+
const allowance = await tokens.allowance({
|
|
277
|
+
token: this.gTokenAddress,
|
|
278
|
+
owner: this.accountAddress,
|
|
279
|
+
spender: this.gTokenStakingAddress
|
|
280
|
+
});
|
|
281
|
+
const txs = [];
|
|
282
|
+
if (allowance < stakeAmount) {
|
|
283
|
+
const approveData = encodeFunctionData({
|
|
284
|
+
abi: [{ name: 'approve', type: 'function', inputs: [{ type: 'address' }, { type: 'uint256' }], outputs: [{ type: 'bool' }], stateMutability: 'nonpayable' }],
|
|
285
|
+
functionName: 'approve',
|
|
286
|
+
args: [this.gTokenStakingAddress, parseEther('1000')]
|
|
287
|
+
});
|
|
288
|
+
txs.push({ target: this.gTokenAddress, value: 0n, data: approveData });
|
|
289
|
+
}
|
|
290
|
+
// 2. Construct Register Call
|
|
291
|
+
// struct EndUserRoleData { address account; address community; string avatarURI; string ensName; uint256 stakeAmount; }
|
|
292
|
+
const roleData = encodeAbiParameters([
|
|
293
|
+
{ type: 'address', name: 'account' },
|
|
294
|
+
{ type: 'address', name: 'community' },
|
|
295
|
+
{ type: 'string', name: 'avatarURI' },
|
|
296
|
+
{ type: 'string', name: 'ensName' },
|
|
297
|
+
{ type: 'uint256', name: 'stakeAmount' }
|
|
298
|
+
], [
|
|
299
|
+
this.accountAddress,
|
|
300
|
+
communityAddress,
|
|
301
|
+
'',
|
|
302
|
+
'',
|
|
303
|
+
stakeAmount
|
|
304
|
+
]);
|
|
305
|
+
const registerData = encodeFunctionData({
|
|
306
|
+
abi: [{ name: 'registerRoleSelf', type: 'function', inputs: [{ type: 'bytes32' }, { type: 'bytes' }], outputs: [{ type: 'uint256' }], stateMutability: 'nonpayable' }],
|
|
307
|
+
functionName: 'registerRoleSelf',
|
|
308
|
+
args: [ROLE_ENDUSER, roleData]
|
|
309
|
+
});
|
|
310
|
+
txs.push({ target: this.registryAddress, value: 0n, data: registerData });
|
|
311
|
+
// 3. Execute
|
|
312
|
+
if (txs.length === 1) {
|
|
313
|
+
return await this.execute(txs[0].target, txs[0].value, txs[0].data, options);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
return await this.executeBatch(txs.map(t => t.target), txs.map(t => t.value), txs.map(t => t.data), options);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ========================================
|
|
324
|
+
// 6. Gasless Execution (Advanced)
|
|
325
|
+
// ========================================
|
|
326
|
+
/**
|
|
327
|
+
* Execute a transaction with Gasless Sponsorship
|
|
328
|
+
*/
|
|
329
|
+
async executeGasless(params, options) {
|
|
330
|
+
try {
|
|
331
|
+
const client = this.client.extend(bundlerActions);
|
|
332
|
+
const ep = this.requireEntryPoint();
|
|
333
|
+
const publicClient = this.getStartPublicClient();
|
|
334
|
+
// 1. Prepare Call Data
|
|
335
|
+
const callData = encodeFunctionData({
|
|
336
|
+
abi: [{ name: 'execute', type: 'function', inputs: [{ name: 'dest', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'func', type: 'bytes' }], outputs: [] }],
|
|
337
|
+
functionName: 'execute',
|
|
338
|
+
args: [params.target, params.value, params.data]
|
|
339
|
+
});
|
|
340
|
+
// 2. Prepare Paymaster Data
|
|
341
|
+
let paymasterAndData = params.paymaster;
|
|
342
|
+
// Note: In real scenarios, PM V4 or Super might need additional encoded data.
|
|
343
|
+
// For now, we use the address as the base.
|
|
344
|
+
// 3. Estimate Gas
|
|
345
|
+
const sender = this.accountAddress;
|
|
346
|
+
const nonce = await this.getNonce();
|
|
347
|
+
const userOpPartial = {
|
|
348
|
+
sender,
|
|
349
|
+
nonce,
|
|
350
|
+
initCode: '0x',
|
|
351
|
+
callData,
|
|
352
|
+
paymasterAndData,
|
|
353
|
+
signature: '0x'
|
|
354
|
+
};
|
|
355
|
+
const gasEstimate = await client.estimateUserOperationGas({
|
|
356
|
+
userOperation: userOpPartial,
|
|
357
|
+
entryPoint: ep
|
|
358
|
+
});
|
|
359
|
+
// 4. Construct Final UserOp
|
|
360
|
+
const fees = await publicClient.estimateFeesPerGas();
|
|
361
|
+
const userOp = {
|
|
362
|
+
...userOpPartial,
|
|
363
|
+
callGasLimit: gasEstimate.callGasLimit,
|
|
364
|
+
verificationGasLimit: gasEstimate.verificationGasLimit + 50000n,
|
|
365
|
+
preVerificationGas: gasEstimate.preVerificationGas,
|
|
366
|
+
maxFeePerGas: fees.maxFeePerGas || fees.gasPrice || 1000000000n,
|
|
367
|
+
maxPriorityFeePerGas: fees.maxPriorityFeePerGas || 1000000000n
|
|
368
|
+
};
|
|
369
|
+
// 5. Sign
|
|
370
|
+
const chainId = this.client.chain?.id || 31337;
|
|
371
|
+
const hash = getUserOperationHash({
|
|
372
|
+
userOperation: userOp,
|
|
373
|
+
entryPointAddress: ep,
|
|
374
|
+
entryPointVersion: '0.7',
|
|
375
|
+
chainId
|
|
376
|
+
});
|
|
377
|
+
const signature = await this.client.signMessage({
|
|
378
|
+
message: { raw: hash },
|
|
379
|
+
account: this.client.account
|
|
380
|
+
});
|
|
381
|
+
const signedUserOp = {
|
|
382
|
+
...userOp,
|
|
383
|
+
signature
|
|
384
|
+
};
|
|
385
|
+
// 6. Send
|
|
386
|
+
return await client.sendUserOperation({
|
|
387
|
+
userOperation: signedUserOp,
|
|
388
|
+
entryPoint: ep
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Address, Hash, PublicClient, WalletClient } from 'viem';
|
|
2
|
+
/**
|
|
3
|
+
* PhD Paper Experiment Test Toolkit
|
|
4
|
+
*
|
|
5
|
+
* **Purpose**: Comprehensive API suite for preparing and managing test accounts
|
|
6
|
+
* for ERC-4337 performance comparison experiments (EOA vs AA vs SuperPaymaster).
|
|
7
|
+
*
|
|
8
|
+
* **Core Features**:
|
|
9
|
+
* 1. **Account Generation**: Create random EOA keys and deploy SimpleAccounts
|
|
10
|
+
* 2. **Token Funding**: Transfer test tokens (GToken, aPNTs, bPNTs, ETH)
|
|
11
|
+
* 3. **AA Deployment**: Deploy SimpleAccount contracts using official factory
|
|
12
|
+
* 4. **UserOp Execution**: Send ERC-4337 UserOperations with various paymasters
|
|
13
|
+
* 5. **Data Collection**: Generate experiment data for PhD paper analysis
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const toolkit = new TestAccountManager(publicClient, supplierWallet);
|
|
18
|
+
*
|
|
19
|
+
* // Prepare complete test environment
|
|
20
|
+
* const env = await toolkit.prepareTestEnvironment({
|
|
21
|
+
* accountCount: 3,
|
|
22
|
+
* fundEachEOAWithETH: parseEther("0.01"),
|
|
23
|
+
* fundEachAAWithETH: parseEther("0.02"),
|
|
24
|
+
* tokens: {
|
|
25
|
+
* gToken: { address: '0x...', amount: parseEther("100") },
|
|
26
|
+
* aPNTs: { address: '0x...', amount: parseEther("50") }
|
|
27
|
+
* }
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare class TestAccountManager {
|
|
32
|
+
private publicClient;
|
|
33
|
+
private walletClient;
|
|
34
|
+
constructor(publicClient: PublicClient, walletClient: WalletClient);
|
|
35
|
+
/**
|
|
36
|
+
* Prepare complete test environment for PhD experiments
|
|
37
|
+
*
|
|
38
|
+
* **Workflow**:
|
|
39
|
+
* 1. Generate N random EOA private keys
|
|
40
|
+
* 2. Deploy SimpleAccount for each EOA
|
|
41
|
+
* 3. Fund EOAs with ETH
|
|
42
|
+
* 4. Fund AAs with ETH
|
|
43
|
+
* 5. Transfer test tokens (GToken, aPNTs, bPNTs) to both EOAs and AAs
|
|
44
|
+
*
|
|
45
|
+
* @param config - Test environment configuration
|
|
46
|
+
* @returns Complete test environment with all accounts and tokens ready
|
|
47
|
+
*/
|
|
48
|
+
prepareTestEnvironment(config: TestEnvironmentConfig): Promise<TestEnvironment>;
|
|
49
|
+
/**
|
|
50
|
+
* Generate multiple test accounts for experiments
|
|
51
|
+
* (Simplified version without token funding)
|
|
52
|
+
*/
|
|
53
|
+
generateTestAccounts(count?: number, options?: {
|
|
54
|
+
fundEachAAWith?: bigint;
|
|
55
|
+
fundEachEOAWith?: bigint;
|
|
56
|
+
startingSalt?: number;
|
|
57
|
+
}): Promise<TestAccount[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Export test accounts to .env format
|
|
60
|
+
*/
|
|
61
|
+
exportToEnv(accounts: TestAccount[]): string;
|
|
62
|
+
/**
|
|
63
|
+
* Load test accounts from environment variables
|
|
64
|
+
*/
|
|
65
|
+
static loadFromEnv(labels?: string[]): (TestAccount | null)[];
|
|
66
|
+
/**
|
|
67
|
+
* Get a single test account by label
|
|
68
|
+
*/
|
|
69
|
+
static getTestAccount(label: string): TestAccount | null;
|
|
70
|
+
private records;
|
|
71
|
+
/**
|
|
72
|
+
* Add a standardized experiment record
|
|
73
|
+
*/
|
|
74
|
+
addExperimentRecord(record: {
|
|
75
|
+
scenario: string;
|
|
76
|
+
group: 'EOA' | 'AA' | 'SuperPaymaster';
|
|
77
|
+
txHash: string;
|
|
78
|
+
gasUsed: bigint;
|
|
79
|
+
gasPrice: bigint;
|
|
80
|
+
status: string;
|
|
81
|
+
meta?: any;
|
|
82
|
+
}): {
|
|
83
|
+
id: string;
|
|
84
|
+
timestamp: number;
|
|
85
|
+
costETH: string;
|
|
86
|
+
scenario: string;
|
|
87
|
+
group: "EOA" | "AA" | "SuperPaymaster";
|
|
88
|
+
txHash: string;
|
|
89
|
+
gasUsed: bigint;
|
|
90
|
+
gasPrice: bigint;
|
|
91
|
+
status: string;
|
|
92
|
+
meta?: any;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Export collected data to CSV for PhD paper analysis
|
|
96
|
+
*/
|
|
97
|
+
exportExperimentResults(filename?: string): void;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Test environment configuration
|
|
101
|
+
*/
|
|
102
|
+
export interface TestEnvironmentConfig {
|
|
103
|
+
accountCount?: number;
|
|
104
|
+
fundEachEOAWithETH?: bigint;
|
|
105
|
+
fundEachAAWithETH?: bigint;
|
|
106
|
+
startingSalt?: number;
|
|
107
|
+
tokens?: {
|
|
108
|
+
[tokenName: string]: {
|
|
109
|
+
address: Address;
|
|
110
|
+
amount: bigint;
|
|
111
|
+
fundEOA?: boolean;
|
|
112
|
+
fundAA?: boolean;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Complete test environment
|
|
118
|
+
*/
|
|
119
|
+
export interface TestEnvironment {
|
|
120
|
+
accounts: TestAccount[];
|
|
121
|
+
tokenFunding: TokenFundingRecord[];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Token funding record
|
|
125
|
+
*/
|
|
126
|
+
export interface TokenFundingRecord {
|
|
127
|
+
account: string;
|
|
128
|
+
token: string;
|
|
129
|
+
eoaAmount: bigint;
|
|
130
|
+
aaAmount: bigint;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Test account data structure
|
|
134
|
+
*/
|
|
135
|
+
export interface TestAccount {
|
|
136
|
+
label: string;
|
|
137
|
+
ownerKey: `0x${string}`;
|
|
138
|
+
ownerAddress: Address;
|
|
139
|
+
aaAddress: Address;
|
|
140
|
+
deployTxHash: Hash;
|
|
141
|
+
salt: number;
|
|
142
|
+
}
|