genlayer 0.38.8 → 0.38.10

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.
Files changed (205) hide show
  1. package/.eslintignore +2 -0
  2. package/.github/workflows/cli-docs.yml +124 -0
  3. package/.github/workflows/publish.yml +55 -0
  4. package/.github/workflows/smoke.yml +27 -0
  5. package/.github/workflows/validate-code.yml +51 -0
  6. package/.prettierignore +19 -0
  7. package/.prettierrc +12 -0
  8. package/.release-it.json +66 -0
  9. package/CHANGELOG.md +545 -0
  10. package/CLAUDE.md +55 -0
  11. package/CONTRIBUTING.md +117 -0
  12. package/dist/index.js +221 -62
  13. package/docs/api-references/_meta.json +9 -0
  14. package/docs/api-references/accounts/_meta.json +3 -0
  15. package/docs/api-references/accounts/account/create.mdx +19 -0
  16. package/docs/api-references/accounts/account/export.mdx +19 -0
  17. package/docs/api-references/accounts/account/import.mdx +22 -0
  18. package/docs/api-references/accounts/account/list.mdx +15 -0
  19. package/docs/api-references/accounts/account/lock.mdx +16 -0
  20. package/docs/api-references/accounts/account/remove.mdx +20 -0
  21. package/docs/api-references/accounts/account/send.mdx +24 -0
  22. package/docs/api-references/accounts/account/show.mdx +17 -0
  23. package/docs/api-references/accounts/account/unlock.mdx +17 -0
  24. package/docs/api-references/accounts/account/use.mdx +19 -0
  25. package/docs/api-references/accounts/account.mdx +32 -0
  26. package/docs/api-references/configuration/_meta.json +4 -0
  27. package/docs/api-references/configuration/config/get.mdx +21 -0
  28. package/docs/api-references/configuration/config/reset.mdx +21 -0
  29. package/docs/api-references/configuration/config/set.mdx +21 -0
  30. package/docs/api-references/configuration/config.mdx +25 -0
  31. package/docs/api-references/configuration/network/info.mdx +15 -0
  32. package/docs/api-references/configuration/network/list.mdx +15 -0
  33. package/docs/api-references/configuration/network/set.mdx +21 -0
  34. package/docs/api-references/configuration/network.mdx +25 -0
  35. package/docs/api-references/contracts/_meta.json +7 -0
  36. package/docs/api-references/contracts/call.mdx +21 -0
  37. package/docs/api-references/contracts/code.mdx +20 -0
  38. package/docs/api-references/contracts/deploy.mdx +17 -0
  39. package/docs/api-references/contracts/schema.mdx +20 -0
  40. package/docs/api-references/contracts/write.mdx +21 -0
  41. package/docs/api-references/environment/_meta.json +7 -0
  42. package/docs/api-references/environment/init.mdx +20 -0
  43. package/docs/api-references/environment/new.mdx +21 -0
  44. package/docs/api-references/environment/stop.mdx +15 -0
  45. package/docs/api-references/environment/up.mdx +20 -0
  46. package/docs/api-references/environment/update/ollama.mdx +16 -0
  47. package/docs/api-references/environment/update.mdx +23 -0
  48. package/docs/api-references/index.mdx +35 -0
  49. package/docs/api-references/localnet/_meta.json +3 -0
  50. package/docs/api-references/localnet/localnet/validators/count.mdx +15 -0
  51. package/docs/api-references/localnet/localnet/validators/create-random.mdx +16 -0
  52. package/docs/api-references/localnet/localnet/validators/create.mdx +19 -0
  53. package/docs/api-references/localnet/localnet/validators/delete.mdx +16 -0
  54. package/docs/api-references/localnet/localnet/validators/get.mdx +16 -0
  55. package/docs/api-references/localnet/localnet/validators/update.mdx +23 -0
  56. package/docs/api-references/localnet/localnet/validators.mdx +28 -0
  57. package/docs/api-references/localnet/localnet.mdx +23 -0
  58. package/docs/api-references/staking/_meta.json +3 -0
  59. package/docs/api-references/staking/staking/active-validators.mdx +18 -0
  60. package/docs/api-references/staking/staking/banned-validators.mdx +18 -0
  61. package/docs/api-references/staking/staking/delegation-info.mdx +25 -0
  62. package/docs/api-references/staking/staking/delegator-claim.mdx +26 -0
  63. package/docs/api-references/staking/staking/delegator-exit.mdx +26 -0
  64. package/docs/api-references/staking/staking/delegator-join.mdx +26 -0
  65. package/docs/api-references/staking/staking/epoch-info.mdx +19 -0
  66. package/docs/api-references/staking/staking/prime-all.mdx +20 -0
  67. package/docs/api-references/staking/staking/quarantined-validators.mdx +18 -0
  68. package/docs/api-references/staking/staking/set-identity.mdx +33 -0
  69. package/docs/api-references/staking/staking/set-operator.mdx +26 -0
  70. package/docs/api-references/staking/staking/validator-claim.mdx +24 -0
  71. package/docs/api-references/staking/staking/validator-deposit.mdx +25 -0
  72. package/docs/api-references/staking/staking/validator-exit.mdx +25 -0
  73. package/docs/api-references/staking/staking/validator-history.mdx +29 -0
  74. package/docs/api-references/staking/staking/validator-info.mdx +25 -0
  75. package/docs/api-references/staking/staking/validator-join.mdx +22 -0
  76. package/docs/api-references/staking/staking/validator-prime.mdx +25 -0
  77. package/docs/api-references/staking/staking/validators.mdx +19 -0
  78. package/docs/api-references/staking/staking/wizard.mdx +20 -0
  79. package/docs/api-references/staking/staking.mdx +42 -0
  80. package/docs/api-references/transactions/_meta.json +6 -0
  81. package/docs/api-references/transactions/appeal-bond.mdx +20 -0
  82. package/docs/api-references/transactions/appeal.mdx +21 -0
  83. package/docs/api-references/transactions/receipt.mdx +25 -0
  84. package/docs/api-references/transactions/trace.mdx +21 -0
  85. package/docs/delegator-guide.md +203 -0
  86. package/docs/validator-guide.md +329 -0
  87. package/esbuild.config.dev.js +17 -0
  88. package/esbuild.config.js +22 -0
  89. package/esbuild.config.prod.js +17 -0
  90. package/eslint.config.js +60 -0
  91. package/package.json +2 -11
  92. package/renovate.json +22 -0
  93. package/scripts/generate-cli-docs.mjs +68 -5
  94. package/src/commands/account/create.ts +30 -0
  95. package/src/commands/account/export.ts +106 -0
  96. package/src/commands/account/import.ts +135 -0
  97. package/src/commands/account/index.ts +129 -0
  98. package/src/commands/account/list.ts +34 -0
  99. package/src/commands/account/lock.ts +39 -0
  100. package/src/commands/account/remove.ts +30 -0
  101. package/src/commands/account/send.ts +162 -0
  102. package/src/commands/account/show.ts +74 -0
  103. package/src/commands/account/unlock.ts +56 -0
  104. package/src/commands/account/use.ts +21 -0
  105. package/src/commands/config/getSetReset.ts +51 -0
  106. package/src/commands/config/index.ts +30 -0
  107. package/src/commands/contracts/call.ts +39 -0
  108. package/src/commands/contracts/code.ts +33 -0
  109. package/src/commands/contracts/deploy.ts +161 -0
  110. package/src/commands/contracts/index.ts +150 -0
  111. package/src/commands/contracts/schema.ts +31 -0
  112. package/src/commands/contracts/write.ts +49 -0
  113. package/src/commands/general/index.ts +45 -0
  114. package/src/commands/general/init.ts +180 -0
  115. package/src/commands/general/start.ts +128 -0
  116. package/src/commands/general/stop.ts +26 -0
  117. package/src/commands/localnet/index.ts +100 -0
  118. package/src/commands/localnet/validators.ts +269 -0
  119. package/src/commands/network/index.ts +29 -0
  120. package/src/commands/network/setNetwork.ts +77 -0
  121. package/src/commands/scaffold/index.ts +16 -0
  122. package/src/commands/scaffold/new.ts +34 -0
  123. package/src/commands/staking/StakingAction.ts +279 -0
  124. package/src/commands/staking/delegatorClaim.ts +41 -0
  125. package/src/commands/staking/delegatorExit.ts +56 -0
  126. package/src/commands/staking/delegatorJoin.ts +44 -0
  127. package/src/commands/staking/index.ts +357 -0
  128. package/src/commands/staking/setIdentity.ts +78 -0
  129. package/src/commands/staking/setOperator.ts +46 -0
  130. package/src/commands/staking/stakingInfo.ts +584 -0
  131. package/src/commands/staking/validatorClaim.ts +43 -0
  132. package/src/commands/staking/validatorDeposit.ts +48 -0
  133. package/src/commands/staking/validatorExit.ts +63 -0
  134. package/src/commands/staking/validatorHistory.ts +300 -0
  135. package/src/commands/staking/validatorJoin.ts +47 -0
  136. package/src/commands/staking/validatorPrime.ts +73 -0
  137. package/src/commands/staking/wizard.ts +809 -0
  138. package/src/commands/transactions/appeal.ts +83 -0
  139. package/src/commands/transactions/index.ts +60 -0
  140. package/src/commands/transactions/receipt.ts +90 -0
  141. package/src/commands/transactions/trace.ts +42 -0
  142. package/src/commands/update/index.ts +25 -0
  143. package/src/commands/update/ollama.ts +103 -0
  144. package/src/lib/actions/BaseAction.ts +301 -0
  145. package/src/lib/clients/jsonRpcClient.ts +41 -0
  146. package/src/lib/clients/system.ts +73 -0
  147. package/src/lib/config/ConfigFileManager.ts +194 -0
  148. package/src/lib/config/KeychainManager.ts +89 -0
  149. package/src/lib/config/simulator.ts +68 -0
  150. package/src/lib/config/text.ts +2 -0
  151. package/src/lib/errors/missingRequirement.ts +9 -0
  152. package/src/lib/errors/versionRequired.ts +9 -0
  153. package/src/lib/interfaces/ISimulatorService.ts +39 -0
  154. package/src/lib/services/simulator.ts +386 -0
  155. package/src/types/node-fetch.d.ts +1 -0
  156. package/tests/actions/appeal.test.ts +141 -0
  157. package/tests/actions/call.test.ts +94 -0
  158. package/tests/actions/code.test.ts +87 -0
  159. package/tests/actions/create.test.ts +65 -0
  160. package/tests/actions/deploy.test.ts +420 -0
  161. package/tests/actions/getSetReset.test.ts +88 -0
  162. package/tests/actions/init.test.ts +483 -0
  163. package/tests/actions/lock.test.ts +86 -0
  164. package/tests/actions/new.test.ts +80 -0
  165. package/tests/actions/ollama.test.ts +193 -0
  166. package/tests/actions/receipt.test.ts +261 -0
  167. package/tests/actions/schema.test.ts +94 -0
  168. package/tests/actions/setNetwork.test.ts +161 -0
  169. package/tests/actions/staking.test.ts +280 -0
  170. package/tests/actions/start.test.ts +257 -0
  171. package/tests/actions/stop.test.ts +77 -0
  172. package/tests/actions/unlock.test.ts +139 -0
  173. package/tests/actions/validators.test.ts +750 -0
  174. package/tests/actions/write.test.ts +102 -0
  175. package/tests/commands/account.test.ts +146 -0
  176. package/tests/commands/appeal.test.ts +97 -0
  177. package/tests/commands/call.test.ts +78 -0
  178. package/tests/commands/code.test.ts +69 -0
  179. package/tests/commands/config.test.ts +54 -0
  180. package/tests/commands/deploy.test.ts +83 -0
  181. package/tests/commands/init.test.ts +101 -0
  182. package/tests/commands/localnet.test.ts +131 -0
  183. package/tests/commands/network.test.ts +60 -0
  184. package/tests/commands/new.test.ts +68 -0
  185. package/tests/commands/parseArg.test.ts +156 -0
  186. package/tests/commands/receipt.test.ts +142 -0
  187. package/tests/commands/schema.test.ts +67 -0
  188. package/tests/commands/staking.test.ts +329 -0
  189. package/tests/commands/stop.test.ts +27 -0
  190. package/tests/commands/up.test.ts +105 -0
  191. package/tests/commands/update.test.ts +45 -0
  192. package/tests/commands/write.test.ts +76 -0
  193. package/tests/index.test.ts +56 -0
  194. package/tests/libs/baseAction.test.ts +535 -0
  195. package/tests/libs/configFileManager.test.ts +118 -0
  196. package/tests/libs/jsonRpcClient.test.ts +59 -0
  197. package/tests/libs/keychainManager.test.ts +156 -0
  198. package/tests/libs/platformCommands.test.ts +78 -0
  199. package/tests/libs/system.test.ts +148 -0
  200. package/tests/services/simulator.test.ts +789 -0
  201. package/tests/smoke.test.ts +134 -0
  202. package/tests/utils.ts +13 -0
  203. package/tsconfig.json +120 -0
  204. package/vitest.config.ts +13 -0
  205. package/vitest.smoke.config.ts +17 -0
