@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 AAStar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { CommunityClient } from '../src/CommunityClient';
|
|
3
|
+
import { createMockPublicClient, createMockWalletClient, resetMocks } from './mocks/client';
|
|
4
|
+
import { parseEther } from 'viem';
|
|
5
|
+
|
|
6
|
+
// Define mocks using vi.hoisted to ensure they are available for vi.mock
|
|
7
|
+
const mocks = vi.hoisted(() => {
|
|
8
|
+
const mockXPNTsFactory = { createToken: vi.fn() };
|
|
9
|
+
const mockRegistry = { ROLE_COMMUNITY: vi.fn(), registerRoleSelf: vi.fn() };
|
|
10
|
+
const mockToken = { allowance: vi.fn(), approve: vi.fn(), transferOwnership: vi.fn() };
|
|
11
|
+
const mockSBT = { airdropMint: vi.fn(), mintForRole: vi.fn(), burnSBT: vi.fn() };
|
|
12
|
+
const mockReputation = { setReputationRule: vi.fn() };
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
mockXPNTsFactory,
|
|
16
|
+
mockRegistry,
|
|
17
|
+
mockToken,
|
|
18
|
+
mockSBT,
|
|
19
|
+
mockReputation,
|
|
20
|
+
// The actions typically return a function that accepts the client and returns the methods
|
|
21
|
+
// xPNTsFactoryActions(addr)(client).createToken(...)
|
|
22
|
+
mockXPNTsFactoryActions: vi.fn(() => () => mockXPNTsFactory),
|
|
23
|
+
mockRegistryActions: vi.fn(() => () => mockRegistry),
|
|
24
|
+
mockTokenActions: vi.fn(() => () => mockToken),
|
|
25
|
+
mockSBTActions: vi.fn(() => () => mockSBT),
|
|
26
|
+
mockReputationActions: vi.fn(() => () => mockReputation),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
vi.mock('@aastar/core', async () => {
|
|
31
|
+
const actual = await vi.importActual('@aastar/core');
|
|
32
|
+
return {
|
|
33
|
+
...actual,
|
|
34
|
+
xPNTsFactoryActions: mocks.mockXPNTsFactoryActions,
|
|
35
|
+
registryActions: mocks.mockRegistryActions,
|
|
36
|
+
tokenActions: mocks.mockTokenActions,
|
|
37
|
+
sbtActions: mocks.mockSBTActions,
|
|
38
|
+
reputationActions: mocks.mockReputationActions,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('CommunityClient', () => {
|
|
43
|
+
let client: CommunityClient;
|
|
44
|
+
const mockPublicClient = createMockPublicClient();
|
|
45
|
+
const mockWalletClient = createMockWalletClient();
|
|
46
|
+
|
|
47
|
+
const config = {
|
|
48
|
+
params: {
|
|
49
|
+
chainId: 11155111,
|
|
50
|
+
rpcUrl: 'https://rpc.sepolia.org',
|
|
51
|
+
},
|
|
52
|
+
services: {
|
|
53
|
+
paymasterUrl: 'https://paymaster.example.com',
|
|
54
|
+
},
|
|
55
|
+
// Required by BaseClient
|
|
56
|
+
client: mockWalletClient as any,
|
|
57
|
+
registryAddress: '0xRegistryAddress' as `0x${string}`,
|
|
58
|
+
gTokenAddress: '0xGTokenAddress' as `0x${string}`,
|
|
59
|
+
gTokenStakingAddress: '0xStakingAddress' as `0x${string}`,
|
|
60
|
+
// Required by CommunityClient
|
|
61
|
+
sbtAddress: '0xSBTAddress' as `0x${string}`,
|
|
62
|
+
factoryAddress: '0xFactoryAddress' as `0x${string}`,
|
|
63
|
+
reputationAddress: '0xReputationAddress' as `0x${string}`,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
resetMocks();
|
|
68
|
+
vi.clearAllMocks();
|
|
69
|
+
|
|
70
|
+
// Setup default successful mocks
|
|
71
|
+
mocks.mockRegistry.ROLE_COMMUNITY.mockResolvedValue(100n);
|
|
72
|
+
mocks.mockToken.allowance.mockResolvedValue(parseEther('100'));
|
|
73
|
+
mocks.mockXPNTsFactory.createToken.mockResolvedValue('0xTxHash');
|
|
74
|
+
mocks.mockRegistry.registerRoleSelf.mockResolvedValue('0xTxHash');
|
|
75
|
+
mocks.mockSBT.airdropMint.mockResolvedValue('0xTxHash');
|
|
76
|
+
mocks.mockSBT.burnSBT.mockResolvedValue('0xTxHash');
|
|
77
|
+
mocks.mockReputation.setReputationRule.mockResolvedValue('0xTxHash');
|
|
78
|
+
mocks.mockToken.transferOwnership.mockResolvedValue('0xTxHash');
|
|
79
|
+
|
|
80
|
+
client = new CommunityClient(config);
|
|
81
|
+
(client as any).client = mockWalletClient;
|
|
82
|
+
(client as any).publicClient = mockPublicClient;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('createCommunityToken', () => {
|
|
86
|
+
it('should successfully create a token', async () => {
|
|
87
|
+
const result = await client.createCommunityToken({
|
|
88
|
+
name: 'Test Token',
|
|
89
|
+
tokenSymbol: 'TEST'
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(mocks.mockXPNTsFactoryActions).toHaveBeenCalledWith(config.factoryAddress);
|
|
93
|
+
expect(mocks.mockXPNTsFactory.createToken).toHaveBeenCalledWith(expect.objectContaining({
|
|
94
|
+
name: 'Test Token',
|
|
95
|
+
symbol: 'TEST',
|
|
96
|
+
community: expect.stringContaining('0x000')
|
|
97
|
+
}));
|
|
98
|
+
expect(result).toBe('0xTxHash');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should throw if factoryAddress is missing', async () => {
|
|
102
|
+
client.factoryAddress = undefined;
|
|
103
|
+
await expect(client.createCommunityToken({ name: 'T', tokenSymbol: 'T' }))
|
|
104
|
+
.rejects.toThrow('Factory address required');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should propagate core errors', async () => {
|
|
108
|
+
mocks.mockXPNTsFactory.createToken.mockRejectedValue(new Error('Core Error'));
|
|
109
|
+
await expect(client.createCommunityToken({ name: 'T', tokenSymbol: 'T' }))
|
|
110
|
+
.rejects.toThrow('Core Error');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('registerAsCommunity', () => {
|
|
115
|
+
it('should register successfully with sufficient allowance', async () => {
|
|
116
|
+
mocks.mockToken.allowance.mockResolvedValue(parseEther('100'));
|
|
117
|
+
|
|
118
|
+
await client.registerAsCommunity({ name: 'My Community' });
|
|
119
|
+
|
|
120
|
+
expect(mocks.mockToken.approve).not.toHaveBeenCalled();
|
|
121
|
+
expect(mocks.mockRegistry.registerRoleSelf).toHaveBeenCalledWith(expect.objectContaining({
|
|
122
|
+
roleId: 100n
|
|
123
|
+
}));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should approve if allowance is insufficient', async () => {
|
|
127
|
+
mocks.mockToken.allowance.mockResolvedValue(0n);
|
|
128
|
+
mocks.mockToken.approve.mockResolvedValue('0xApproveHash');
|
|
129
|
+
|
|
130
|
+
await client.registerAsCommunity({ name: 'My Community' });
|
|
131
|
+
|
|
132
|
+
expect(mocks.mockToken.approve).toHaveBeenCalled();
|
|
133
|
+
expect(mockPublicClient.waitForTransactionReceipt).toHaveBeenCalledWith({ hash: '0xApproveHash' });
|
|
134
|
+
expect(mocks.mockRegistry.registerRoleSelf).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('airdropSBT', () => {
|
|
139
|
+
it('should airdrop to a single user', async () => {
|
|
140
|
+
const user = '0xUser' as `0x${string}`;
|
|
141
|
+
await client.airdropSBT([user], 1n);
|
|
142
|
+
|
|
143
|
+
expect(mocks.mockSBTActions).toHaveBeenCalledWith(config.sbtAddress);
|
|
144
|
+
expect(mocks.mockSBT.mintForRole).toHaveBeenCalledWith(expect.objectContaining({
|
|
145
|
+
user: user
|
|
146
|
+
}));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should throw if sbtAddress is missing', async () => {
|
|
150
|
+
client.sbtAddress = undefined;
|
|
151
|
+
await expect(client.airdropSBT(['0xUser'], 1n))
|
|
152
|
+
.rejects.toThrow('SBT address required');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should throw if multiple users provided (not implemented)', async () => {
|
|
156
|
+
await expect(client.airdropSBT(['0xU1', '0xU2'], 1n))
|
|
157
|
+
.rejects.toThrow('Batch airdrop not fully implemented');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('setReputationRule', () => {
|
|
162
|
+
it('setReputationRule should call reputation setReputationRule', async () => {
|
|
163
|
+
await client.setReputationRule(1n, {});
|
|
164
|
+
expect(mocks.mockReputationActions).toHaveBeenCalledWith(config.reputationAddress);
|
|
165
|
+
// Mock passes ruleId as hex because implementation converts it
|
|
166
|
+
expect(mocks.mockReputation.setReputationRule).toHaveBeenCalledWith(expect.objectContaining({
|
|
167
|
+
ruleId: '0x0000000000000000000000000000000000000000000000000000000000000001'
|
|
168
|
+
}));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should throw if reputationAddress is missing', async () => {
|
|
172
|
+
client.reputationAddress = undefined;
|
|
173
|
+
await expect(client.setReputationRule(1n, {}))
|
|
174
|
+
.rejects.toThrow('Reputation address required');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('revokeMembership', () => {
|
|
179
|
+
it('should burn SBT', async () => {
|
|
180
|
+
const user = '0xUser' as `0x${string}`;
|
|
181
|
+
await client.revokeMembership(user);
|
|
182
|
+
expect(mocks.mockSBTActions).toHaveBeenCalledWith(config.sbtAddress);
|
|
183
|
+
expect(mocks.mockSBT.burnSBT).toHaveBeenCalledWith(expect.objectContaining({
|
|
184
|
+
user: user
|
|
185
|
+
}));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should throw if sbtAddress is missing', async () => {
|
|
189
|
+
client.sbtAddress = undefined;
|
|
190
|
+
await expect(client.revokeMembership(123n))
|
|
191
|
+
.rejects.toThrow('SBT address required');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('transferCommunityTokenOwnership', () => {
|
|
196
|
+
it('should transfer ownership', async () => {
|
|
197
|
+
await client.transferCommunityTokenOwnership('0xToken', '0x2222222222222222222222222222222222222222');
|
|
198
|
+
expect(mocks.mockTokenActions).toHaveBeenCalled();
|
|
199
|
+
expect(mocks.mockToken.transferOwnership).toHaveBeenCalledWith(expect.objectContaining({
|
|
200
|
+
token: '0xToken',
|
|
201
|
+
newOwner: '0x2222222222222222222222222222222222222222'
|
|
202
|
+
}));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { UserClient } from '../src/UserClient';
|
|
3
|
+
import { createMockPublicClient, createMockWalletClient, resetMocks } from './mocks/client';
|
|
4
|
+
import { parseEther } from 'viem';
|
|
5
|
+
|
|
6
|
+
// Define mocks using vi.hoisted
|
|
7
|
+
const mocks = vi.hoisted(() => {
|
|
8
|
+
const mockAccount = { owner: vi.fn(), execute: vi.fn(), executeBatch: vi.fn() };
|
|
9
|
+
const mockSBT = { balanceOf: vi.fn(), mintForRole: vi.fn(), leaveCommunity: vi.fn() };
|
|
10
|
+
const mockToken = { transfer: vi.fn(), balanceOf: vi.fn(), allowance: vi.fn() };
|
|
11
|
+
const mockStaking = { lockStake: vi.fn(), unlockAndTransfer: vi.fn(), getLockedStake: vi.fn() };
|
|
12
|
+
const mockRegistry = { exitRole: vi.fn(), registerRoleSelf: vi.fn() }; // registerRoleSelf calls execute in implementation logic but mocked here just in case? No, registerAsEndUser constructs tx data and calls execute/executeBatch.
|
|
13
|
+
// Wait, registerAsEndUser calls this.execute or this.executeBatch, so we don't mock registryActions for that call directly if we test logic flow,
|
|
14
|
+
// BUT registerAsEndUser calls registryActions(this.registryAddress) effectively to get address? No, it uses this.registryAddress.
|
|
15
|
+
// It calls registryActions to get ABI maybe? In the code:
|
|
16
|
+
// const registry = registryActions(this.registryAddress);
|
|
17
|
+
// ... encodeFunctionData ...
|
|
18
|
+
// registerAsEndUser MANUALLY constructs tx data using encodeFunctionData. It DOES NOT use registryActions returned object methods for the transaction itself to send via execute.
|
|
19
|
+
// EXCEPT: It initializes registry = registryActions(...) which might be unused in logic?
|
|
20
|
+
// Let's look at source:
|
|
21
|
+
// const registry = registryActions(this.registryAddress);
|
|
22
|
+
// ...
|
|
23
|
+
// It effectively uses it for nothing if it builds manually?
|
|
24
|
+
// Line 293: const registry = registryActions(this.registryAddress);
|
|
25
|
+
// Then lines 315-337: const roleData = ... encodeFunctionData ...
|
|
26
|
+
// So registryActions might be just for show? Or maybe strict mode requires it.
|
|
27
|
+
|
|
28
|
+
const mockEntryPoint = { getNonce: vi.fn() };
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
mockAccount,
|
|
32
|
+
mockSBT,
|
|
33
|
+
mockToken,
|
|
34
|
+
mockStaking,
|
|
35
|
+
mockRegistry,
|
|
36
|
+
mockEntryPoint,
|
|
37
|
+
// Factory functions
|
|
38
|
+
mockAccountActions: vi.fn(() => () => mockAccount),
|
|
39
|
+
mockSBTActions: vi.fn(() => () => mockSBT),
|
|
40
|
+
mockTokenActions: vi.fn(() => () => mockToken),
|
|
41
|
+
mockStakingActions: vi.fn(() => () => mockStaking),
|
|
42
|
+
mockRegistryActions: vi.fn(() => () => mockRegistry),
|
|
43
|
+
mockEntryPointActions: vi.fn(() => () => mockEntryPoint),
|
|
44
|
+
// Bundler actions (mock extend)
|
|
45
|
+
mockBundlerActions: {},
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
vi.mock('@aastar/core', async () => {
|
|
50
|
+
const actual = await vi.importActual('@aastar/core');
|
|
51
|
+
return {
|
|
52
|
+
...actual,
|
|
53
|
+
accountActions: mocks.mockAccountActions,
|
|
54
|
+
sbtActions: mocks.mockSBTActions,
|
|
55
|
+
tokenActions: mocks.mockTokenActions,
|
|
56
|
+
stakingActions: mocks.mockStakingActions,
|
|
57
|
+
registryActions: mocks.mockRegistryActions,
|
|
58
|
+
entryPointActions: mocks.mockEntryPointActions,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
vi.mock('viem/account-abstraction', async () => {
|
|
63
|
+
const actual = await vi.importActual('viem/account-abstraction');
|
|
64
|
+
return {
|
|
65
|
+
...actual,
|
|
66
|
+
bundlerActions: () => ({
|
|
67
|
+
estimateUserOperationGas: vi.fn().mockResolvedValue({
|
|
68
|
+
callGasLimit: 1000n,
|
|
69
|
+
verificationGasLimit: 1000n,
|
|
70
|
+
preVerificationGas: 1000n
|
|
71
|
+
}),
|
|
72
|
+
sendUserOperation: vi.fn().mockResolvedValue('0xUserOpHash')
|
|
73
|
+
}),
|
|
74
|
+
getUserOperationHash: vi.fn().mockReturnValue('0xHash'),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('UserClient', () => {
|
|
79
|
+
let client: UserClient;
|
|
80
|
+
const mockPublicClient = createMockPublicClient();
|
|
81
|
+
const mockWalletClient = createMockWalletClient();
|
|
82
|
+
|
|
83
|
+
const config = {
|
|
84
|
+
params: {
|
|
85
|
+
chainId: 11155111,
|
|
86
|
+
rpcUrl: 'https://rpc.sepolia.org',
|
|
87
|
+
},
|
|
88
|
+
services: {
|
|
89
|
+
paymasterUrl: 'https://paymaster.example.com',
|
|
90
|
+
},
|
|
91
|
+
client: mockWalletClient as any,
|
|
92
|
+
accountAddress: '0x1234567890123456789012345678901234567890' as `0x${string}`,
|
|
93
|
+
sbtAddress: '0x1111111111111111111111111111111111111111' as `0x${string}`,
|
|
94
|
+
entryPointAddress: '0x2222222222222222222222222222222222222222' as `0x${string}`,
|
|
95
|
+
gTokenStakingAddress: '0x3333333333333333333333333333333333333333' as `0x${string}`,
|
|
96
|
+
registryAddress: '0x4444444444444444444444444444444444444444' as `0x${string}`,
|
|
97
|
+
gTokenAddress: '0x5555555555555555555555555555555555555555' as `0x${string}`,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
resetMocks();
|
|
102
|
+
vi.clearAllMocks();
|
|
103
|
+
|
|
104
|
+
// Default Resolves
|
|
105
|
+
mocks.mockAccount.owner.mockResolvedValue('0xOwner');
|
|
106
|
+
mocks.mockAccount.execute.mockResolvedValue('0xTxHash');
|
|
107
|
+
mocks.mockAccount.executeBatch.mockResolvedValue('0xTxHash');
|
|
108
|
+
mocks.mockEntryPoint.getNonce.mockResolvedValue(0n);
|
|
109
|
+
mocks.mockSBT.balanceOf.mockResolvedValue(1n);
|
|
110
|
+
mocks.mockSBT.mintForRole.mockResolvedValue('0xTxHash');
|
|
111
|
+
mocks.mockToken.transfer.mockResolvedValue('0xTxHash');
|
|
112
|
+
mocks.mockToken.balanceOf.mockResolvedValue(100n);
|
|
113
|
+
mocks.mockToken.allowance.mockResolvedValue(parseEther('1000'));
|
|
114
|
+
mocks.mockStaking.lockStake.mockResolvedValue('0xTxHash');
|
|
115
|
+
mocks.mockStaking.unlockAndTransfer.mockResolvedValue('0xTxHash');
|
|
116
|
+
mocks.mockStaking.getLockedStake.mockResolvedValue(50n);
|
|
117
|
+
mocks.mockRegistry.exitRole.mockResolvedValue('0xTxHash');
|
|
118
|
+
mocks.mockSBT.leaveCommunity.mockResolvedValue('0xTxHash');
|
|
119
|
+
|
|
120
|
+
// Mock public client methods needed for gasless
|
|
121
|
+
(mockPublicClient as any).estimateFeesPerGas = vi.fn().mockResolvedValue({
|
|
122
|
+
maxFeePerGas: 10n,
|
|
123
|
+
maxPriorityFeePerGas: 1n
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
client = new UserClient(config);
|
|
127
|
+
(client as any).client = mockWalletClient;
|
|
128
|
+
(client as any).publicClient = mockPublicClient;
|
|
129
|
+
// Mock extend for gasless
|
|
130
|
+
(client as any).client.extend = vi.fn().mockReturnValue({
|
|
131
|
+
estimateUserOperationGas: vi.fn().mockResolvedValue({
|
|
132
|
+
callGasLimit: 1000n,
|
|
133
|
+
verificationGasLimit: 1000n,
|
|
134
|
+
preVerificationGas: 1000n
|
|
135
|
+
}),
|
|
136
|
+
sendUserOperation: vi.fn().mockResolvedValue('0xUserOpHash'),
|
|
137
|
+
signMessage: vi.fn().mockResolvedValue('0xSig')
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Also mock signMessage on the client itself if used directly
|
|
141
|
+
(client as any).client.signMessage = vi.fn().mockResolvedValue('0xSig');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('Basic Account Operations', () => {
|
|
145
|
+
it('getNonce should call entryPoint', async () => {
|
|
146
|
+
const nonce = await client.getNonce();
|
|
147
|
+
expect(mocks.mockEntryPointActions).toHaveBeenCalledWith(config.entryPointAddress);
|
|
148
|
+
expect(mocks.mockEntryPoint.getNonce).toHaveBeenCalledWith({
|
|
149
|
+
sender: config.accountAddress,
|
|
150
|
+
key: 0n
|
|
151
|
+
});
|
|
152
|
+
expect(nonce).toBe(0n);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('getOwner should call account', async () => {
|
|
156
|
+
const owner = await client.getOwner();
|
|
157
|
+
expect(mocks.mockAccountActions).toHaveBeenCalledWith(config.accountAddress);
|
|
158
|
+
expect(mocks.mockAccount.owner).toHaveBeenCalled();
|
|
159
|
+
expect(owner).toBe('0xOwner');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('execute should call account execute', async () => {
|
|
163
|
+
await client.execute('0x1111111111111111111111111111111111111111', 0n, '0x1234');
|
|
164
|
+
expect(mocks.mockAccountActions).toHaveBeenCalledWith(config.accountAddress);
|
|
165
|
+
expect(mocks.mockAccount.execute).toHaveBeenCalledWith(expect.objectContaining({
|
|
166
|
+
dest: '0x1111111111111111111111111111111111111111',
|
|
167
|
+
value: 0n,
|
|
168
|
+
func: '0x1234'
|
|
169
|
+
}));
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('SBT Operations', () => {
|
|
174
|
+
it('getSBTBalance should call sbt', async () => {
|
|
175
|
+
await client.getSBTBalance();
|
|
176
|
+
expect(mocks.mockSBTActions).toHaveBeenCalledWith(config.sbtAddress);
|
|
177
|
+
expect(mocks.mockSBT.balanceOf).toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('mintSBT should call sbt mintForRole', async () => {
|
|
181
|
+
await client.mintSBT('0x4444444444444444444444444444444444444444444444444444444444444444');
|
|
182
|
+
expect(mocks.mockSBT.mintForRole).toHaveBeenCalledWith(expect.objectContaining({
|
|
183
|
+
roleId: '0x4444444444444444444444444444444444444444444444444444444444444444',
|
|
184
|
+
user: config.accountAddress
|
|
185
|
+
}));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should throw if sbtAddress missing', async () => {
|
|
189
|
+
client.sbtAddress = undefined;
|
|
190
|
+
await expect(client.mintSBT('0x4444444444444444444444444444444444444444444444444444444444444444')).rejects.toThrow('SBT address required');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('Asset Operations', () => {
|
|
195
|
+
it('transferToken should call token transfer', async () => {
|
|
196
|
+
await client.transferToken('0x5555555555555555555555555555555555555555', '0x6666666666666666666666666666666666666666', 100n);
|
|
197
|
+
expect(mocks.mockToken.transfer).toHaveBeenCalledWith(expect.objectContaining({
|
|
198
|
+
token: '0x5555555555555555555555555555555555555555',
|
|
199
|
+
to: '0x6666666666666666666666666666666666666666',
|
|
200
|
+
amount: 100n
|
|
201
|
+
}));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('getTokenBalance should call token balanceOf', async () => {
|
|
205
|
+
await client.getTokenBalance('0x5555555555555555555555555555555555555555');
|
|
206
|
+
expect(mocks.mockToken.balanceOf).toHaveBeenCalledWith(expect.objectContaining({
|
|
207
|
+
token: '0x5555555555555555555555555555555555555555',
|
|
208
|
+
account: config.accountAddress
|
|
209
|
+
}));
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('Staking Operations', () => {
|
|
214
|
+
it('stakeForRole should call lockStake', async () => {
|
|
215
|
+
await client.stakeForRole('0x4444444444444444444444444444444444444444444444444444444444444444', 100n);
|
|
216
|
+
expect(mocks.mockStakingActions).toHaveBeenCalledWith(config.gTokenStakingAddress);
|
|
217
|
+
expect(mocks.mockStaking.lockStake).toHaveBeenCalledWith(expect.objectContaining({
|
|
218
|
+
user: config.accountAddress,
|
|
219
|
+
roleId: '0x4444444444444444444444444444444444444444444444444444444444444444',
|
|
220
|
+
stakeAmount: 100n
|
|
221
|
+
}));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('unstakeFromRole should call unlockAndTransfer', async () => {
|
|
225
|
+
await client.unstakeFromRole('0x4444444444444444444444444444444444444444444444444444444444444444');
|
|
226
|
+
expect(mocks.mockStaking.unlockAndTransfer).toHaveBeenCalledWith(expect.objectContaining({
|
|
227
|
+
user: config.accountAddress,
|
|
228
|
+
roleId: '0x4444444444444444444444444444444444444444444444444444444444444444'
|
|
229
|
+
}));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('getStakedBalance should call getLockedStake', async () => {
|
|
233
|
+
await client.getStakedBalance('0x4444444444444444444444444444444444444444444444444444444444444444');
|
|
234
|
+
expect(mocks.mockStaking.getLockedStake).toHaveBeenCalled();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('Lifecycle', () => {
|
|
239
|
+
it('exitRole should call registry exitRole', async () => {
|
|
240
|
+
await client.exitRole('0x4444444444444444444444444444444444444444444444444444444444444444');
|
|
241
|
+
expect(mocks.mockRegistryActions).toHaveBeenCalledWith(config.registryAddress);
|
|
242
|
+
expect(mocks.mockRegistry.exitRole).toHaveBeenCalledWith(expect.objectContaining({
|
|
243
|
+
roleId: '0x4444444444444444444444444444444444444444444444444444444444444444'
|
|
244
|
+
}));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('leaveCommunity should call sbt leaveCommunity', async () => {
|
|
248
|
+
await client.leaveCommunity('0x3333333333333333333333333333333333333333');
|
|
249
|
+
expect(mocks.mockSBT.leaveCommunity).toHaveBeenCalledWith(expect.objectContaining({
|
|
250
|
+
community: '0x3333333333333333333333333333333333333333'
|
|
251
|
+
}));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('registerAsEndUser should execute batch (approve + register)', async () => {
|
|
255
|
+
mocks.mockToken.allowance.mockResolvedValue(0n); // Force approve
|
|
256
|
+
|
|
257
|
+
await client.registerAsEndUser('0x3333333333333333333333333333333333333333', 100n);
|
|
258
|
+
|
|
259
|
+
// Should verify executeBatch is called because allowance is low
|
|
260
|
+
expect(mocks.mockAccount.executeBatch).toHaveBeenCalled();
|
|
261
|
+
// first arg (targets) should contain [gToken, registry]
|
|
262
|
+
const callArgs = mocks.mockAccount.executeBatch.mock.calls[0][0];
|
|
263
|
+
expect(callArgs.dest).toHaveLength(2);
|
|
264
|
+
expect(callArgs.dest[0]).toBe(config.gTokenAddress);
|
|
265
|
+
expect(callArgs.dest[1]).toBe(config.registryAddress);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('registerAsEndUser should execute single if already approved', async () => {
|
|
269
|
+
mocks.mockToken.allowance.mockResolvedValue(parseEther('10000'));
|
|
270
|
+
|
|
271
|
+
await client.registerAsEndUser('0x3333333333333333333333333333333333333333', 100n);
|
|
272
|
+
|
|
273
|
+
expect(mocks.mockAccount.execute).toHaveBeenCalled();
|
|
274
|
+
const callArgs = mocks.mockAccount.execute.mock.calls[0][0];
|
|
275
|
+
expect(callArgs.dest).toBe(config.registryAddress);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('Gasless', () => {
|
|
280
|
+
it('executeGasless should construct and send UserOp', async () => {
|
|
281
|
+
const result = await client.executeGasless({
|
|
282
|
+
target: '0x1111111111111111111111111111111111111111',
|
|
283
|
+
value: 0n,
|
|
284
|
+
data: '0x1234',
|
|
285
|
+
paymaster: '0x2222222222222222222222222222222222222222',
|
|
286
|
+
paymasterType: 'V4'
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(client.client.extend).toHaveBeenCalled();
|
|
290
|
+
expect(result).toBe('0xUserOpHash');
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
import { CommunityClient } from '../src/CommunityClient';
|
|
3
|
+
import { UserClient } from '../src/UserClient';
|
|
4
|
+
import * as Exports from '../src/index';
|
|
5
|
+
|
|
6
|
+
describe('EndUser Package Exports', () => {
|
|
7
|
+
it('should export CommunityClient', () => {
|
|
8
|
+
expect(Exports.CommunityClient).toBeDefined();
|
|
9
|
+
expect(Exports.CommunityClient).toBe(CommunityClient);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should export UserClient', () => {
|
|
13
|
+
expect(Exports.UserClient).toBeDefined();
|
|
14
|
+
expect(Exports.UserClient).toBe(UserClient);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import type { PublicClient, WalletClient } from 'viem';
|
|
3
|
+
|
|
4
|
+
export const createMockPublicClient = (): PublicClient => {
|
|
5
|
+
return {
|
|
6
|
+
readContract: vi.fn(),
|
|
7
|
+
chain: { id: 11155111 },
|
|
8
|
+
waitForTransactionReceipt: vi.fn().mockResolvedValue({ status: 'success' }),
|
|
9
|
+
} as any;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const createMockWalletClient = (): WalletClient => {
|
|
13
|
+
return {
|
|
14
|
+
writeContract: vi.fn(),
|
|
15
|
+
account: { address: '0x1234567890123456789012345678901234567890' },
|
|
16
|
+
chain: { id: 11155111 },
|
|
17
|
+
} as any;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const resetMocks = () => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
};
|