@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,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