@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,267 @@
1
+ import { parseEther } from 'viem';
2
+ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
3
+ import { accountFactoryActions, TEST_ACCOUNT_ADDRESSES } from '@aastar/core';
4
+ /**
5
+ * PhD Paper Experiment Test Toolkit
6
+ *
7
+ * **Purpose**: Comprehensive API suite for preparing and managing test accounts
8
+ * for ERC-4337 performance comparison experiments (EOA vs AA vs SuperPaymaster).
9
+ *
10
+ * **Core Features**:
11
+ * 1. **Account Generation**: Create random EOA keys and deploy SimpleAccounts
12
+ * 2. **Token Funding**: Transfer test tokens (GToken, aPNTs, bPNTs, ETH)
13
+ * 3. **AA Deployment**: Deploy SimpleAccount contracts using official factory
14
+ * 4. **UserOp Execution**: Send ERC-4337 UserOperations with various paymasters
15
+ * 5. **Data Collection**: Generate experiment data for PhD paper analysis
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const toolkit = new TestAccountManager(publicClient, supplierWallet);
20
+ *
21
+ * // Prepare complete test environment
22
+ * const env = await toolkit.prepareTestEnvironment({
23
+ * accountCount: 3,
24
+ * fundEachEOAWithETH: parseEther("0.01"),
25
+ * fundEachAAWithETH: parseEther("0.02"),
26
+ * tokens: {
27
+ * gToken: { address: '0x...', amount: parseEther("100") },
28
+ * aPNTs: { address: '0x...', amount: parseEther("50") }
29
+ * }
30
+ * });
31
+ * ```
32
+ */
33
+ export class TestAccountManager {
34
+ publicClient;
35
+ walletClient;
36
+ constructor(publicClient, walletClient) {
37
+ this.publicClient = publicClient;
38
+ this.walletClient = walletClient;
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
+ * Prepare complete test environment for PhD experiments
46
+ *
47
+ * **Workflow**:
48
+ * 1. Generate N random EOA private keys
49
+ * 2. Deploy SimpleAccount for each EOA
50
+ * 3. Fund EOAs with ETH
51
+ * 4. Fund AAs with ETH
52
+ * 5. Transfer test tokens (GToken, aPNTs, bPNTs) to both EOAs and AAs
53
+ *
54
+ * @param config - Test environment configuration
55
+ * @returns Complete test environment with all accounts and tokens ready
56
+ */
57
+ async prepareTestEnvironment(config) {
58
+ const { accountCount = 3, fundEachEOAWithETH = parseEther("0.01"), fundEachAAWithETH = parseEther("0.02"), tokens = {}, startingSalt = 0 } = config;
59
+ console.log(`๐Ÿงช Preparing PhD Experiment Test Environment (${accountCount} accounts)...\n`);
60
+ const accounts = [];
61
+ const labels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
62
+ const factoryAddress = TEST_ACCOUNT_ADDRESSES.simpleAccountFactory;
63
+ const factoryRead = accountFactoryActions(factoryAddress)(this.publicClient);
64
+ const factoryWrite = accountFactoryActions(factoryAddress)(this.walletClient);
65
+ // Step 1: Generate accounts and deploy AAs
66
+ for (let i = 0; i < accountCount; i++) {
67
+ const label = labels[i] || `${i + 1}`;
68
+ console.log(`\n๐Ÿ“ [${i + 1}/${accountCount}] Setting up Account ${label}...`);
69
+ // Generate EOA
70
+ const ownerKey = generatePrivateKey();
71
+ const ownerAccount = privateKeyToAccount(ownerKey);
72
+ console.log(` ๐Ÿ”‘ EOA: ${ownerAccount.address}`);
73
+ // Deploy AA
74
+ const salt = BigInt(startingSalt + i);
75
+ console.log(` ๐Ÿญ Deploying SimpleAccount (salt: ${salt})...`);
76
+ // Predict address
77
+ const accountAddress = await factoryRead.getAddress({ owner: ownerAccount.address, salt });
78
+ // Deploy if needed (createAccount sends tx regardless, or we can check code?)
79
+ // For simplicitly we just call createAccount. If already deployed it might revert?
80
+ // SimpleAccountFactory doesn't seem to check existence in createAccount, but CREATE2 validates.
81
+ // But we are using a fresh key/salt, so collision is unlikely unless we rerun.
82
+ // We'll try-catch or just execute.
83
+ let deployTxHash = '0x0';
84
+ try {
85
+ deployTxHash = await factoryWrite.createAccount({
86
+ owner: ownerAccount.address,
87
+ salt,
88
+ account: this.walletClient.account
89
+ });
90
+ await this.publicClient.waitForTransactionReceipt({ hash: deployTxHash });
91
+ }
92
+ catch (e) {
93
+ console.log(` โš ๏ธ Deployment might have failed (or already deployed): ${e.message?.split('\n')[0]}`);
94
+ }
95
+ console.log(` โœ… AA: ${accountAddress}`);
96
+ // Fund AA with ETH
97
+ if (fundEachAAWithETH > 0n) {
98
+ console.log(` โ›ฝ Funding AA with ${fundEachAAWithETH} wei ETH...`);
99
+ const fundTx = await this.walletClient.sendTransaction({
100
+ to: accountAddress,
101
+ value: fundEachAAWithETH,
102
+ account: this.walletClient.account,
103
+ chain: this.walletClient.chain
104
+ });
105
+ await this.publicClient.waitForTransactionReceipt({ hash: fundTx });
106
+ }
107
+ // Fund EOA with ETH
108
+ if (fundEachEOAWithETH > 0n) {
109
+ console.log(` โ›ฝ Funding EOA with ${fundEachEOAWithETH} wei ETH...`);
110
+ const fundTx = await this.walletClient.sendTransaction({
111
+ to: ownerAccount.address,
112
+ value: fundEachEOAWithETH,
113
+ account: this.walletClient.account,
114
+ chain: this.walletClient.chain
115
+ });
116
+ await this.publicClient.waitForTransactionReceipt({ hash: fundTx });
117
+ }
118
+ accounts.push({
119
+ label,
120
+ ownerKey,
121
+ ownerAddress: ownerAccount.address,
122
+ aaAddress: accountAddress,
123
+ deployTxHash,
124
+ salt: startingSalt + i
125
+ });
126
+ }
127
+ // Step 2: Fund with test tokens
128
+ console.log(`\n๐Ÿ’ฐ Funding accounts with test tokens...`);
129
+ const tokenFunding = [];
130
+ for (const [tokenName, tokenConfig] of Object.entries(tokens)) {
131
+ if (!tokenConfig)
132
+ continue;
133
+ console.log(`\n ๐Ÿ“Š Distributing ${tokenName}...`);
134
+ const erc20Abi = [
135
+ { name: 'transfer', type: 'function', stateMutability: 'nonpayable', inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ name: '', type: 'bool' }] }
136
+ ];
137
+ for (const account of accounts) {
138
+ // Fund EOA
139
+ if (tokenConfig.fundEOA !== false) {
140
+ const tx = await this.walletClient.writeContract({
141
+ address: tokenConfig.address,
142
+ abi: erc20Abi,
143
+ functionName: 'transfer',
144
+ args: [account.ownerAddress, tokenConfig.amount],
145
+ account: this.walletClient.account,
146
+ chain: this.walletClient.chain
147
+ });
148
+ await this.publicClient.waitForTransactionReceipt({ hash: tx });
149
+ console.log(` โœ… ${account.label} EOA: ${tokenConfig.amount}`);
150
+ }
151
+ // Fund AA
152
+ if (tokenConfig.fundAA !== false) {
153
+ const tx = await this.walletClient.writeContract({
154
+ address: tokenConfig.address,
155
+ abi: erc20Abi,
156
+ functionName: 'transfer',
157
+ args: [account.aaAddress, tokenConfig.amount],
158
+ account: this.walletClient.account,
159
+ chain: this.walletClient.chain
160
+ });
161
+ await this.publicClient.waitForTransactionReceipt({ hash: tx });
162
+ console.log(` โœ… ${account.label} AA: ${tokenConfig.amount}`);
163
+ }
164
+ tokenFunding.push({
165
+ account: account.label,
166
+ token: tokenName,
167
+ eoaAmount: tokenConfig.fundEOA !== false ? tokenConfig.amount : 0n,
168
+ aaAmount: tokenConfig.fundAA !== false ? tokenConfig.amount : 0n
169
+ });
170
+ }
171
+ }
172
+ console.log(`\nโœ… Test environment ready!`);
173
+ return { accounts, tokenFunding };
174
+ }
175
+ /**
176
+ * Generate multiple test accounts for experiments
177
+ * (Simplified version without token funding)
178
+ */
179
+ async generateTestAccounts(count = 3, options = {}) {
180
+ const { fundEachAAWith = parseEther("0.02"), fundEachEOAWith = parseEther("0.01"), startingSalt = 0 } = options;
181
+ const env = await this.prepareTestEnvironment({
182
+ accountCount: count,
183
+ fundEachEOAWithETH: fundEachEOAWith,
184
+ fundEachAAWithETH: fundEachAAWith,
185
+ startingSalt
186
+ });
187
+ return env.accounts;
188
+ }
189
+ /**
190
+ * Export test accounts to .env format
191
+ */
192
+ exportToEnv(accounts) {
193
+ const lines = [
194
+ '# Test Accounts for PhD Paper Experiments',
195
+ '# Generated by TestAccountManager API',
196
+ ''
197
+ ];
198
+ accounts.forEach(acc => lines.push(`TEST_OWNER_KEY_${acc.label}=${acc.ownerKey}`));
199
+ lines.push('');
200
+ accounts.forEach(acc => lines.push(`TEST_OWNER_EOA_${acc.label}=${acc.ownerAddress}`));
201
+ lines.push('');
202
+ accounts.forEach(acc => lines.push(`TEST_SIMPLE_ACCOUNT_${acc.label}=${acc.aaAddress}`));
203
+ return lines.join('\n');
204
+ }
205
+ /**
206
+ * Load test accounts from environment variables
207
+ */
208
+ static loadFromEnv(labels = ['A', 'B', 'C']) {
209
+ return labels.map(label => {
210
+ const ownerKey = process.env[`TEST_OWNER_KEY_${label}`];
211
+ const ownerAddress = process.env[`TEST_OWNER_EOA_${label}`];
212
+ const aaAddress = process.env[`TEST_SIMPLE_ACCOUNT_${label}`];
213
+ if (!ownerKey || !ownerAddress || !aaAddress)
214
+ return null;
215
+ return {
216
+ label,
217
+ ownerKey: ownerKey,
218
+ ownerAddress: ownerAddress,
219
+ aaAddress: aaAddress,
220
+ deployTxHash: '0x0',
221
+ salt: 0
222
+ };
223
+ });
224
+ }
225
+ /**
226
+ * Get a single test account by label
227
+ */
228
+ static getTestAccount(label) {
229
+ const accounts = TestAccountManager.loadFromEnv([label]);
230
+ return accounts[0] || null;
231
+ }
232
+ // --- Data Collection Extensions ---
233
+ records = [];
234
+ /**
235
+ * Add a standardized experiment record
236
+ */
237
+ addExperimentRecord(record) {
238
+ const fullRecord = {
239
+ ...record,
240
+ id: `${Date.now()}-${Math.floor(Math.random() * 1000)}`,
241
+ timestamp: Date.now(),
242
+ costETH: formatEther(record.gasUsed * record.gasPrice)
243
+ };
244
+ this.records.push(fullRecord);
245
+ return fullRecord;
246
+ }
247
+ /**
248
+ * Export collected data to CSV for PhD paper analysis
249
+ */
250
+ exportExperimentResults(filename = 'sdk_experiment_data.csv') {
251
+ const fs = require('fs');
252
+ const path = require('path');
253
+ const headers = ['ID', 'Scenario', 'Group', 'TxHash', 'GasUsed', 'GasPrice', 'CostETH', 'Status', 'Timestamp', 'Latency'];
254
+ const rows = this.records.map(r => [
255
+ r.id, r.scenario, r.group, r.txHash,
256
+ r.gasUsed.toString(), r.gasPrice.toString(), r.costETH,
257
+ r.status, new Date(r.timestamp).toISOString(),
258
+ r.meta?.latency || ''
259
+ ].join(','));
260
+ const csv = [headers.join(','), ...rows].join('\n');
261
+ fs.writeFileSync(path.join(process.cwd(), filename), csv);
262
+ console.log(`๐Ÿ“Š Exported ${this.records.length} records to ${filename}`);
263
+ }
264
+ }
265
+ function formatEther(wei) {
266
+ return (Number(wei) / 1e18).toString();
267
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@aastar/enduser",
3
+ "version": "0.16.11",
4
+ "type": "module",
5
+ "description": "Enduser client for AAstar SDK",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "keywords": [
9
+ "aastar",
10
+ "enduser",
11
+ "web3"
12
+ ],
13
+ "author": "AAstar Team",
14
+ "license": "MIT",
15
+ "dependencies": {
16
+ "viem": "2.43.3",
17
+ "@aastar/core": "0.16.11"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "5.7.2"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "test": "vitest run"
25
+ }
26
+ }
@@ -0,0 +1,235 @@
1
+ import { type Address, type Hash, parseEther, encodeAbiParameters, parseAbiParameters } from 'viem';
2
+ import { BaseClient, type ClientConfig, type TransactionOptions } from '@aastar/core';
3
+ import { registryActions, sbtActions, xPNTsFactoryActions, reputationActions, tokenActions } from '@aastar/core';
4
+
5
+ export interface CommunityClientConfig extends ClientConfig {
6
+ sbtAddress?: Address;
7
+ factoryAddress?: Address;
8
+ reputationAddress?: Address;
9
+ }
10
+
11
+ export interface CreateCommunityParams {
12
+ name: string;
13
+ tokenSymbol: string;
14
+ ensName?: string;
15
+ description?: string;
16
+ }
17
+
18
+ export interface CommunityInfo {
19
+ address: Address; // Community ID (hash) usually, but here likely the SBT/profile? No, Community in Registry is bytes32 ID.
20
+ // Wait, Registry defines Community as a Role (ROLE_COMMUNITY).
21
+ // The "Community" entity usually implies a collection of contracts (Token, maybe Paymaster).
22
+ }
23
+
24
+ /**
25
+ * Client for Community Managers (`ROLE_COMMUNITY`)
26
+ */
27
+ export class CommunityClient extends BaseClient {
28
+ public sbtAddress?: Address;
29
+ public factoryAddress?: Address;
30
+ public reputationAddress?: Address;
31
+
32
+ constructor(config: CommunityClientConfig) {
33
+ super(config);
34
+ this.sbtAddress = config.sbtAddress;
35
+ this.factoryAddress = config.factoryAddress;
36
+ this.reputationAddress = config.reputationAddress;
37
+ }
38
+
39
+ // ========================================
40
+ // 1. ็คพๅŒบๅˆ›ๅปบไธŽ้…็ฝฎ
41
+ // ========================================
42
+
43
+ /**
44
+ * Create a new Community Token (xPNTs) and register it.
45
+ * Note: In the current architecture, creating a community often involves:
46
+ * 1. Registering the ROLE_COMMUNITY on Registry (if not exists) -> usually manual or self-register
47
+ * 2. Deploying a Token (xPNTs) via Factory
48
+ * 3. Linking the Token to the Community in Registry
49
+ */
50
+ async createCommunityToken(params: CreateCommunityParams, options?: TransactionOptions): Promise<Hash> {
51
+ try {
52
+ if (!this.factoryAddress) {
53
+ throw new Error('Factory address required for this client');
54
+ }
55
+ const factory = xPNTsFactoryActions(this.factoryAddress);
56
+
57
+ // 1. Deploy Token
58
+ // Note: The address calculation should be handled via event parsing or predictive deployment
59
+ // For now, returning the transaction hash as per L1 pattern
60
+ return await factory(this.client).createToken({
61
+ name: params.name,
62
+ symbol: params.tokenSymbol,
63
+ community: '0x0000000000000000000000000000000000000000', // Default empty community mapping
64
+ account: options?.account
65
+ });
66
+ } catch (error) {
67
+ // Error is likely already an AAStarError from L1, but we wrap it for context
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Register self as a Community Manager.
74
+ * This method handles all necessary steps:
75
+ * 1. Checks and approves GToken to GTokenStaking
76
+ * 2. Encodes CommunityRoleData with provided parameters
77
+ * 3. Calls registerRoleSelf on Registry
78
+ *
79
+ * @param params Community registration parameters
80
+ * @param options Transaction options
81
+ * @returns Transaction hash
82
+ */
83
+ async registerAsCommunity(params: {
84
+ name: string;
85
+ ensName?: string;
86
+ website?: string;
87
+ description?: string;
88
+ logoURI?: string;
89
+ stakeAmount?: bigint;
90
+ }, options?: TransactionOptions): Promise<Hash> {
91
+ try {
92
+ const registryAddr = this.requireRegistry();
93
+ const registry = registryActions(registryAddr);
94
+ const gTokenStakingAddr = this.requireGTokenStaking();
95
+ const gTokenAddr = this.requireGToken();
96
+
97
+ // 1. Get ROLE_COMMUNITY
98
+ const roleCommunity = await registry(this.getStartPublicClient()).ROLE_COMMUNITY();
99
+
100
+ // 2. Prepare stake amount (default 30 GToken as per Registry config)
101
+ const stakeAmount = params.stakeAmount || parseEther('30');
102
+
103
+ // 3. Check and approve GToken to GTokenStaking if needed
104
+ const gToken = tokenActions();
105
+
106
+ const allowance = await gToken(this.getStartPublicClient()).allowance({
107
+ token: gTokenAddr,
108
+ owner: this.getAddress(),
109
+ spender: gTokenStakingAddr
110
+ });
111
+
112
+ if (allowance < stakeAmount) {
113
+ const approveHash = await gToken(this.client).approve({
114
+ token: gTokenAddr,
115
+ spender: gTokenStakingAddr,
116
+ amount: stakeAmount * BigInt(2), // Approve 2x for future use
117
+ account: options?.account
118
+ });
119
+ await (this.getStartPublicClient() as any).waitForTransactionReceipt({ hash: approveHash });
120
+ }
121
+
122
+ // 4. Encode CommunityRoleData
123
+ // struct CommunityRoleData { string name; string ensName; string website; string description; string logoURI; uint256 stakeAmount; }
124
+ const communityData = encodeAbiParameters(
125
+ parseAbiParameters('string, string, string, string, string, uint256'),
126
+ [
127
+ params.name,
128
+ params.ensName || '',
129
+ params.website || '',
130
+ params.description || `${params.name} Community`,
131
+ params.logoURI || '',
132
+ stakeAmount
133
+ ]
134
+ );
135
+
136
+ // 5. Register role
137
+ return await registry(this.client).registerRoleSelf({
138
+ roleId: roleCommunity,
139
+ data: communityData,
140
+ account: options?.account
141
+ });
142
+ } catch (error) {
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ // ========================================
148
+ // 2. ๆˆๅ‘˜็ฎก็†
149
+ // ========================================
150
+
151
+ /**
152
+ * Airdrop SBTs to users to make them members
153
+ */
154
+ async airdropSBT(users: Address[], roleId: bigint, options?: TransactionOptions): Promise<Hash> {
155
+ try {
156
+ if (!this.sbtAddress) throw new Error('SBT address required for this client');
157
+ const sbt = sbtActions(this.sbtAddress);
158
+
159
+ if (users.length === 1) {
160
+ // Convert roleId to Hex (bytes32)
161
+ const roleIdHex = `0x${roleId.toString(16).padStart(64, '0')}` as Hash;
162
+
163
+ return await sbt(this.client).mintForRole({
164
+ user: users[0],
165
+ roleId: roleIdHex,
166
+ roleData: '0x',
167
+ account: options?.account
168
+ });
169
+ }
170
+
171
+ throw new Error('Batch airdrop not fully implemented in L1 yet, use single user');
172
+ } catch (error) {
173
+ throw error;
174
+ }
175
+ }
176
+
177
+ // ========================================
178
+ // 3. ไฟก่ช‰็ณป็ปŸ
179
+ // ========================================
180
+
181
+ async setReputationRule(ruleId: bigint, ruleConfig: any, options?: TransactionOptions): Promise<Hash> {
182
+ try {
183
+ if (!this.reputationAddress) throw new Error('Reputation address required for this client');
184
+ const reputation = reputationActions(this.reputationAddress);
185
+
186
+ const ruleIdHex = `0x${ruleId.toString(16).padStart(64, '0')}` as Hash;
187
+
188
+ return await reputation(this.client).setReputationRule({
189
+ ruleId: ruleIdHex,
190
+ rule: ruleConfig,
191
+ account: options?.account
192
+ });
193
+ } catch (error) {
194
+ throw error;
195
+ }
196
+ }
197
+
198
+ // ========================================
199
+ // 4. ็ฎก็†ๅŠŸ่ƒฝ
200
+ // ========================================
201
+
202
+ /**
203
+ * Revoke membership (Burn SBT)
204
+ */
205
+ async revokeMembership(userAddr: Address, options?: TransactionOptions): Promise<Hash> {
206
+ try {
207
+ if (!this.sbtAddress) throw new Error('SBT address required for this client');
208
+ const sbt = sbtActions(this.sbtAddress);
209
+
210
+ return await sbt(this.client).burnSBT({
211
+ user: userAddr,
212
+ account: options?.account
213
+ });
214
+ } catch (error) {
215
+ throw error;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Transfer ownership of the Community Token
221
+ */
222
+ async transferCommunityTokenOwnership(tokenAddress: Address, newOwner: Address, options?: TransactionOptions): Promise<Hash> {
223
+ try {
224
+ const token = tokenActions()(this.client);
225
+
226
+ return await token.transferOwnership({
227
+ token: tokenAddress,
228
+ newOwner,
229
+ account: options?.account
230
+ });
231
+ } catch (error) {
232
+ throw error;
233
+ }
234
+ }
235
+ }