genlayer 0.31.0 → 0.32.1

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 (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/CLAUDE.md +55 -0
  3. package/README.md +121 -8
  4. package/dist/index.js +7161 -3706
  5. package/docs/delegator-guide.md +203 -0
  6. package/docs/validator-guide.md +291 -0
  7. package/package.json +2 -2
  8. package/src/commands/account/create.ts +29 -0
  9. package/src/commands/account/export.ts +106 -0
  10. package/src/commands/account/import.ts +135 -0
  11. package/src/commands/account/index.ts +126 -0
  12. package/src/commands/account/list.ts +34 -0
  13. package/src/commands/account/lock.ts +39 -0
  14. package/src/commands/account/remove.ts +30 -0
  15. package/src/commands/account/send.ts +156 -0
  16. package/src/commands/account/show.ts +74 -0
  17. package/src/commands/account/unlock.ts +51 -0
  18. package/src/commands/account/use.ts +21 -0
  19. package/src/commands/network/index.ts +18 -3
  20. package/src/commands/network/setNetwork.ts +43 -26
  21. package/src/commands/staking/StakingAction.ts +157 -0
  22. package/src/commands/staking/delegatorClaim.ts +41 -0
  23. package/src/commands/staking/delegatorExit.ts +50 -0
  24. package/src/commands/staking/delegatorJoin.ts +44 -0
  25. package/src/commands/staking/index.ts +251 -0
  26. package/src/commands/staking/setIdentity.ts +66 -0
  27. package/src/commands/staking/setOperator.ts +40 -0
  28. package/src/commands/staking/stakingInfo.ts +300 -0
  29. package/src/commands/staking/validatorClaim.ts +38 -0
  30. package/src/commands/staking/validatorDeposit.ts +35 -0
  31. package/src/commands/staking/validatorExit.ts +44 -0
  32. package/src/commands/staking/validatorJoin.ts +47 -0
  33. package/src/commands/staking/validatorPrime.ts +35 -0
  34. package/src/commands/staking/wizard.ts +802 -0
  35. package/src/index.ts +4 -2
  36. package/src/lib/actions/BaseAction.ts +114 -55
  37. package/src/lib/config/ConfigFileManager.ts +143 -0
  38. package/src/lib/config/KeychainManager.ts +23 -7
  39. package/tests/actions/create.test.ts +41 -21
  40. package/tests/actions/deploy.test.ts +7 -0
  41. package/tests/actions/lock.test.ts +33 -13
  42. package/tests/actions/setNetwork.test.ts +18 -57
  43. package/tests/actions/staking.test.ts +323 -0
  44. package/tests/actions/unlock.test.ts +51 -33
  45. package/tests/commands/account.test.ts +146 -0
  46. package/tests/commands/network.test.ts +10 -10
  47. package/tests/commands/staking.test.ts +333 -0
  48. package/tests/index.test.ts +6 -2
  49. package/tests/libs/baseAction.test.ts +71 -42
  50. package/tests/libs/configFileManager.test.ts +8 -1
  51. package/tests/libs/keychainManager.test.ts +56 -16
  52. package/src/commands/keygen/create.ts +0 -23
  53. package/src/commands/keygen/index.ts +0 -39
  54. package/src/commands/keygen/lock.ts +0 -31
  55. package/src/commands/keygen/unlock.ts +0 -41
  56. package/src/lib/interfaces/KeystoreData.ts +0 -5
  57. package/tests/commands/keygen.test.ts +0 -123
@@ -0,0 +1,333 @@
1
+ import {Command} from "commander";
2
+ import {vi, describe, beforeEach, afterEach, test, expect} from "vitest";
3
+ import {initializeStakingCommands} from "../../src/commands/staking";
4
+ import {ValidatorJoinAction} from "../../src/commands/staking/validatorJoin";
5
+ import {ValidatorDepositAction} from "../../src/commands/staking/validatorDeposit";
6
+ import {ValidatorExitAction} from "../../src/commands/staking/validatorExit";
7
+ import {ValidatorClaimAction} from "../../src/commands/staking/validatorClaim";
8
+ import {ValidatorPrimeAction} from "../../src/commands/staking/validatorPrime";
9
+ import {SetOperatorAction} from "../../src/commands/staking/setOperator";
10
+ import {SetIdentityAction} from "../../src/commands/staking/setIdentity";
11
+ import {DelegatorJoinAction} from "../../src/commands/staking/delegatorJoin";
12
+ import {DelegatorExitAction} from "../../src/commands/staking/delegatorExit";
13
+ import {DelegatorClaimAction} from "../../src/commands/staking/delegatorClaim";
14
+ import {StakingInfoAction} from "../../src/commands/staking/stakingInfo";
15
+
16
+ vi.mock("../../src/commands/staking/validatorJoin");
17
+ vi.mock("../../src/commands/staking/validatorDeposit");
18
+ vi.mock("../../src/commands/staking/validatorExit");
19
+ vi.mock("../../src/commands/staking/validatorClaim");
20
+ vi.mock("../../src/commands/staking/validatorPrime");
21
+ vi.mock("../../src/commands/staking/setOperator");
22
+ vi.mock("../../src/commands/staking/setIdentity");
23
+ vi.mock("../../src/commands/staking/delegatorJoin");
24
+ vi.mock("../../src/commands/staking/delegatorExit");
25
+ vi.mock("../../src/commands/staking/delegatorClaim");
26
+ vi.mock("../../src/commands/staking/stakingInfo");
27
+
28
+ describe("staking commands", () => {
29
+ let program: Command;
30
+
31
+ beforeEach(() => {
32
+ program = new Command();
33
+ initializeStakingCommands(program);
34
+ vi.clearAllMocks();
35
+ });
36
+
37
+ afterEach(() => {
38
+ vi.restoreAllMocks();
39
+ });
40
+
41
+ describe("validator-join", () => {
42
+ test("calls ValidatorJoinAction.execute with amount", async () => {
43
+ program.parse(["node", "test", "staking", "validator-join", "--amount", "42000gen"]);
44
+
45
+ expect(ValidatorJoinAction).toHaveBeenCalledTimes(1);
46
+ expect(ValidatorJoinAction.prototype.execute).toHaveBeenCalledWith({
47
+ amount: "42000gen",
48
+ });
49
+ });
50
+
51
+ test("calls ValidatorJoinAction.execute with operator", async () => {
52
+ program.parse([
53
+ "node",
54
+ "test",
55
+ "staking",
56
+ "validator-join",
57
+ "--amount",
58
+ "42000gen",
59
+ "--operator",
60
+ "0xOperator",
61
+ ]);
62
+
63
+ expect(ValidatorJoinAction.prototype.execute).toHaveBeenCalledWith({
64
+ amount: "42000gen",
65
+ operator: "0xOperator",
66
+ });
67
+ });
68
+
69
+ test("accepts staking-address option", async () => {
70
+ program.parse([
71
+ "node",
72
+ "test",
73
+ "staking",
74
+ "validator-join",
75
+ "--amount",
76
+ "42000",
77
+ "--staking-address",
78
+ "0xStaking",
79
+ ]);
80
+
81
+ expect(ValidatorJoinAction.prototype.execute).toHaveBeenCalledWith(
82
+ expect.objectContaining({stakingAddress: "0xStaking"}),
83
+ );
84
+ });
85
+ });
86
+
87
+ describe("validator-deposit", () => {
88
+ test("calls ValidatorDepositAction.execute", async () => {
89
+ program.parse(["node", "test", "staking", "validator-deposit", "--amount", "1000gen"]);
90
+
91
+ expect(ValidatorDepositAction).toHaveBeenCalledTimes(1);
92
+ expect(ValidatorDepositAction.prototype.execute).toHaveBeenCalledWith({
93
+ amount: "1000gen",
94
+ });
95
+ });
96
+ });
97
+
98
+ describe("validator-exit", () => {
99
+ test("calls ValidatorExitAction.execute", async () => {
100
+ program.parse(["node", "test", "staking", "validator-exit", "--shares", "100"]);
101
+
102
+ expect(ValidatorExitAction).toHaveBeenCalledTimes(1);
103
+ expect(ValidatorExitAction.prototype.execute).toHaveBeenCalledWith({
104
+ shares: "100",
105
+ });
106
+ });
107
+ });
108
+
109
+ describe("validator-claim", () => {
110
+ test("calls ValidatorClaimAction.execute", async () => {
111
+ program.parse(["node", "test", "staking", "validator-claim", "--validator", "0xValidator"]);
112
+
113
+ expect(ValidatorClaimAction).toHaveBeenCalledTimes(1);
114
+ expect(ValidatorClaimAction.prototype.execute).toHaveBeenCalledWith({
115
+ validator: "0xValidator",
116
+ });
117
+ });
118
+
119
+ test("works without validator option", async () => {
120
+ program.parse(["node", "test", "staking", "validator-claim"]);
121
+
122
+ expect(ValidatorClaimAction.prototype.execute).toHaveBeenCalledWith({});
123
+ });
124
+ });
125
+
126
+ describe("delegator-join", () => {
127
+ test("calls DelegatorJoinAction.execute", async () => {
128
+ program.parse([
129
+ "node",
130
+ "test",
131
+ "staking",
132
+ "delegator-join",
133
+ "--validator",
134
+ "0xValidator",
135
+ "--amount",
136
+ "42gen",
137
+ ]);
138
+
139
+ expect(DelegatorJoinAction).toHaveBeenCalledTimes(1);
140
+ expect(DelegatorJoinAction.prototype.execute).toHaveBeenCalledWith({
141
+ validator: "0xValidator",
142
+ amount: "42gen",
143
+ });
144
+ });
145
+ });
146
+
147
+ describe("delegator-exit", () => {
148
+ test("calls DelegatorExitAction.execute", async () => {
149
+ program.parse([
150
+ "node",
151
+ "test",
152
+ "staking",
153
+ "delegator-exit",
154
+ "--validator",
155
+ "0xValidator",
156
+ "--shares",
157
+ "50",
158
+ ]);
159
+
160
+ expect(DelegatorExitAction).toHaveBeenCalledTimes(1);
161
+ expect(DelegatorExitAction.prototype.execute).toHaveBeenCalledWith({
162
+ validator: "0xValidator",
163
+ shares: "50",
164
+ });
165
+ });
166
+ });
167
+
168
+ describe("delegator-claim", () => {
169
+ test("calls DelegatorClaimAction.execute", async () => {
170
+ program.parse([
171
+ "node",
172
+ "test",
173
+ "staking",
174
+ "delegator-claim",
175
+ "--validator",
176
+ "0xValidator",
177
+ "--delegator",
178
+ "0xDelegator",
179
+ ]);
180
+
181
+ expect(DelegatorClaimAction).toHaveBeenCalledTimes(1);
182
+ expect(DelegatorClaimAction.prototype.execute).toHaveBeenCalledWith({
183
+ validator: "0xValidator",
184
+ delegator: "0xDelegator",
185
+ });
186
+ });
187
+ });
188
+
189
+ describe("validator-info", () => {
190
+ test("calls StakingInfoAction.getValidatorInfo", async () => {
191
+ program.parse(["node", "test", "staking", "validator-info", "--validator", "0xValidator"]);
192
+
193
+ expect(StakingInfoAction).toHaveBeenCalledTimes(1);
194
+ expect(StakingInfoAction.prototype.getValidatorInfo).toHaveBeenCalledWith({
195
+ validator: "0xValidator",
196
+ });
197
+ });
198
+ });
199
+
200
+ describe("epoch-info", () => {
201
+ test("calls StakingInfoAction.getEpochInfo", async () => {
202
+ program.parse(["node", "test", "staking", "epoch-info"]);
203
+
204
+ expect(StakingInfoAction).toHaveBeenCalledTimes(1);
205
+ expect(StakingInfoAction.prototype.getEpochInfo).toHaveBeenCalledWith({});
206
+ });
207
+ });
208
+
209
+ describe("active-validators", () => {
210
+ test("calls StakingInfoAction.listActiveValidators", async () => {
211
+ program.parse(["node", "test", "staking", "active-validators"]);
212
+
213
+ expect(StakingInfoAction).toHaveBeenCalledTimes(1);
214
+ expect(StakingInfoAction.prototype.listActiveValidators).toHaveBeenCalledWith({});
215
+ });
216
+ });
217
+
218
+ describe("validator-prime", () => {
219
+ test("calls ValidatorPrimeAction.execute", async () => {
220
+ program.parse(["node", "test", "staking", "validator-prime", "--validator", "0xValidator"]);
221
+
222
+ expect(ValidatorPrimeAction).toHaveBeenCalledTimes(1);
223
+ expect(ValidatorPrimeAction.prototype.execute).toHaveBeenCalledWith({
224
+ validator: "0xValidator",
225
+ });
226
+ });
227
+ });
228
+
229
+ describe("set-operator", () => {
230
+ test("calls SetOperatorAction.execute", async () => {
231
+ program.parse([
232
+ "node",
233
+ "test",
234
+ "staking",
235
+ "set-operator",
236
+ "--validator",
237
+ "0xValidator",
238
+ "--operator",
239
+ "0xOperator",
240
+ ]);
241
+
242
+ expect(SetOperatorAction).toHaveBeenCalledTimes(1);
243
+ expect(SetOperatorAction.prototype.execute).toHaveBeenCalledWith({
244
+ validator: "0xValidator",
245
+ operator: "0xOperator",
246
+ });
247
+ });
248
+ });
249
+
250
+ describe("set-identity", () => {
251
+ test("calls SetIdentityAction.execute with required fields", async () => {
252
+ program.parse([
253
+ "node",
254
+ "test",
255
+ "staking",
256
+ "set-identity",
257
+ "--validator",
258
+ "0xValidator",
259
+ "--moniker",
260
+ "My Validator",
261
+ ]);
262
+
263
+ expect(SetIdentityAction).toHaveBeenCalledTimes(1);
264
+ expect(SetIdentityAction.prototype.execute).toHaveBeenCalledWith({
265
+ validator: "0xValidator",
266
+ moniker: "My Validator",
267
+ });
268
+ });
269
+
270
+ test("calls SetIdentityAction.execute with all optional fields", async () => {
271
+ program.parse([
272
+ "node",
273
+ "test",
274
+ "staking",
275
+ "set-identity",
276
+ "--validator",
277
+ "0xValidator",
278
+ "--moniker",
279
+ "My Validator",
280
+ "--website",
281
+ "https://example.com",
282
+ "--twitter",
283
+ "myhandle",
284
+ "--github",
285
+ "mygithub",
286
+ ]);
287
+
288
+ expect(SetIdentityAction.prototype.execute).toHaveBeenCalledWith({
289
+ validator: "0xValidator",
290
+ moniker: "My Validator",
291
+ website: "https://example.com",
292
+ twitter: "myhandle",
293
+ github: "mygithub",
294
+ });
295
+ });
296
+ });
297
+
298
+ describe("delegation-info", () => {
299
+ test("calls StakingInfoAction.getStakeInfo", async () => {
300
+ program.parse([
301
+ "node",
302
+ "test",
303
+ "staking",
304
+ "delegation-info",
305
+ "--validator",
306
+ "0xValidator",
307
+ ]);
308
+
309
+ expect(StakingInfoAction).toHaveBeenCalledTimes(1);
310
+ expect(StakingInfoAction.prototype.getStakeInfo).toHaveBeenCalledWith({
311
+ validator: "0xValidator",
312
+ });
313
+ });
314
+
315
+ test("calls StakingInfoAction.getStakeInfo with delegator", async () => {
316
+ program.parse([
317
+ "node",
318
+ "test",
319
+ "staking",
320
+ "delegation-info",
321
+ "--validator",
322
+ "0xValidator",
323
+ "--delegator",
324
+ "0xDelegator",
325
+ ]);
326
+
327
+ expect(StakingInfoAction.prototype.getStakeInfo).toHaveBeenCalledWith({
328
+ validator: "0xValidator",
329
+ delegator: "0xDelegator",
330
+ });
331
+ });
332
+ });
333
+ });
@@ -13,8 +13,8 @@ vi.mock("../src/commands/general", () => ({
13
13
  initializeGeneralCommands: vi.fn(),
14
14
  }));
15
15
 
16
- vi.mock("../src/commands/keygen", () => ({
17
- initializeKeygenCommands: vi.fn(),
16
+ vi.mock("../src/commands/account", () => ({
17
+ initializeAccountCommands: vi.fn(),
18
18
  }));
19
19
 
20
20
  vi.mock("../src/commands/contracts", () => ({
@@ -45,6 +45,10 @@ vi.mock("../src/commands/transactions", () => ({
45
45
  initializeTransactionsCommands: vi.fn(),
46
46
  }));
47
47
 
48
+ vi.mock("../src/commands/staking", () => ({
49
+ initializeStakingCommands: vi.fn(),
50
+ }));
51
+
48
52
  describe("CLI", () => {
49
53
  it("should initialize CLI", () => {
50
54
  expect(initializeCLI).not.toThrow();
@@ -28,10 +28,18 @@ describe("BaseAction", () => {
28
28
  let consoleErrorSpy: any;
29
29
  let processExitSpy: any;
30
30
 
31
+ // Standard web3 keystore format
31
32
  const mockKeystoreData = {
32
- version: 1,
33
- encrypted: '{"address":"test","crypto":{"cipher":"aes-128-ctr"}}',
34
- address: "0x1234567890123456789012345678901234567890",
33
+ address: "1234567890123456789012345678901234567890",
34
+ crypto: {
35
+ cipher: "aes-128-ctr",
36
+ ciphertext: "test",
37
+ cipherparams: {iv: "test"},
38
+ kdf: "scrypt",
39
+ kdfparams: {},
40
+ mac: "test"
41
+ },
42
+ version: 3
35
43
  };
36
44
 
37
45
  const mockWallet = {
@@ -81,8 +89,11 @@ describe("BaseAction", () => {
81
89
  vi.spyOn(baseAction as any, "getConfigByKey").mockReturnValue("./test-keypair.json");
82
90
  vi.spyOn(baseAction as any, "getFilePath").mockImplementation(() => "./test-keypair.json");
83
91
  vi.spyOn(baseAction as any, "writeConfig").mockImplementation(() => {});
84
- vi.spyOn(baseAction as any, "getConfig").mockReturnValue({});
85
-
92
+ vi.spyOn(baseAction as any, "getConfig").mockReturnValue({activeAccount: "default"});
93
+ vi.spyOn(baseAction as any, "resolveAccountName").mockReturnValue("default");
94
+ vi.spyOn(baseAction as any, "getKeystorePath").mockReturnValue("/mocked/home/.genlayer/keystores/default.json");
95
+ vi.spyOn(baseAction as any, "getActiveAccount").mockReturnValue("default");
96
+
86
97
  // Mock keychainManager methods
87
98
  vi.spyOn(baseAction["keychainManager"], "isKeychainAvailable").mockResolvedValue(false);
88
99
  vi.spyOn(baseAction["keychainManager"], "getPrivateKey").mockResolvedValue(null);
@@ -109,7 +120,7 @@ describe("BaseAction", () => {
109
120
 
110
121
  test("should fail the spinner with an error message", () => {
111
122
  const error = new Error("Something went wrong");
112
- baseAction["failSpinner"]("Failure", error);
123
+ baseAction["failSpinner"]("Failure", error, false); // Don't exit for test
113
124
 
114
125
  expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Error:"));
115
126
  expect(consoleSpy).toHaveBeenCalledWith(inspect(error, {depth: null, colors: false}));
@@ -117,6 +128,12 @@ describe("BaseAction", () => {
117
128
  expect(mockSpinner.fail).toHaveBeenCalledWith(expect.stringContaining("Failure"));
118
129
  });
119
130
 
131
+ test("should fail the spinner and exit by default", () => {
132
+ const error = new Error("Fatal error");
133
+ expect(() => baseAction["failSpinner"]("Fatal", error)).toThrow("process exited");
134
+ expect(processExitSpy).toHaveBeenCalledWith(1);
135
+ });
136
+
120
137
  test("should stop the spinner", () => {
121
138
  baseAction["stopSpinner"]();
122
139
  expect(mockSpinner.stop).toHaveBeenCalled();
@@ -256,16 +273,16 @@ describe("BaseAction", () => {
256
273
  const account = await baseAction["getAccount"](false);
257
274
 
258
275
  expect((account as any).privateKey).toBe(mockWallet.privateKey);
259
- expect(existsSync).toHaveBeenCalledWith("./test-keypair.json");
260
- expect(readFileSync).toHaveBeenCalledWith("./test-keypair.json", "utf-8");
276
+ expect(existsSync).toHaveBeenCalledWith("/mocked/home/.genlayer/keystores/default.json");
277
+ expect(readFileSync).toHaveBeenCalledWith("/mocked/home/.genlayer/keystores/default.json", "utf-8");
261
278
  });
262
279
 
263
280
  test("should return address when called with readOnly=true", async () => {
264
281
  const address = await baseAction["getAccount"](true);
265
-
282
+
266
283
  expect(address).toBe(mockKeystoreData.address);
267
- expect(existsSync).toHaveBeenCalledWith("./test-keypair.json");
268
- expect(readFileSync).toHaveBeenCalledWith("./test-keypair.json", "utf-8");
284
+ expect(existsSync).toHaveBeenCalledWith("/mocked/home/.genlayer/keystores/default.json");
285
+ expect(readFileSync).toHaveBeenCalledWith("/mocked/home/.genlayer/keystores/default.json", "utf-8");
269
286
  });
270
287
 
271
288
  test("should create new keypair when keystore file does not exist", async () => {
@@ -279,7 +296,7 @@ describe("BaseAction", () => {
279
296
 
280
297
  expect((account as any).privateKey).toBe(mockWallet.privateKey);
281
298
  expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
282
- expect.objectContaining({message: chalk.yellow("Keypair file not found. Would you like to create a new keypair?")})
299
+ expect.objectContaining({message: chalk.yellow("Account 'default' not found. Would you like to create it?")})
283
300
  ]));
284
301
  });
285
302
 
@@ -298,7 +315,7 @@ describe("BaseAction", () => {
298
315
  const account = await baseAction["getAccount"](false);
299
316
 
300
317
  expect((account as any).privateKey).toBe(mockWallet.privateKey);
301
- expect(baseAction["keychainManager"].getPrivateKey).toHaveBeenCalled();
318
+ expect(baseAction["keychainManager"].getPrivateKey).toHaveBeenCalledWith("default");
302
319
  expect(inquirer.prompt).not.toHaveBeenCalled();
303
320
  });
304
321
 
@@ -314,14 +331,14 @@ describe("BaseAction", () => {
314
331
  expect((account as any).privateKey).toBe(mockWallet.privateKey);
315
332
  expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Invalid keystore format. Expected encrypted keystore file."));
316
333
  expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
317
- expect.objectContaining({message: chalk.yellow("Would you like to create a new keypair?")})
334
+ expect.objectContaining({message: "Would you like to recreate account 'default'?"})
318
335
  ]));
319
336
  });
320
337
 
321
338
  test("should decrypt keystore successfully on first attempt", async () => {
322
339
  vi.mocked(inquirer.prompt).mockResolvedValue({password: "correct-password"});
323
340
 
324
- const result = await baseAction["decryptKeystore"](mockKeystoreData);
341
+ const result = await baseAction["decryptKeystore"](JSON.stringify(mockKeystoreData));
325
342
 
326
343
  expect(result).toBe(mockWallet.privateKey);
327
344
  expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
@@ -333,12 +350,12 @@ describe("BaseAction", () => {
333
350
  vi.mocked(ethers.Wallet.fromEncryptedJson)
334
351
  .mockRejectedValueOnce(new Error("Incorrect password"))
335
352
  .mockResolvedValueOnce(mockWallet as any);
336
-
353
+
337
354
  vi.mocked(inquirer.prompt)
338
355
  .mockResolvedValueOnce({password: "wrong-password"})
339
356
  .mockResolvedValueOnce({password: "correct-password"});
340
357
 
341
- const result = await baseAction["decryptKeystore"](mockKeystoreData);
358
+ const result = await baseAction["decryptKeystore"](JSON.stringify(mockKeystoreData));
342
359
 
343
360
  expect(result).toBe(mockWallet.privateKey);
344
361
  expect(inquirer.prompt).toHaveBeenCalledTimes(2);
@@ -351,8 +368,8 @@ describe("BaseAction", () => {
351
368
  vi.mocked(ethers.Wallet.fromEncryptedJson).mockRejectedValue(new Error("Incorrect password"));
352
369
  vi.mocked(inquirer.prompt).mockResolvedValue({password: "wrong-password"});
353
370
 
354
- await expect(baseAction["decryptKeystore"](mockKeystoreData)).rejects.toThrow("process exited");
355
-
371
+ await expect(baseAction["decryptKeystore"](JSON.stringify(mockKeystoreData))).rejects.toThrow("process exited");
372
+
356
373
  expect(inquirer.prompt).toHaveBeenCalledTimes(3);
357
374
  expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Maximum password attempts exceeded (3/3)."));
358
375
  expect(processExitSpy).toHaveBeenCalledWith(1);
@@ -364,22 +381,22 @@ describe("BaseAction", () => {
364
381
  .mockResolvedValueOnce({password: "test-password"})
365
382
  .mockResolvedValueOnce({password: "test-password"});
366
383
 
367
- const result = await baseAction["createKeypair"]("./new-keypair.json", false);
384
+ const result = await baseAction["createKeypairByName"]("test-account", false);
368
385
 
369
386
  expect(result).toBe(mockWallet.privateKey);
370
387
  expect(ethers.Wallet.createRandom).toHaveBeenCalled();
371
388
  expect(mockWallet.encrypt).toHaveBeenCalledWith("test-password");
372
389
  expect(writeFileSync).toHaveBeenCalled();
373
- expect(baseAction["keychainManager"].removePrivateKey).toHaveBeenCalled();
390
+ expect(baseAction["keychainManager"].removePrivateKey).toHaveBeenCalledWith("test-account");
374
391
  });
375
392
 
376
- test("should fail when file exists and overwrite is false", async () => {
393
+ test("should fail when account exists and overwrite is false", async () => {
377
394
  vi.mocked(existsSync).mockReturnValue(true);
378
395
 
379
- await expect(baseAction["createKeypair"]("./test-keypair.json", false)).rejects.toThrow("process exited");
380
-
396
+ await expect(baseAction["createKeypairByName"]("test-account", false)).rejects.toThrow("process exited");
397
+
381
398
  expect(mockSpinner.fail).toHaveBeenCalledWith(
382
- chalk.red("The file at ./test-keypair.json already exists. Use the '--overwrite' option to replace it.")
399
+ chalk.red("Account 'test-account' already exists. Use '--overwrite' to replace it.")
383
400
  );
384
401
  });
385
402
 
@@ -389,8 +406,8 @@ describe("BaseAction", () => {
389
406
  .mockResolvedValueOnce({password: "password1"})
390
407
  .mockResolvedValueOnce({password: "password2"});
391
408
 
392
- await expect(baseAction["createKeypair"]("./new-keypair.json", false)).rejects.toThrow("process exited");
393
-
409
+ await expect(baseAction["createKeypairByName"]("test-account", false)).rejects.toThrow("process exited");
410
+
394
411
  expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Passwords do not match"));
395
412
  });
396
413
 
@@ -400,50 +417,62 @@ describe("BaseAction", () => {
400
417
  .mockResolvedValueOnce({password: "short"})
401
418
  .mockResolvedValueOnce({password: "short"});
402
419
 
403
- await expect(baseAction["createKeypair"]("./new-keypair.json", false)).rejects.toThrow("process exited");
404
-
420
+ await expect(baseAction["createKeypairByName"]("test-account", false)).rejects.toThrow("process exited");
421
+
405
422
  expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Password must be at least 8 characters long"));
406
423
  });
407
424
 
408
- test("should overwrite existing file when overwrite is true", async () => {
425
+ test("should overwrite existing account when overwrite is true", async () => {
409
426
  vi.mocked(existsSync).mockReturnValue(true);
410
427
  vi.mocked(inquirer.prompt)
411
428
  .mockResolvedValueOnce({password: "test-password"})
412
429
  .mockResolvedValueOnce({password: "test-password"});
413
430
 
414
- const result = await baseAction["createKeypair"]("./existing.json", true);
431
+ const result = await baseAction["createKeypairByName"]("test-account", true);
415
432
 
416
433
  expect(result).toBe(mockWallet.privateKey);
417
434
  expect(writeFileSync).toHaveBeenCalled();
418
- expect(baseAction["keychainManager"].removePrivateKey).toHaveBeenCalled();
435
+ expect(baseAction["keychainManager"].removePrivateKey).toHaveBeenCalledWith("test-account");
419
436
  });
420
437
 
421
438
  test("should return true for valid keystore format", () => {
439
+ // Standard web3 keystore format
440
+ const validKeystore = {
441
+ address: "1234567890123456789012345678901234567890",
442
+ crypto: {cipher: "aes-128-ctr", ciphertext: "test"},
443
+ version: 3,
444
+ };
445
+
446
+ const result = baseAction["isValidKeystoreFormat"](validKeystore);
447
+ expect(result).toBe(true);
448
+ });
449
+
450
+ test("should return true for keystore with uppercase Crypto field", () => {
451
+ // Some tools use uppercase 'Crypto'
422
452
  const validKeystore = {
423
- version: 1,
424
- encrypted: "encrypted-data",
425
- address: "0x1234567890123456789012345678901234567890",
453
+ address: "1234567890123456789012345678901234567890",
454
+ Crypto: {cipher: "aes-128-ctr", ciphertext: "test"},
455
+ version: 3,
426
456
  };
427
457
 
428
458
  const result = baseAction["isValidKeystoreFormat"](validKeystore);
429
459
  expect(result).toBe(true);
430
460
  });
431
461
 
432
- test("should return false for invalid keystore version", () => {
462
+ test("should return false for keystore missing crypto field", () => {
433
463
  const invalidKeystore = {
434
- version: 2,
435
- encrypted: "encrypted-data",
436
- address: "0x1234567890123456789012345678901234567890",
464
+ address: "1234567890123456789012345678901234567890",
465
+ version: 3,
437
466
  };
438
467
 
439
468
  const result = baseAction["isValidKeystoreFormat"](invalidKeystore);
440
469
  expect(result).toBe(false);
441
470
  });
442
471
 
443
- test("should return false for keystore missing fields", () => {
472
+ test("should return false for keystore missing address", () => {
444
473
  const invalidKeystore = {
445
- version: 1,
446
- encrypted: "encrypted-data",
474
+ crypto: {cipher: "aes-128-ctr"},
475
+ version: 3,
447
476
  };
448
477
 
449
478
  const result = baseAction["isValidKeystoreFormat"](invalidKeystore);
@@ -10,6 +10,7 @@ vi.mock("os")
10
10
 
11
11
  describe("ConfigFileManager", () => {
12
12
  const mockFolderPath = "/mocked/home/.genlayer";
13
+ const mockKeystoresPath = `${mockFolderPath}/keystores`;
13
14
  const mockConfigFilePath = `${mockFolderPath}/genlayer-config.json`;
14
15
 
15
16
  let configFileManager: ConfigFileManager;
@@ -17,6 +18,8 @@ describe("ConfigFileManager", () => {
17
18
  beforeEach(() => {
18
19
  vi.clearAllMocks();
19
20
  vi.mocked(os.homedir).mockReturnValue("/mocked/home");
21
+ vi.mocked(fs.existsSync).mockReturnValue(true);
22
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({}));
20
23
  configFileManager = new ConfigFileManager();
21
24
  });
22
25
 
@@ -26,12 +29,16 @@ describe("ConfigFileManager", () => {
26
29
 
27
30
  test("ensures folder and config file are created if they don't exist", () => {
28
31
  vi.mocked(fs.existsSync).mockReturnValue(false);
32
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({}));
29
33
 
30
34
  new ConfigFileManager();
31
35
 
32
36
  expect(fs.existsSync).toHaveBeenCalledWith(mockFolderPath);
33
37
  expect(fs.mkdirSync).toHaveBeenCalledWith(mockFolderPath, { recursive: true });
34
38
 
39
+ expect(fs.existsSync).toHaveBeenCalledWith(mockKeystoresPath);
40
+ expect(fs.mkdirSync).toHaveBeenCalledWith(mockKeystoresPath, { recursive: true });
41
+
35
42
  expect(fs.existsSync).toHaveBeenCalledWith(mockConfigFilePath);
36
43
  expect(fs.writeFileSync).toHaveBeenCalledWith(mockConfigFilePath, JSON.stringify({}, null, 2));
37
44
  });
@@ -39,11 +46,11 @@ describe("ConfigFileManager", () => {
39
46
  test("does not recreate folder or config file if they exist", () => {
40
47
  vi.clearAllMocks();
41
48
  vi.mocked(fs.existsSync).mockReturnValue(true);
49
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({}));
42
50
 
43
51
  new ConfigFileManager();
44
52
 
45
53
  expect(fs.mkdirSync).not.toHaveBeenCalled();
46
- expect(fs.writeFileSync).not.toHaveBeenCalled();
47
54
  });
48
55
 
49
56
  test("getFolderPath returns the correct folder path", () => {