@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.
@@ -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
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.build.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "noEmit": false,
7
+ "declaration": true
8
+ },
9
+ "include": ["src/**/*"]
10
+ }