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