@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,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
+ }
@@ -0,0 +1,2 @@
1
+ export * from './CommunityClient.js';
2
+ export * from './UserClient.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './CommunityClient.js';
2
+ export * from './UserClient.js';
@@ -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
+ }