@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,447 @@
1
+ import { type Address, type Hash, type Hex } from 'viem';
2
+ import { BaseClient, type ClientConfig, type TransactionOptions } from '@aastar/core';
3
+ import { accountActions, sbtActions, tokenActions, entryPointActions, stakingActions, registryActions, paymasterActions, superPaymasterActions } from '@aastar/core';
4
+ import { bundlerActions, type UserOperation, getUserOperationHash } from 'viem/account-abstraction';
5
+ import { encodeFunctionData } from 'viem';
6
+
7
+ export interface UserClientConfig extends ClientConfig {
8
+ accountAddress: Address; // The AA account address
9
+ sbtAddress?: Address;
10
+ entryPointAddress?: Address;
11
+ superPaymasterAddress?: Address; // For sponsorship queries
12
+ gTokenStakingAddress?: Address; // For staking/investing
13
+ registryAddress?: Address; // For role management
14
+ gTokenAddress?: Address; // For fee payment approval
15
+ }
16
+
17
+ export class UserClient extends BaseClient {
18
+ public accountAddress: Address;
19
+ public sbtAddress?: Address;
20
+ public entryPointAddress?: Address;
21
+ public gTokenStakingAddress?: Address;
22
+ public registryAddress?: Address;
23
+ public gTokenAddress?: Address;
24
+
25
+ constructor(config: UserClientConfig) {
26
+ super(config);
27
+ this.accountAddress = config.accountAddress;
28
+ this.sbtAddress = config.sbtAddress;
29
+ this.entryPointAddress = config.entryPointAddress;
30
+ this.gTokenStakingAddress = config.gTokenStakingAddress;
31
+ this.registryAddress = config.registryAddress;
32
+ this.gTokenAddress = config.gTokenAddress;
33
+ }
34
+
35
+ // ========================================
36
+ // 1. 账户基本操作 (基于 L1 simpleAccountActions)
37
+ // ========================================
38
+
39
+ /**
40
+ * Get the nonce of the account from EntryPoint (more reliable for 4337)
41
+ */
42
+ async getNonce(key: bigint = 0n): Promise<bigint> {
43
+ try {
44
+ if (!this.entryPointAddress) {
45
+ throw new Error('EntryPoint address required for this client');
46
+ }
47
+ const entryPoint = entryPointActions(this.entryPointAddress);
48
+ return await entryPoint(this.getStartPublicClient()).getNonce({
49
+ sender: this.accountAddress,
50
+ key
51
+ });
52
+ } catch (error) {
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get the owner of the AA account
59
+ */
60
+ async getOwner(): Promise<Address> {
61
+ try {
62
+ const account = accountActions(this.accountAddress);
63
+ return await account(this.getStartPublicClient()).owner();
64
+ } catch (error) {
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Execute a transaction from the AA account
71
+ */
72
+ async execute(target: Address, value: bigint, data: Hex, options?: TransactionOptions): Promise<Hash> {
73
+ try {
74
+ const account = accountActions(this.accountAddress);
75
+
76
+ // Use standard AA execute
77
+ return await account(this.client).execute({
78
+ dest: target,
79
+ value,
80
+ func: data,
81
+ account: options?.account
82
+ });
83
+ } catch (error) {
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Execute a batch of transactions
90
+ */
91
+ async executeBatch(targets: Address[], values: bigint[], datas: Hex[], options?: TransactionOptions): Promise<Hash> {
92
+ try {
93
+ const account = accountActions(this.accountAddress);
94
+
95
+ return await account(this.client).executeBatch({
96
+ dest: targets,
97
+ value: values,
98
+ func: datas,
99
+ account: options?.account
100
+ });
101
+ } catch (error) {
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ // ========================================
107
+ // identity 与 SBT (基于 L1 sbtActions)
108
+ // ========================================
109
+
110
+ /**
111
+ * Get user's SBT balance
112
+ */
113
+ async getSBTBalance(): Promise<bigint> {
114
+ try {
115
+ if (!this.sbtAddress) throw new Error('SBT address required for this client');
116
+ const sbt = sbtActions(this.sbtAddress);
117
+
118
+ return await sbt(this.getStartPublicClient()).balanceOf({
119
+ owner: this.accountAddress
120
+ });
121
+ } catch (error) {
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Self-mint SBT for a role (user self-service)
128
+ */
129
+ async mintSBT(roleId: Hex, options?: TransactionOptions): Promise<Hash> {
130
+ try {
131
+ if (!this.sbtAddress) throw new Error('SBT address required for this client');
132
+ const sbt = sbtActions(this.sbtAddress);
133
+
134
+ return await sbt(this.client).mintForRole({
135
+ user: this.accountAddress,
136
+ roleId,
137
+ roleData: '0x',
138
+ account: options?.account
139
+ });
140
+ } catch (error) {
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ // ========================================
146
+ // 3. 资产管理 (基于 L1 tokenActions)
147
+ // ========================================
148
+
149
+ /**
150
+ * Transfer GToken or any ERC20
151
+ */
152
+ async transferToken(token: Address, to: Address, amount: bigint, options?: TransactionOptions): Promise<Hash> {
153
+ try {
154
+ const tokens = tokenActions()(this.client);
155
+
156
+ return await tokens.transfer({
157
+ token,
158
+ to,
159
+ amount,
160
+ account: options?.account
161
+ });
162
+ } catch (error) {
163
+ throw error;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Get Token Balance
169
+ */
170
+ async getTokenBalance(token: Address): Promise<bigint> {
171
+ try {
172
+ const tokens = tokenActions()(this.getStartPublicClient());
173
+
174
+ return await tokens.balanceOf({
175
+ token,
176
+ account: this.accountAddress
177
+ });
178
+ } catch (error) {
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ // ========================================
184
+ // 4. 委托与质押 (Delegation & Staking)
185
+ // ========================================
186
+
187
+ /**
188
+ * Delegate stake to a role (Delegate to an operator/community)
189
+ */
190
+ async stakeForRole(roleId: Hex, amount: bigint, options?: TransactionOptions): Promise<Hash> {
191
+ try {
192
+ if (!this.gTokenStakingAddress) throw new Error('GTokenStaking address required for this client');
193
+ const staking = stakingActions(this.gTokenStakingAddress);
194
+
195
+ return await staking(this.client).lockStake({
196
+ user: this.accountAddress,
197
+ roleId,
198
+ stakeAmount: amount,
199
+ entryBurn: 0n,
200
+ payer: this.accountAddress,
201
+ account: options?.account
202
+ });
203
+ } catch (error) {
204
+ throw error;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Unstake from a role
210
+ */
211
+ async unstakeFromRole(roleId: Hex, options?: TransactionOptions): Promise<Hash> {
212
+ try {
213
+ if (!this.gTokenStakingAddress) throw new Error('GTokenStaking address required for this client');
214
+ const staking = stakingActions(this.gTokenStakingAddress);
215
+
216
+ return await staking(this.client).unlockAndTransfer({
217
+ user: this.accountAddress,
218
+ roleId,
219
+ account: options?.account
220
+ });
221
+ } catch (error) {
222
+ throw error;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Get staked balance for a specific role
228
+ */
229
+ async getStakedBalance(roleId: Hex): Promise<bigint> {
230
+ try {
231
+ if (!this.gTokenStakingAddress) throw new Error('GTokenStaking address required for this client');
232
+ const staking = stakingActions(this.gTokenStakingAddress);
233
+
234
+ return await staking(this.getStartPublicClient()).getLockedStake({
235
+ user: this.accountAddress,
236
+ roleId
237
+ });
238
+ } catch (error) {
239
+ throw error;
240
+ }
241
+ }
242
+
243
+ // ========================================
244
+ // 5. 生命周期管理 (Lifecycle)
245
+ // ========================================
246
+
247
+ /**
248
+ * Exit a specific role (Cleanup registry status)
249
+ */
250
+ async exitRole(roleId: Hex, options?: TransactionOptions): Promise<Hash> {
251
+ try {
252
+ if (!this.registryAddress) throw new Error('Registry address required for this client');
253
+ const registry = registryActions(this.registryAddress);
254
+
255
+ return await registry(this.client).exitRole({
256
+ roleId,
257
+ account: options?.account
258
+ });
259
+ } catch (error) {
260
+ throw error;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Leave a community (Burn SBT and clean up)
266
+ */
267
+ async leaveCommunity(community: Address, options?: TransactionOptions): Promise<Hash> {
268
+ try {
269
+ if (!this.sbtAddress) throw new Error('SBT address required for this client');
270
+ const sbt = sbtActions(this.sbtAddress);
271
+
272
+ return await sbt(this.client).leaveCommunity({
273
+ community,
274
+ account: options?.account
275
+ });
276
+ } catch (error) {
277
+ throw error;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Register as EndUser (One-click: Approve + Register)
283
+ * Handles GToken approval to Staking contract and Role registration.
284
+ */
285
+ async registerAsEndUser(communityAddress: Address, stakeAmount: bigint, options?: TransactionOptions): Promise<Hash> {
286
+ try {
287
+ if (!this.registryAddress) throw new Error('Registry address required for this client');
288
+ if (!this.gTokenStakingAddress) throw new Error('GTokenStaking address required for this client');
289
+ if (!this.gTokenAddress) throw new Error('GToken address required for this client');
290
+
291
+ const { encodeAbiParameters, keccak256, toBytes, parseEther } = await import('viem');
292
+ const ROLE_ENDUSER = keccak256(toBytes("ENDUSER"));
293
+ // Correct mapping for Registry Actions
294
+ const registry = registryActions(this.registryAddress);
295
+ const tokens = tokenActions()(this.getStartPublicClient());
296
+
297
+ // 1. Check Allowance
298
+ const allowance = await tokens.allowance({
299
+ token: this.gTokenAddress,
300
+ owner: this.accountAddress,
301
+ spender: this.gTokenStakingAddress
302
+ });
303
+
304
+ const txs: { target: Address, value: bigint, data: Hex }[] = [];
305
+
306
+ if (allowance < stakeAmount) {
307
+ const approveData = encodeFunctionData({
308
+ abi: [{name:'approve', type:'function', inputs:[{type:'address'},{type:'uint256'}], outputs:[{type:'bool'}], stateMutability:'nonpayable'}],
309
+ functionName: 'approve',
310
+ args: [this.gTokenStakingAddress, parseEther('1000')]
311
+ });
312
+ txs.push({ target: this.gTokenAddress, value: 0n, data: approveData });
313
+ }
314
+
315
+ // 2. Construct Register Call
316
+ // struct EndUserRoleData { address account; address community; string avatarURI; string ensName; uint256 stakeAmount; }
317
+ const roleData = encodeAbiParameters(
318
+ [
319
+ { type: 'address', name: 'account' },
320
+ { type: 'address', name: 'community' },
321
+ { type: 'string', name: 'avatarURI' },
322
+ { type: 'string', name: 'ensName' },
323
+ { type: 'uint256', name: 'stakeAmount' }
324
+ ],
325
+ [
326
+ this.accountAddress,
327
+ communityAddress,
328
+ '',
329
+ '',
330
+ stakeAmount
331
+ ]
332
+ );
333
+
334
+ const registerData = encodeFunctionData({
335
+ abi: [{ name: 'registerRoleSelf', type: 'function', inputs: [{type:'bytes32'}, {type:'bytes'}], outputs: [{type:'uint256'}], stateMutability: 'nonpayable' }],
336
+ functionName: 'registerRoleSelf',
337
+ args: [ROLE_ENDUSER, roleData]
338
+ });
339
+
340
+ txs.push({ target: this.registryAddress, value: 0n, data: registerData });
341
+
342
+ // 3. Execute
343
+ if (txs.length === 1) {
344
+ return await this.execute(txs[0].target, txs[0].value, txs[0].data, options);
345
+ } else {
346
+ return await this.executeBatch(
347
+ txs.map(t => t.target),
348
+ txs.map(t => t.value),
349
+ txs.map(t => t.data),
350
+ options
351
+ );
352
+ }
353
+ } catch (error) {
354
+ throw error;
355
+ }
356
+ }
357
+
358
+ // ========================================
359
+ // 6. Gasless Execution (Advanced)
360
+ // ========================================
361
+
362
+ /**
363
+ * Execute a transaction with Gasless Sponsorship
364
+ */
365
+ async executeGasless(params: {
366
+ target: Address;
367
+ value: bigint;
368
+ data: Hex;
369
+ paymaster: Address;
370
+ paymasterType: 'V4' | 'Super';
371
+ }, options?: TransactionOptions): Promise<Hash> {
372
+ try {
373
+ const client = (this.client as any).extend(bundlerActions);
374
+ const ep = this.requireEntryPoint();
375
+ const publicClient = this.getStartPublicClient();
376
+
377
+ // 1. Prepare Call Data
378
+ const callData = encodeFunctionData({
379
+ abi: [{ name: 'execute', type: 'function', inputs: [{ name: 'dest', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'func', type: 'bytes' }], outputs: [] }],
380
+ functionName: 'execute',
381
+ args: [params.target, params.value, params.data]
382
+ });
383
+
384
+ // 2. Prepare Paymaster Data
385
+ let paymasterAndData: Hex = params.paymaster;
386
+ // Note: In real scenarios, PM V4 or Super might need additional encoded data.
387
+ // For now, we use the address as the base.
388
+
389
+ // 3. Estimate Gas
390
+ const sender = this.accountAddress;
391
+ const nonce = await this.getNonce();
392
+
393
+ const userOpPartial = {
394
+ sender,
395
+ nonce,
396
+ initCode: '0x' as Hex,
397
+ callData,
398
+ paymasterAndData,
399
+ signature: '0x' as Hex
400
+ };
401
+
402
+ const gasEstimate = await (client as any).estimateUserOperationGas({
403
+ userOperation: userOpPartial as any,
404
+ entryPoint: ep
405
+ });
406
+
407
+ // 4. Construct Final UserOp
408
+ const fees = await (publicClient as any).estimateFeesPerGas();
409
+
410
+ const userOp: UserOperation = {
411
+ ...userOpPartial,
412
+ callGasLimit: gasEstimate.callGasLimit,
413
+ verificationGasLimit: gasEstimate.verificationGasLimit + 50000n,
414
+ preVerificationGas: gasEstimate.preVerificationGas,
415
+ maxFeePerGas: fees.maxFeePerGas || fees.gasPrice || 1000000000n,
416
+ maxPriorityFeePerGas: fees.maxPriorityFeePerGas || 1000000000n
417
+ };
418
+
419
+ // 5. Sign
420
+ const chainId = this.client.chain?.id || 31337;
421
+ const hash = getUserOperationHash({
422
+ userOperation: userOp,
423
+ entryPointAddress: ep,
424
+ entryPointVersion: '0.7',
425
+ chainId
426
+ });
427
+
428
+ const signature = await this.client.signMessage({
429
+ message: { raw: hash },
430
+ account: this.client.account
431
+ });
432
+
433
+ const signedUserOp = {
434
+ ...userOp,
435
+ signature
436
+ };
437
+
438
+ // 6. Send
439
+ return await (client as any).sendUserOperation({
440
+ userOperation: signedUserOp,
441
+ entryPoint: ep
442
+ });
443
+ } catch (error) {
444
+ throw error;
445
+ }
446
+ }
447
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './CommunityClient.js';
2
+ export * from './UserClient.js';