@@ -0,0 +1,301 @@
1
+ import {ConfigFileManager} from "../../lib/config/ConfigFileManager";
2
+ import {KeychainManager} from "../../lib/config/KeychainManager";
3
+ import ora, {Ora} from "ora";
4
+ import chalk from "chalk";
5
+ import inquirer from "inquirer";
6
+ import { inspect } from "util";
7
+ import {createClient, createAccount} from "genlayer-js";
8
+ import {localnet, studionet, testnetAsimov, testnetBradbury} from "genlayer-js/chains";
9
+ import type {GenLayerClient, GenLayerChain, Hash, Address, Account} from "genlayer-js/types";
10
+
11
+ // Built-in networks - always resolve fresh from genlayer-js
12
+ export const BUILT_IN_NETWORKS: Record<string, GenLayerChain> = {
13
+ "localnet": localnet,
14
+ "studionet": studionet,
15
+ "testnet-asimov": testnetAsimov,
16
+ "testnet-bradbury": testnetBradbury,
17
+ };
18
+
19
+ /**
20
+ * Resolves a stored network config to a fresh chain object.
21
+ * Handles both new format (alias string) and old format (JSON object) for backwards compat.
22
+ */
23
+ export function resolveNetwork(stored: string | undefined): GenLayerChain {
24
+ if (!stored) return localnet;
25
+
26
+ // Try as alias first (new format)
27
+ if (BUILT_IN_NETWORKS[stored]) {
28
+ return BUILT_IN_NETWORKS[stored];
29
+ }
30
+
31
+ // Backwards compat: try parsing as JSON (old format)
32
+ try {
33
+ const parsed = JSON.parse(stored);
34
+ // If it has a known name, use fresh version instead
35
+ const alias = Object.entries(BUILT_IN_NETWORKS)
36
+ .find(([_, chain]) => chain.name === parsed.name)?.[0];
37
+ if (alias) {
38
+ return BUILT_IN_NETWORKS[alias];
39
+ }
40
+ // Custom network - use as-is
41
+ return parsed;
42
+ } catch {
43
+ throw new Error(`Unknown network: ${stored}`);
44
+ }
45
+ }
46
+ import { ethers } from "ethers";
47
+ import { writeFileSync, existsSync, readFileSync } from "fs";
48
+
49
+ export class BaseAction extends ConfigFileManager {
50
+ private static readonly DEFAULT_ACCOUNT_NAME = "default";
51
+ private static readonly MAX_PASSWORD_ATTEMPTS = 3;
52
+ protected static readonly MIN_PASSWORD_LENGTH = 8;
53
+
54
+ private spinner: Ora;
55
+ private _genlayerClient: GenLayerClient<GenLayerChain> | null = null;
56
+ protected keychainManager: KeychainManager;
57
+ protected accountOverride: string | null = null;
58
+
59
+ constructor() {
60
+ super();
61
+ this.spinner = ora({text: "", spinner: "dots"});
62
+ this.keychainManager = new KeychainManager();
63
+ }
64
+
65
+ private async decryptKeystore(keystoreJson: string, attempt: number = 1): Promise<string> {
66
+ try {
67
+ const message = attempt === 1
68
+ ? "Enter password to decrypt keystore:"
69
+ : `Invalid password. Attempt ${attempt}/${BaseAction.MAX_PASSWORD_ATTEMPTS} - Enter password to decrypt keystore:`;
70
+ const password = await this.promptPassword(message);
71
+ const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
72
+
73
+ return wallet.privateKey;
74
+ } catch (error) {
75
+ if (attempt >= BaseAction.MAX_PASSWORD_ATTEMPTS) {
76
+ this.failSpinner(`Maximum password attempts exceeded (${BaseAction.MAX_PASSWORD_ATTEMPTS}/${BaseAction.MAX_PASSWORD_ATTEMPTS}).`);
77
+ }
78
+ return await this.decryptKeystore(keystoreJson, attempt + 1);
79
+ }
80
+ }
81
+
82
+ protected isValidKeystoreFormat(data: any): boolean {
83
+ // Standard web3 keystore format has 'crypto' (or 'Crypto') and 'address' fields
84
+ return Boolean(
85
+ data &&
86
+ (data.crypto || data.Crypto) &&
87
+ typeof data.address === "string"
88
+ );
89
+ }
90
+
91
+ private formatOutput(data: any): string {
92
+ if (typeof data === "string") {
93
+ return data;
94
+ }
95
+ return inspect(data, { depth: null, colors: false });
96
+ }
97
+
98
+ protected async getClient(rpcUrl?: string, readOnly: boolean = false): Promise<GenLayerClient<GenLayerChain>> {
99
+ if (!this._genlayerClient) {
100
+ const network = resolveNetwork(this.getConfig().network);
101
+ const account = await this.getAccount(readOnly);
102
+ this._genlayerClient = createClient({
103
+ chain: network,
104
+ endpoint: rpcUrl,
105
+ account: account,
106
+ });
107
+ }
108
+ return this._genlayerClient;
109
+ }
110
+
111
+ protected resolveAccountName(): string {
112
+ // Priority: explicit override > config active account > default
113
+ if (this.accountOverride) {
114
+ return this.accountOverride;
115
+ }
116
+ const activeAccount = this.getActiveAccount();
117
+ if (activeAccount) {
118
+ return activeAccount;
119
+ }
120
+ return BaseAction.DEFAULT_ACCOUNT_NAME;
121
+ }
122
+
123
+ private async getAccount(readOnly: boolean = false): Promise<Account | Address> {
124
+ const accountName = this.resolveAccountName();
125
+ const keystorePath = this.getKeystorePath(accountName);
126
+ let decryptedPrivateKey;
127
+ let keystoreJson: string;
128
+ let keystoreData: any;
129
+
130
+ if (!existsSync(keystorePath)) {
131
+ await this.confirmPrompt(`Account '${accountName}' not found. Would you like to create it?`);
132
+ decryptedPrivateKey = await this.createKeypairByName(accountName, false);
133
+ }
134
+
135
+ keystoreJson = readFileSync(keystorePath, "utf-8");
136
+ keystoreData = JSON.parse(keystoreJson);
137
+
138
+ if (!this.isValidKeystoreFormat(keystoreData)) {
139
+ this.failSpinner("Invalid keystore format. Expected encrypted keystore file.", undefined, false);
140
+ await this.confirmPrompt(`Would you like to recreate account '${accountName}'?`);
141
+ decryptedPrivateKey = await this.createKeypairByName(accountName, true);
142
+ keystoreJson = readFileSync(keystorePath, "utf-8");
143
+ keystoreData = JSON.parse(keystoreJson);
144
+ }
145
+
146
+ if (readOnly) {
147
+ return this.getAddress(keystoreData);
148
+ }
149
+
150
+ if (!decryptedPrivateKey) {
151
+ const cachedKey = await this.keychainManager.getPrivateKey(accountName);
152
+ if (cachedKey) {
153
+ // Verify cached key matches keystore address
154
+ const tempAccount = createAccount(cachedKey as Hash);
155
+ const cachedAddress = tempAccount.address.toLowerCase();
156
+ const keystoreAddress = `0x${keystoreData.address.toLowerCase().replace(/^0x/, '')}`;
157
+ if (cachedAddress === keystoreAddress) {
158
+ decryptedPrivateKey = cachedKey;
159
+ } else {
160
+ // Cached key doesn't match keystore - invalidate it
161
+ await this.keychainManager.removePrivateKey(accountName);
162
+ decryptedPrivateKey = await this.decryptKeystore(keystoreJson);
163
+ }
164
+ } else {
165
+ decryptedPrivateKey = await this.decryptKeystore(keystoreJson);
166
+ }
167
+ }
168
+ return createAccount(decryptedPrivateKey as Hash);
169
+ }
170
+
171
+ private getAddress(keystoreData: any): Address {
172
+ const addr = keystoreData.address;
173
+ return (addr.startsWith('0x') ? addr : `0x${addr}`) as Address;
174
+ }
175
+
176
+ protected async createKeypairByName(accountName: string, overwrite: boolean, passwordInput?: string): Promise<string> {
177
+ const keystorePath = this.getKeystorePath(accountName);
178
+ this.stopSpinner();
179
+
180
+ if (existsSync(keystorePath) && !overwrite) {
181
+ this.failSpinner(`Account '${accountName}' already exists. Use '--overwrite' to replace it.`);
182
+ }
183
+
184
+ const wallet = ethers.Wallet.createRandom();
185
+
186
+ let password: string;
187
+ if (passwordInput) {
188
+ password = passwordInput;
189
+ } else {
190
+ password = await this.promptPassword("Enter a password to encrypt your keystore (minimum 8 characters):");
191
+ const confirmPassword = await this.promptPassword("Confirm password:");
192
+ if (password !== confirmPassword) {
193
+ this.failSpinner("Passwords do not match");
194
+ }
195
+ }
196
+
197
+ if (password.length < BaseAction.MIN_PASSWORD_LENGTH) {
198
+ this.failSpinner(`Password must be at least ${BaseAction.MIN_PASSWORD_LENGTH} characters long`);
199
+ }
200
+
201
+ // Write standard web3 keystore format directly
202
+ const encryptedJson = await wallet.encrypt(password);
203
+ writeFileSync(keystorePath, encryptedJson);
204
+
205
+ // Set as active account if no active account exists
206
+ if (!this.getActiveAccount()) {
207
+ this.setActiveAccount(accountName);
208
+ }
209
+
210
+ await this.keychainManager.removePrivateKey(accountName);
211
+
212
+ return wallet.privateKey;
213
+ }
214
+
215
+ protected async promptPassword(message: string): Promise<string> {
216
+ const answer = await inquirer.prompt([
217
+ {
218
+ type: "password",
219
+ name: "password",
220
+ message: chalk.yellow(message),
221
+ mask: "*",
222
+ validate: (input: string) => {
223
+ if (!input) {
224
+ return "Password cannot be empty";
225
+ }
226
+ return true;
227
+ },
228
+ },
229
+ ]);
230
+ return answer.password;
231
+ }
232
+
233
+ protected async confirmPrompt(message: string): Promise<void> {
234
+ const answer = await inquirer.prompt([
235
+ {
236
+ type: "confirm",
237
+ name: "confirmAction",
238
+ message: chalk.yellow(message),
239
+ default: true,
240
+ },
241
+ ]);
242
+
243
+ if (!answer.confirmAction) {
244
+ this.logError("Operation aborted!");
245
+ process.exit(0);
246
+ }
247
+ }
248
+
249
+ protected log(message: string, data?: any): void {
250
+ console.log(chalk.white(`\n${message}`));
251
+ if (data !== undefined) console.log(this.formatOutput(data));
252
+ }
253
+
254
+ protected logSuccess(message: string, data?: any): void {
255
+ console.log(chalk.green(`\n✔ ${message}`));
256
+ if (data !== undefined) console.log(chalk.green(this.formatOutput(data)));
257
+ }
258
+
259
+ protected logInfo(message: string, data?: any): void {
260
+ console.log(chalk.blue(`\nℹ ${message}`));
261
+ if (data !== undefined) console.log(chalk.blue(this.formatOutput(data)));
262
+ }
263
+
264
+ protected logWarning(message: string, data?: any): void {
265
+ console.log(chalk.yellow(`\n⚠ ${message}`));
266
+ if (data !== undefined) console.log(chalk.yellow(this.formatOutput(data)));
267
+ }
268
+
269
+ protected logError(message: string, error?: any): void {
270
+ console.error(chalk.red(`\n✖ ${message}`));
271
+ if (error !== undefined) console.error(chalk.red(this.formatOutput(error)));
272
+ }
273
+
274
+ protected startSpinner(message: string) {
275
+ this.spinner.text = chalk.blue(`${message}`);
276
+ this.spinner.start();
277
+ }
278
+
279
+ protected succeedSpinner(message: string, data?: any): void {
280
+ if (data !== undefined) this.log("Result:", data);
281
+ console.log('');
282
+ this.spinner.succeed(chalk.green(message));
283
+ }
284
+
285
+ protected failSpinner(message: string, error?: any, shouldExit = true): void {
286
+ if (error) this.log("Error:", error);
287
+ console.log("");
288
+ this.spinner.fail(chalk.red(message));
289
+ if (shouldExit) {
290
+ process.exit(1);
291
+ }
292
+ }
293
+
294
+ protected stopSpinner(): void {
295
+ this.spinner.stop();
296
+ }
297
+
298
+ protected setSpinnerText(message: string): void {
299
+ this.spinner.text = chalk.blue(message);
300
+ }
301
+ }
@@ -0,0 +1,41 @@
1
+ import fetch from "node-fetch";
2
+ import {v4 as uuidv4} from "uuid";
3
+
4
+ import {DEFAULT_JSON_RPC_URL} from "../config/simulator";
5
+
6
+ export interface JsonRPCParams {
7
+ method: string;
8
+ params: any[];
9
+ }
10
+
11
+ export class JsonRpcClient {
12
+ serverUrl: string;
13
+
14
+ constructor(serverUrl: string) {
15
+ this.serverUrl = serverUrl;
16
+ }
17
+
18
+ async request({method, params}: JsonRPCParams): Promise<any | null> {
19
+ const response = await fetch(this.serverUrl, {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json",
23
+ },
24
+ body: JSON.stringify({
25
+ jsonrpc: "2.0",
26
+ id: uuidv4(),
27
+ method,
28
+ params,
29
+ }),
30
+ });
31
+
32
+ if (response.ok) {
33
+ return response.json();
34
+ }
35
+ const result = await response.json();
36
+
37
+ throw new Error(result?.error?.message || response.statusText);
38
+
39
+ }
40
+ }
41
+ export const rpcClient = new JsonRpcClient(DEFAULT_JSON_RPC_URL);
@@ -0,0 +1,73 @@
1
+ import util from "node:util";
2
+ import {ChildProcess, exec} from "child_process";
3
+ import open from "open";
4
+
5
+ import {RunningPlatform, AVAILABLE_PLATFORMS} from "../config/simulator";
6
+ import {MissingRequirementError} from "../errors/missingRequirement";
7
+
8
+ export async function checkCommand(command: string, toolName: string): Promise<void> {
9
+ try {
10
+ await util.promisify(exec)(command);
11
+ }catch (error:any) {
12
+ if (error.stderr) {
13
+ throw new MissingRequirementError(toolName);
14
+ }
15
+ }
16
+ }
17
+
18
+ type ExecuteCommandResult = {
19
+ stdout: string;
20
+ stderr: string;
21
+ };
22
+
23
+ type ExecuteCommandByPlatformInput = {
24
+ [key in RunningPlatform]: string;
25
+ };
26
+
27
+ export async function executeCommand(
28
+ cmdsByPlatform: ExecuteCommandByPlatformInput,
29
+ toolName?: string,
30
+ ): Promise<ExecuteCommandResult> {
31
+ const runningPlatform = getPlatform();
32
+ const command = cmdsByPlatform[runningPlatform];
33
+ try {
34
+ return await util.promisify(exec)(command);
35
+ } catch (error: any) {
36
+ throw new Error(`Error executing ${toolName || command}: ${error.message}.`);
37
+ }
38
+ }
39
+
40
+ function getPlatform(): RunningPlatform {
41
+ const currentPlatform = process.platform as RunningPlatform;
42
+ if (!AVAILABLE_PLATFORMS.includes(currentPlatform)) {
43
+ throw new Error(`Unsupported platform: ${currentPlatform}.`);
44
+ }
45
+ return currentPlatform;
46
+ }
47
+
48
+ export function openUrl(url: string): Promise<ChildProcess> {
49
+ return open(url);
50
+ }
51
+
52
+ export async function getVersion(toolName: string): Promise<string> {
53
+ try {
54
+ const toolResponse = await util.promisify(exec)(`${toolName} --version`);
55
+
56
+ if (toolResponse.stderr) {
57
+ throw new Error(toolResponse.stderr);
58
+ }
59
+
60
+ try {
61
+ const versionMatch = toolResponse.stdout.match(/(\d+\.\d+\.\d+)/);
62
+ if (versionMatch) {
63
+ return versionMatch[1];
64
+ }
65
+ } catch (err) {
66
+ throw new Error(`Could not parse ${toolName} version.`);
67
+ }
68
+ } catch (error) {
69
+ throw new Error(`Error getting ${toolName} version.`);
70
+ }
71
+
72
+ return "";
73
+ }
@@ -0,0 +1,194 @@
1
+ import path from "path";
2
+ import os from "os";
3
+ import fs from "fs";
4
+
5
+ export interface AccountInfo {
6
+ name: string;
7
+ address: string;
8
+ path: string;
9
+ }
10
+
11
+ export class ConfigFileManager {
12
+ private folderPath: string;
13
+ private configFilePath: string;
14
+ private keystoresPath: string;
15
+
16
+ constructor(baseFolder: string = ".genlayer/", configFileName: string = "genlayer-config.json") {
17
+ this.folderPath = path.resolve(os.homedir(), baseFolder);
18
+ this.configFilePath = path.resolve(this.folderPath, configFileName);
19
+ this.keystoresPath = path.resolve(this.folderPath, "keystores");
20
+ this.ensureFolderExists();
21
+ this.ensureKeystoresDirExists();
22
+ this.ensureConfigFileExists();
23
+ this.migrateOldConfig();
24
+ this.migrateKeystoreFormats();
25
+ }
26
+
27
+ private ensureFolderExists(): void {
28
+ if (!fs.existsSync(this.folderPath)) {
29
+ fs.mkdirSync(this.folderPath, { recursive: true });
30
+ }
31
+ }
32
+
33
+ private ensureKeystoresDirExists(): void {
34
+ if (!fs.existsSync(this.keystoresPath)) {
35
+ fs.mkdirSync(this.keystoresPath, { recursive: true });
36
+ }
37
+ }
38
+
39
+ private ensureConfigFileExists(): void {
40
+ if (!fs.existsSync(this.configFilePath)) {
41
+ fs.writeFileSync(this.configFilePath, JSON.stringify({}, null, 2));
42
+ }
43
+ }
44
+
45
+ private migrateOldConfig(): void {
46
+ const config = this.getConfig();
47
+ if (config.keyPairPath && !config.activeAccount) {
48
+ const oldPath = config.keyPairPath;
49
+ if (fs.existsSync(oldPath)) {
50
+ const newPath = this.getKeystorePath("default");
51
+ // Read old keystore and convert format if needed
52
+ const content = fs.readFileSync(oldPath, "utf-8");
53
+ const web3Content = this.convertToWeb3Format(content);
54
+ fs.writeFileSync(newPath, web3Content);
55
+ delete config.keyPairPath;
56
+ config.activeAccount = "default";
57
+ fs.writeFileSync(this.configFilePath, JSON.stringify(config, null, 2));
58
+ }
59
+ }
60
+ }
61
+
62
+ private migrateKeystoreFormats(): void {
63
+ if (!fs.existsSync(this.keystoresPath)) {
64
+ return;
65
+ }
66
+ const files = fs.readdirSync(this.keystoresPath);
67
+ if (!Array.isArray(files)) {
68
+ return;
69
+ }
70
+ for (const file of files) {
71
+ if (!file.endsWith(".json")) continue;
72
+ const filePath = path.resolve(this.keystoresPath, file);
73
+ try {
74
+ const content = fs.readFileSync(filePath, "utf-8");
75
+ const parsed = JSON.parse(content);
76
+ // Check if it's old GenLayer format (has 'encrypted' string field)
77
+ if (parsed.encrypted && typeof parsed.encrypted === "string") {
78
+ const web3Content = this.convertToWeb3Format(content);
79
+ fs.writeFileSync(filePath, web3Content);
80
+ }
81
+ // If it has 'crypto' field, it's already web3 format - skip
82
+ } catch {
83
+ // Skip files that can't be parsed
84
+ }
85
+ }
86
+ }
87
+
88
+ private convertToWeb3Format(content: string): string {
89
+ try {
90
+ const parsed = JSON.parse(content);
91
+ // If it's GenLayer wrapper format (has 'encrypted' string field)
92
+ if (parsed.encrypted && typeof parsed.encrypted === "string") {
93
+ // The 'encrypted' field contains the actual web3 keystore JSON
94
+ return parsed.encrypted;
95
+ }
96
+ // Already web3 format or unknown - return as-is
97
+ return content;
98
+ } catch {
99
+ return content;
100
+ }
101
+ }
102
+
103
+ getFolderPath(): string {
104
+ return this.folderPath;
105
+ }
106
+
107
+ getFilePath(fileName: string): string {
108
+ return path.resolve(this.folderPath, fileName);
109
+ }
110
+
111
+ getConfig(): Record<string, any> {
112
+ const configContent = fs.readFileSync(this.configFilePath, "utf-8");
113
+ return JSON.parse(configContent);
114
+ }
115
+
116
+ getConfigByKey(key: string): any {
117
+ const config = this.getConfig();
118
+ return config[key] !== undefined ? config[key] : null;
119
+ }
120
+
121
+ writeConfig(key: string, value: any): void {
122
+ const config = this.getConfig();
123
+ config[key] = value;
124
+ fs.writeFileSync(this.configFilePath, JSON.stringify(config, null, 2));
125
+ }
126
+
127
+ removeConfig(key: string): void {
128
+ const config = this.getConfig();
129
+ delete config[key];
130
+ fs.writeFileSync(this.configFilePath, JSON.stringify(config, null, 2));
131
+ }
132
+
133
+ getKeystoresPath(): string {
134
+ return this.keystoresPath;
135
+ }
136
+
137
+ getKeystorePath(name: string): string {
138
+ return path.resolve(this.keystoresPath, `${name}.json`);
139
+ }
140
+
141
+ accountExists(name: string): boolean {
142
+ return fs.existsSync(this.getKeystorePath(name));
143
+ }
144
+
145
+ getActiveAccount(): string | null {
146
+ return this.getConfigByKey("activeAccount");
147
+ }
148
+
149
+ setActiveAccount(name: string): void {
150
+ if (!this.accountExists(name)) {
151
+ throw new Error(`Account '${name}' does not exist`);
152
+ }
153
+ this.writeConfig("activeAccount", name);
154
+ }
155
+
156
+ listAccounts(): AccountInfo[] {
157
+ if (!fs.existsSync(this.keystoresPath)) {
158
+ return [];
159
+ }
160
+ const files = fs.readdirSync(this.keystoresPath);
161
+ const accounts: AccountInfo[] = [];
162
+
163
+ for (const file of files) {
164
+ if (!file.endsWith(".json")) continue;
165
+ const name = file.replace(".json", "");
166
+ const filePath = this.getKeystorePath(name);
167
+ try {
168
+ const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
169
+ const addr = content.address || "unknown";
170
+ accounts.push({
171
+ name,
172
+ address: addr.startsWith("0x") ? addr : `0x${addr}`,
173
+ path: filePath,
174
+ });
175
+ } catch {
176
+ // Skip invalid files
177
+ }
178
+ }
179
+ return accounts;
180
+ }
181
+
182
+ removeAccount(name: string): void {
183
+ const keystorePath = this.getKeystorePath(name);
184
+ if (!fs.existsSync(keystorePath)) {
185
+ throw new Error(`Account '${name}' does not exist`);
186
+ }
187
+ fs.unlinkSync(keystorePath);
188
+
189
+ // If this was the active account, clear it
190
+ if (this.getActiveAccount() === name) {
191
+ this.removeConfig("activeAccount");
192
+ }
193
+ }
194
+ }
@@ -0,0 +1,89 @@
1
+ type Keytar = typeof import('keytar').default;
2
+
3
+ let keytarModule: Keytar | null = null;
4
+ let keytarLoadAttempted = false;
5
+
6
+ async function getKeytar(): Promise<Keytar | null> {
7
+ if (keytarLoadAttempted) return keytarModule;
8
+ keytarLoadAttempted = true;
9
+ try {
10
+ const mod = await import('keytar');
11
+ keytarModule = mod.default ?? mod;
12
+ return keytarModule;
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ export class KeychainManager {
19
+ private static readonly SERVICE = 'genlayer-cli';
20
+
21
+ constructor() {}
22
+
23
+ private getKeychainAccount(accountName: string): string {
24
+ return `account:${accountName}`;
25
+ }
26
+
27
+ async isKeychainAvailable(): Promise<boolean> {
28
+ try {
29
+ const keytar = await getKeytar();
30
+ if (!keytar) return false;
31
+ await keytar.findCredentials('test-service');
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ async storePrivateKey(accountName: string, privateKey: string): Promise<void> {
39
+ const keytar = await getKeytar();
40
+ if (!keytar) throw new Error('Keychain not available. Install libsecret-1-dev on Linux.');
41
+ try {
42
+ return await keytar.setPassword(KeychainManager.SERVICE, this.getKeychainAccount(accountName), privateKey);
43
+ } catch (error: any) {
44
+ if (error?.message?.includes('org.freedesktop.secrets')) {
45
+ throw new Error('Keychain service not running. Install and start gnome-keyring or another secrets service.');
46
+ }
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ async getPrivateKey(accountName: string): Promise<string | null> {
52
+ const keytar = await getKeytar();
53
+ if (!keytar) return null;
54
+ try {
55
+ return await keytar.getPassword(KeychainManager.SERVICE, this.getKeychainAccount(accountName));
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ async removePrivateKey(accountName: string): Promise<boolean> {
62
+ const keytar = await getKeytar();
63
+ if (!keytar) return false;
64
+ try {
65
+ return await keytar.deletePassword(KeychainManager.SERVICE, this.getKeychainAccount(accountName));
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ async listUnlockedAccounts(): Promise<string[]> {
72
+ const keytar = await getKeytar();
73
+ if (!keytar) return [];
74
+ try {
75
+ const credentials = await keytar.findCredentials(KeychainManager.SERVICE);
76
+ return credentials
77
+ .map(c => c.account)
78
+ .filter(a => a.startsWith('account:'))
79
+ .map(a => a.replace('account:', ''));
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
85
+ async isAccountUnlocked(accountName: string): Promise<boolean> {
86
+ const key = await this.getPrivateKey(accountName);
87
+ return key !== null;
88
+ }
89
+ }