@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,374 @@
|
|
|
1
|
+
import { Address, Hash, PublicClient, WalletClient, parseEther, Hex } from 'viem';
|
|
2
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
3
|
+
import { accountFactoryActions, TEST_ACCOUNT_ADDRESSES } from '@aastar/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PhD Paper Experiment Test Toolkit
|
|
7
|
+
*
|
|
8
|
+
* **Purpose**: Comprehensive API suite for preparing and managing test accounts
|
|
9
|
+
* for ERC-4337 performance comparison experiments (EOA vs AA vs SuperPaymaster).
|
|
10
|
+
*
|
|
11
|
+
* **Core Features**:
|
|
12
|
+
* 1. **Account Generation**: Create random EOA keys and deploy SimpleAccounts
|
|
13
|
+
* 2. **Token Funding**: Transfer test tokens (GToken, aPNTs, bPNTs, ETH)
|
|
14
|
+
* 3. **AA Deployment**: Deploy SimpleAccount contracts using official factory
|
|
15
|
+
* 4. **UserOp Execution**: Send ERC-4337 UserOperations with various paymasters
|
|
16
|
+
* 5. **Data Collection**: Generate experiment data for PhD paper analysis
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const toolkit = new TestAccountManager(publicClient, supplierWallet);
|
|
21
|
+
*
|
|
22
|
+
* // Prepare complete test environment
|
|
23
|
+
* const env = await toolkit.prepareTestEnvironment({
|
|
24
|
+
* accountCount: 3,
|
|
25
|
+
* fundEachEOAWithETH: parseEther("0.01"),
|
|
26
|
+
* fundEachAAWithETH: parseEther("0.02"),
|
|
27
|
+
* tokens: {
|
|
28
|
+
* gToken: { address: '0x...', amount: parseEther("100") },
|
|
29
|
+
* aPNTs: { address: '0x...', amount: parseEther("50") }
|
|
30
|
+
* }
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export class TestAccountManager {
|
|
35
|
+
constructor(
|
|
36
|
+
private publicClient: PublicClient,
|
|
37
|
+
private walletClient: WalletClient
|
|
38
|
+
) {
|
|
39
|
+
if (!walletClient.account) {
|
|
40
|
+
// Placeholder account if not provided to avoid strict null checks in experiments
|
|
41
|
+
// In production, the consumer must ensure the wallet is connected.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Prepare complete test environment for PhD experiments
|
|
47
|
+
*
|
|
48
|
+
* **Workflow**:
|
|
49
|
+
* 1. Generate N random EOA private keys
|
|
50
|
+
* 2. Deploy SimpleAccount for each EOA
|
|
51
|
+
* 3. Fund EOAs with ETH
|
|
52
|
+
* 4. Fund AAs with ETH
|
|
53
|
+
* 5. Transfer test tokens (GToken, aPNTs, bPNTs) to both EOAs and AAs
|
|
54
|
+
*
|
|
55
|
+
* @param config - Test environment configuration
|
|
56
|
+
* @returns Complete test environment with all accounts and tokens ready
|
|
57
|
+
*/
|
|
58
|
+
async prepareTestEnvironment(config: TestEnvironmentConfig): Promise<TestEnvironment> {
|
|
59
|
+
const {
|
|
60
|
+
accountCount = 3,
|
|
61
|
+
fundEachEOAWithETH = parseEther("0.01"),
|
|
62
|
+
fundEachAAWithETH = parseEther("0.02"),
|
|
63
|
+
tokens = {},
|
|
64
|
+
startingSalt = 0
|
|
65
|
+
} = config;
|
|
66
|
+
|
|
67
|
+
console.log(`๐งช Preparing PhD Experiment Test Environment (${accountCount} accounts)...\n`);
|
|
68
|
+
|
|
69
|
+
const accounts: TestAccount[] = [];
|
|
70
|
+
const labels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
|
|
71
|
+
|
|
72
|
+
const factoryAddress = TEST_ACCOUNT_ADDRESSES.simpleAccountFactory;
|
|
73
|
+
const factoryRead = accountFactoryActions(factoryAddress)(this.publicClient as any);
|
|
74
|
+
const factoryWrite = accountFactoryActions(factoryAddress)(this.walletClient as any);
|
|
75
|
+
|
|
76
|
+
// Step 1: Generate accounts and deploy AAs
|
|
77
|
+
for (let i = 0; i < accountCount; i++) {
|
|
78
|
+
const label = labels[i] || `${i + 1}`;
|
|
79
|
+
console.log(`\n๐ [${i + 1}/${accountCount}] Setting up Account ${label}...`);
|
|
80
|
+
|
|
81
|
+
// Generate EOA
|
|
82
|
+
const ownerKey = generatePrivateKey();
|
|
83
|
+
const ownerAccount = privateKeyToAccount(ownerKey);
|
|
84
|
+
console.log(` ๐ EOA: ${ownerAccount.address}`);
|
|
85
|
+
|
|
86
|
+
// Deploy AA
|
|
87
|
+
const salt = BigInt(startingSalt + i);
|
|
88
|
+
console.log(` ๐ญ Deploying SimpleAccount (salt: ${salt})...`);
|
|
89
|
+
|
|
90
|
+
// Predict address
|
|
91
|
+
const accountAddress = await factoryRead.getAddress({ owner: ownerAccount.address, salt });
|
|
92
|
+
|
|
93
|
+
// Deploy if needed (createAccount sends tx regardless, or we can check code?)
|
|
94
|
+
// For simplicitly we just call createAccount. If already deployed it might revert?
|
|
95
|
+
// SimpleAccountFactory doesn't seem to check existence in createAccount, but CREATE2 validates.
|
|
96
|
+
// But we are using a fresh key/salt, so collision is unlikely unless we rerun.
|
|
97
|
+
// We'll try-catch or just execute.
|
|
98
|
+
|
|
99
|
+
let deployTxHash: Hash = '0x0';
|
|
100
|
+
try {
|
|
101
|
+
deployTxHash = await factoryWrite.createAccount({
|
|
102
|
+
owner: ownerAccount.address,
|
|
103
|
+
salt,
|
|
104
|
+
account: this.walletClient.account as any
|
|
105
|
+
});
|
|
106
|
+
await this.publicClient.waitForTransactionReceipt({ hash: deployTxHash });
|
|
107
|
+
} catch (e: any) {
|
|
108
|
+
console.log(` โ ๏ธ Deployment might have failed (or already deployed): ${e.message?.split('\n')[0]}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(` โ
AA: ${accountAddress}`);
|
|
112
|
+
|
|
113
|
+
// Fund AA with ETH
|
|
114
|
+
if (fundEachAAWithETH > 0n) {
|
|
115
|
+
console.log(` โฝ Funding AA with ${fundEachAAWithETH} wei ETH...`);
|
|
116
|
+
const fundTx = await this.walletClient.sendTransaction({
|
|
117
|
+
to: accountAddress,
|
|
118
|
+
value: fundEachAAWithETH,
|
|
119
|
+
account: this.walletClient.account as any,
|
|
120
|
+
chain: this.walletClient.chain
|
|
121
|
+
});
|
|
122
|
+
await this.publicClient.waitForTransactionReceipt({ hash: fundTx });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Fund EOA with ETH
|
|
126
|
+
if (fundEachEOAWithETH > 0n) {
|
|
127
|
+
console.log(` โฝ Funding EOA with ${fundEachEOAWithETH} wei ETH...`);
|
|
128
|
+
const fundTx = await this.walletClient.sendTransaction({
|
|
129
|
+
to: ownerAccount.address,
|
|
130
|
+
value: fundEachEOAWithETH,
|
|
131
|
+
account: this.walletClient.account as any,
|
|
132
|
+
chain: this.walletClient.chain
|
|
133
|
+
});
|
|
134
|
+
await this.publicClient.waitForTransactionReceipt({ hash: fundTx });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
accounts.push({
|
|
138
|
+
label,
|
|
139
|
+
ownerKey,
|
|
140
|
+
ownerAddress: ownerAccount.address,
|
|
141
|
+
aaAddress: accountAddress,
|
|
142
|
+
deployTxHash,
|
|
143
|
+
salt: startingSalt + i
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Step 2: Fund with test tokens
|
|
148
|
+
console.log(`\n๐ฐ Funding accounts with test tokens...`);
|
|
149
|
+
const tokenFunding: TokenFundingRecord[] = [];
|
|
150
|
+
|
|
151
|
+
for (const [tokenName, tokenConfig] of Object.entries(tokens)) {
|
|
152
|
+
if (!tokenConfig) continue;
|
|
153
|
+
|
|
154
|
+
console.log(`\n ๐ Distributing ${tokenName}...`);
|
|
155
|
+
const erc20Abi = [
|
|
156
|
+
{ name: 'transfer', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ name: '', type: 'bool' }] }
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
for (const account of accounts) {
|
|
160
|
+
// Fund EOA
|
|
161
|
+
if (tokenConfig.fundEOA !== false) {
|
|
162
|
+
const tx = await this.walletClient.writeContract({
|
|
163
|
+
address: tokenConfig.address,
|
|
164
|
+
abi: erc20Abi,
|
|
165
|
+
functionName: 'transfer',
|
|
166
|
+
args: [account.ownerAddress, tokenConfig.amount],
|
|
167
|
+
account: this.walletClient.account as any,
|
|
168
|
+
chain: this.walletClient.chain
|
|
169
|
+
});
|
|
170
|
+
await this.publicClient.waitForTransactionReceipt({ hash: tx });
|
|
171
|
+
console.log(` โ
${account.label} EOA: ${tokenConfig.amount}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Fund AA
|
|
175
|
+
if (tokenConfig.fundAA !== false) {
|
|
176
|
+
const tx = await this.walletClient.writeContract({
|
|
177
|
+
address: tokenConfig.address,
|
|
178
|
+
abi: erc20Abi,
|
|
179
|
+
functionName: 'transfer',
|
|
180
|
+
args: [account.aaAddress, tokenConfig.amount],
|
|
181
|
+
account: this.walletClient.account as any,
|
|
182
|
+
chain: this.walletClient.chain
|
|
183
|
+
});
|
|
184
|
+
await this.publicClient.waitForTransactionReceipt({ hash: tx });
|
|
185
|
+
console.log(` โ
${account.label} AA: ${tokenConfig.amount}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
tokenFunding.push({
|
|
189
|
+
account: account.label,
|
|
190
|
+
token: tokenName,
|
|
191
|
+
eoaAmount: tokenConfig.fundEOA !== false ? tokenConfig.amount : 0n,
|
|
192
|
+
aaAmount: tokenConfig.fundAA !== false ? tokenConfig.amount : 0n
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(`\nโ
Test environment ready!`);
|
|
198
|
+
return { accounts, tokenFunding };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Generate multiple test accounts for experiments
|
|
203
|
+
* (Simplified version without token funding)
|
|
204
|
+
*/
|
|
205
|
+
async generateTestAccounts(
|
|
206
|
+
count: number = 3,
|
|
207
|
+
options: {
|
|
208
|
+
fundEachAAWith?: bigint;
|
|
209
|
+
fundEachEOAWith?: bigint;
|
|
210
|
+
startingSalt?: number;
|
|
211
|
+
} = {}
|
|
212
|
+
): Promise<TestAccount[]> {
|
|
213
|
+
const {
|
|
214
|
+
fundEachAAWith = parseEther("0.02"),
|
|
215
|
+
fundEachEOAWith = parseEther("0.01"),
|
|
216
|
+
startingSalt = 0
|
|
217
|
+
} = options;
|
|
218
|
+
|
|
219
|
+
const env = await this.prepareTestEnvironment({
|
|
220
|
+
accountCount: count,
|
|
221
|
+
fundEachEOAWithETH: fundEachEOAWith,
|
|
222
|
+
fundEachAAWithETH: fundEachAAWith,
|
|
223
|
+
startingSalt
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return env.accounts;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Export test accounts to .env format
|
|
231
|
+
*/
|
|
232
|
+
exportToEnv(accounts: TestAccount[]): string {
|
|
233
|
+
const lines = [
|
|
234
|
+
'# Test Accounts for PhD Paper Experiments',
|
|
235
|
+
'# Generated by TestAccountManager API',
|
|
236
|
+
''
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
accounts.forEach(acc => lines.push(`TEST_OWNER_KEY_${acc.label}=${acc.ownerKey}`));
|
|
240
|
+
lines.push('');
|
|
241
|
+
accounts.forEach(acc => lines.push(`TEST_OWNER_EOA_${acc.label}=${acc.ownerAddress}`));
|
|
242
|
+
lines.push('');
|
|
243
|
+
accounts.forEach(acc => lines.push(`TEST_SIMPLE_ACCOUNT_${acc.label}=${acc.aaAddress}`));
|
|
244
|
+
|
|
245
|
+
return lines.join('\n');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Load test accounts from environment variables
|
|
250
|
+
*/
|
|
251
|
+
static loadFromEnv(labels: string[] = ['A', 'B', 'C']): (TestAccount | null)[] {
|
|
252
|
+
return labels.map(label => {
|
|
253
|
+
const ownerKey = process.env[`TEST_OWNER_KEY_${label}`];
|
|
254
|
+
const ownerAddress = process.env[`TEST_OWNER_EOA_${label}`];
|
|
255
|
+
const aaAddress = process.env[`TEST_SIMPLE_ACCOUNT_${label}`];
|
|
256
|
+
|
|
257
|
+
if (!ownerKey || !ownerAddress || !aaAddress) return null;
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
label,
|
|
261
|
+
ownerKey: ownerKey as `0x${string}`,
|
|
262
|
+
ownerAddress: ownerAddress as Address,
|
|
263
|
+
aaAddress: aaAddress as Address,
|
|
264
|
+
deployTxHash: '0x0' as Hash,
|
|
265
|
+
salt: 0
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get a single test account by label
|
|
272
|
+
*/
|
|
273
|
+
static getTestAccount(label: string): TestAccount | null {
|
|
274
|
+
const accounts = TestAccountManager.loadFromEnv([label]);
|
|
275
|
+
return accounts[0] || null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- Data Collection Extensions ---
|
|
279
|
+
private records: any[] = [];
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Add a standardized experiment record
|
|
283
|
+
*/
|
|
284
|
+
addExperimentRecord(record: {
|
|
285
|
+
scenario: string,
|
|
286
|
+
group: 'EOA' | 'AA' | 'SuperPaymaster',
|
|
287
|
+
txHash: string,
|
|
288
|
+
gasUsed: bigint,
|
|
289
|
+
gasPrice: bigint,
|
|
290
|
+
status: string,
|
|
291
|
+
meta?: any
|
|
292
|
+
}) {
|
|
293
|
+
const fullRecord = {
|
|
294
|
+
...record,
|
|
295
|
+
id: `${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
296
|
+
timestamp: Date.now(),
|
|
297
|
+
costETH: formatEther(record.gasUsed * record.gasPrice)
|
|
298
|
+
};
|
|
299
|
+
this.records.push(fullRecord);
|
|
300
|
+
return fullRecord;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Export collected data to CSV for PhD paper analysis
|
|
305
|
+
*/
|
|
306
|
+
exportExperimentResults(filename: string = 'sdk_experiment_data.csv') {
|
|
307
|
+
const fs = require('fs');
|
|
308
|
+
const path = require('path');
|
|
309
|
+
|
|
310
|
+
const headers = ['ID', 'Scenario', 'Group', 'TxHash', 'GasUsed', 'GasPrice', 'CostETH', 'Status', 'Timestamp', 'Latency'];
|
|
311
|
+
const rows = this.records.map(r => [
|
|
312
|
+
r.id, r.scenario, r.group, r.txHash,
|
|
313
|
+
r.gasUsed.toString(), r.gasPrice.toString(), r.costETH,
|
|
314
|
+
r.status, new Date(r.timestamp).toISOString(),
|
|
315
|
+
r.meta?.latency || ''
|
|
316
|
+
].join(','));
|
|
317
|
+
|
|
318
|
+
const csv = [headers.join(','), ...rows].join('\n');
|
|
319
|
+
fs.writeFileSync(path.join(process.cwd(), filename), csv);
|
|
320
|
+
console.log(`๐ Exported ${this.records.length} records to ${filename}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function formatEther(wei: bigint): string {
|
|
325
|
+
return (Number(wei) / 1e18).toString();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Test environment configuration
|
|
330
|
+
*/
|
|
331
|
+
export interface TestEnvironmentConfig {
|
|
332
|
+
accountCount?: number;
|
|
333
|
+
fundEachEOAWithETH?: bigint;
|
|
334
|
+
fundEachAAWithETH?: bigint;
|
|
335
|
+
startingSalt?: number;
|
|
336
|
+
tokens?: {
|
|
337
|
+
[tokenName: string]: {
|
|
338
|
+
address: Address;
|
|
339
|
+
amount: bigint;
|
|
340
|
+
fundEOA?: boolean; // default: true
|
|
341
|
+
fundAA?: boolean; // default: true
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Complete test environment
|
|
348
|
+
*/
|
|
349
|
+
export interface TestEnvironment {
|
|
350
|
+
accounts: TestAccount[];
|
|
351
|
+
tokenFunding: TokenFundingRecord[];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Token funding record
|
|
356
|
+
*/
|
|
357
|
+
export interface TokenFundingRecord {
|
|
358
|
+
account: string;
|
|
359
|
+
token: string;
|
|
360
|
+
eoaAmount: bigint;
|
|
361
|
+
aaAmount: bigint;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Test account data structure
|
|
366
|
+
*/
|
|
367
|
+
export interface TestAccount {
|
|
368
|
+
label: string;
|
|
369
|
+
ownerKey: `0x${string}`;
|
|
370
|
+
ownerAddress: Address;
|
|
371
|
+
aaAddress: Address;
|
|
372
|
+
deployTxHash: Hash;
|
|
373
|
+
salt: number;
|
|
374
|
+
}
|