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,56 @@
1
+ import {describe, it, vi, expect} from "vitest";
2
+ import {initializeCLI} from "../src/index";
3
+
4
+ vi.mock("commander", () => ({
5
+ program: {
6
+ version: vi.fn().mockReturnThis(),
7
+ description: vi.fn().mockReturnThis(),
8
+ parse: vi.fn(),
9
+ },
10
+ }));
11
+
12
+ vi.mock("../src/commands/general", () => ({
13
+ initializeGeneralCommands: vi.fn(),
14
+ }));
15
+
16
+ vi.mock("../src/commands/account", () => ({
17
+ initializeAccountCommands: vi.fn(),
18
+ }));
19
+
20
+ vi.mock("../src/commands/contracts", () => ({
21
+ initializeContractsCommands: vi.fn(),
22
+ }));
23
+
24
+ vi.mock("../src/commands/config", () => ({
25
+ initializeConfigCommands: vi.fn(),
26
+ }));
27
+
28
+ vi.mock("../src/commands/localnet", () => ({
29
+ initializeValidatorCommands: vi.fn(),
30
+ }));
31
+
32
+ vi.mock("../src/commands/update", () => ({
33
+ initializeUpdateCommands: vi.fn(),
34
+ }));
35
+
36
+ vi.mock("../src/commands/scaffold", () => ({
37
+ initializeScaffoldCommands: vi.fn(),
38
+ }));
39
+
40
+ vi.mock("../src/commands/network", () => ({
41
+ initializeNetworkCommands: vi.fn(),
42
+ }));
43
+
44
+ vi.mock("../src/commands/transactions", () => ({
45
+ initializeTransactionsCommands: vi.fn(),
46
+ }));
47
+
48
+ vi.mock("../src/commands/staking", () => ({
49
+ initializeStakingCommands: vi.fn(),
50
+ }));
51
+
52
+ describe("CLI", () => {
53
+ it("should initialize CLI", () => {
54
+ expect(initializeCLI).not.toThrow();
55
+ });
56
+ });
@@ -0,0 +1,535 @@
1
+ import {describe, test, vi, beforeEach, afterEach, expect, Mock} from "vitest";
2
+ import {BaseAction} from "../../src/lib/actions/BaseAction";
3
+ import inquirer from "inquirer";
4
+ import ora, {Ora} from "ora";
5
+ import chalk from "chalk";
6
+ import {inspect} from "util";
7
+ import { ethers } from "ethers";
8
+ import { writeFileSync, existsSync, readFileSync } from "fs";
9
+ import fs from "fs";
10
+ import os from "os";
11
+ import { createAccount } from "genlayer-js";
12
+
13
+ vi.mock("inquirer");
14
+ vi.mock("ora");
15
+ vi.mock("fs");
16
+ vi.mock("os");
17
+ vi.mock("ethers");
18
+ vi.mock("genlayer-js", () => ({
19
+ createAccount: vi.fn(),
20
+ createClient: vi.fn(),
21
+ localnet: {}
22
+ }));
23
+
24
+ describe("BaseAction", () => {
25
+ let baseAction: BaseAction;
26
+ let mockSpinner: Ora;
27
+ let consoleSpy: any;
28
+ let consoleErrorSpy: any;
29
+ let processExitSpy: any;
30
+
31
+ // Standard web3 keystore format
32
+ const mockKeystoreData = {
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
43
+ };
44
+
45
+ const mockWallet = {
46
+ privateKey: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
47
+ address: "0x1234567890123456789012345678901234567890",
48
+ encrypt: vi.fn().mockResolvedValue('{"address":"test","crypto":{"cipher":"aes-128-ctr"}}'),
49
+ };
50
+
51
+ beforeEach(() => {
52
+ vi.clearAllMocks();
53
+ consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
54
+ consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
55
+ processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
56
+ throw new Error("process exited");
57
+ });
58
+ mockSpinner = {
59
+ start: vi.fn(),
60
+ stop: vi.fn(),
61
+ succeed: vi.fn(),
62
+ fail: vi.fn(),
63
+ text: "",
64
+ } as unknown as Ora;
65
+
66
+ (ora as unknown as Mock).mockReturnValue(mockSpinner);
67
+
68
+ vi.mocked(os.homedir).mockReturnValue("/mocked/home");
69
+ vi.mocked(os.tmpdir).mockReturnValue("/mocked/tmp");
70
+
71
+ vi.mocked(existsSync).mockReturnValue(true);
72
+ vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockKeystoreData));
73
+ vi.mocked(writeFileSync).mockImplementation(() => {});
74
+ vi.mocked(fs.readdirSync).mockReturnValue([] as any);
75
+ vi.mocked(fs.mkdirSync).mockImplementation(() => "/mocked/path");
76
+
77
+ // Mock ethers
78
+ vi.mocked(ethers.Wallet.createRandom).mockReturnValue(mockWallet as any);
79
+ vi.mocked(ethers.Wallet.fromEncryptedJson).mockResolvedValue(mockWallet as any);
80
+
81
+ vi.mocked(createAccount).mockReturnValue({
82
+ privateKey: mockWallet.privateKey,
83
+ address: mockWallet.address
84
+ } as any);
85
+
86
+ baseAction = new BaseAction();
87
+
88
+ // Mock config methods
89
+ vi.spyOn(baseAction as any, "getConfigByKey").mockReturnValue("./test-keypair.json");
90
+ vi.spyOn(baseAction as any, "getFilePath").mockImplementation(() => "./test-keypair.json");
91
+ vi.spyOn(baseAction as any, "writeConfig").mockImplementation(() => {});
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
+
97
+ // Mock keychainManager methods
98
+ vi.spyOn(baseAction["keychainManager"], "isKeychainAvailable").mockResolvedValue(false);
99
+ vi.spyOn(baseAction["keychainManager"], "getPrivateKey").mockResolvedValue(null);
100
+ vi.spyOn(baseAction["keychainManager"], "storePrivateKey").mockResolvedValue();
101
+ vi.spyOn(baseAction["keychainManager"], "removePrivateKey").mockResolvedValue(true);
102
+
103
+ });
104
+
105
+ afterEach(() => {
106
+ vi.restoreAllMocks();
107
+ });
108
+
109
+ test("should start the spinner with a message", () => {
110
+ baseAction["startSpinner"]("Loading...");
111
+ expect(mockSpinner.start).toHaveBeenCalled();
112
+ expect(mockSpinner.text).toBe(chalk.blue("Loading..."));
113
+ });
114
+
115
+ test("should succeed the spinner with a message", () => {
116
+ baseAction["succeedSpinner"]("Success");
117
+ expect(consoleSpy).toHaveBeenCalledWith("");
118
+ expect(mockSpinner.succeed).toHaveBeenCalledWith(expect.stringContaining("Success"));
119
+ });
120
+
121
+ test("should fail the spinner with an error message", () => {
122
+ const error = new Error("Something went wrong");
123
+ baseAction["failSpinner"]("Failure", error, false); // Don't exit for test
124
+
125
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Error:"));
126
+ expect(consoleSpy).toHaveBeenCalledWith(inspect(error, {depth: null, colors: false}));
127
+ expect(consoleSpy).toHaveBeenCalledWith("");
128
+ expect(mockSpinner.fail).toHaveBeenCalledWith(expect.stringContaining("Failure"));
129
+ });
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
+
137
+ test("should stop the spinner", () => {
138
+ baseAction["stopSpinner"]();
139
+ expect(mockSpinner.stop).toHaveBeenCalled();
140
+ });
141
+
142
+ test("should set spinner text", () => {
143
+ baseAction["setSpinnerText"]("Updated text");
144
+ expect(mockSpinner.text).toBe(chalk.blue("Updated text"));
145
+ });
146
+
147
+ test("should confirm prompt and proceed when confirmed", async () => {
148
+ vi.mocked(inquirer.prompt).mockResolvedValue({confirmAction: true});
149
+
150
+ await expect(baseAction["confirmPrompt"]("Are you sure?")).resolves.not.toThrow();
151
+ expect(inquirer.prompt).toHaveBeenCalled();
152
+ });
153
+
154
+ test("should confirm prompt and exit when declined", async () => {
155
+ vi.mocked(inquirer.prompt).mockResolvedValue({confirmAction: false});
156
+ const processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
157
+ throw new Error("process exited");
158
+ });
159
+
160
+ await expect(baseAction["confirmPrompt"]("Are you sure?")).rejects.toThrow("process exited");
161
+ expect(inquirer.prompt).toHaveBeenCalled();
162
+ expect(processExitSpy).toHaveBeenCalledWith(0);
163
+ });
164
+
165
+ test("should log a success message", () => {
166
+ baseAction["logSuccess"]("Success message");
167
+
168
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("✔ Success message"));
169
+ });
170
+
171
+ test("should log an error message", () => {
172
+ baseAction["logError"]("Error message");
173
+
174
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("✖ Error message"));
175
+ });
176
+
177
+ test("should log a info message", () => {
178
+ baseAction["logInfo"]("Info message");
179
+
180
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("ℹ Info message"));
181
+ });
182
+
183
+ test("should log a warning message", () => {
184
+ baseAction["logWarning"]("Warning message");
185
+
186
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("⚠ Warning message"));
187
+ });
188
+
189
+ test("should log a success message with data", () => {
190
+ const data = {key: "value"};
191
+
192
+ baseAction["logSuccess"]("Success message", data);
193
+
194
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("✔ Success message"));
195
+ expect(consoleSpy).toHaveBeenCalledWith(chalk.green(inspect(data, {depth: null, colors: false})));
196
+ });
197
+
198
+ test("should log an error message with error details", () => {
199
+ const error = new Error("Something went wrong");
200
+
201
+ baseAction["logError"]("Error message", error);
202
+
203
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("✖ Error message"));
204
+ expect(consoleErrorSpy).toHaveBeenCalledWith(chalk.red(inspect(error, {depth: null, colors: false})));
205
+ });
206
+
207
+ test("should log an info message with data", () => {
208
+ const data = {info: "This is some info"};
209
+
210
+ baseAction["logInfo"]("Info message", data);
211
+
212
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("ℹ Info message"));
213
+ expect(consoleSpy).toHaveBeenCalledWith(chalk.blue(inspect(data, {depth: null, colors: false})));
214
+ });
215
+
216
+ test("should log a warning message with data", () => {
217
+ const data = {warning: "This is a warning"};
218
+
219
+ baseAction["logWarning"]("Warning message", data);
220
+
221
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("⚠ Warning message"));
222
+ expect(consoleSpy).toHaveBeenCalledWith(chalk.yellow(inspect(data, {depth: null, colors: false})));
223
+ });
224
+
225
+ test("should succeed the spinner with a message and log result if data is provided", () => {
226
+ const mockData = {key: "value"};
227
+
228
+ baseAction["succeedSpinner"]("Success", mockData);
229
+
230
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Result:"));
231
+ expect(consoleSpy).toHaveBeenCalledWith(inspect(mockData, {depth: null, colors: false}));
232
+ expect(consoleSpy).toHaveBeenCalledWith("");
233
+ expect(mockSpinner.succeed).toHaveBeenCalledWith(expect.stringContaining("Success"));
234
+ });
235
+
236
+ test("should return a string representation of a primitive", () => {
237
+ expect((baseAction as any).formatOutput("Hello")).toBe("Hello");
238
+ expect((baseAction as any).formatOutput(42)).toBe("42");
239
+ expect((baseAction as any).formatOutput(true)).toBe("true");
240
+ });
241
+
242
+ test("should prompt for password successfully", async () => {
243
+ const mockPassword = "test-password";
244
+ vi.mocked(inquirer.prompt).mockResolvedValue({password: mockPassword});
245
+
246
+ const result = await baseAction["promptPassword"]("Enter password:");
247
+
248
+ expect(result).toBe(mockPassword);
249
+ expect(inquirer.prompt).toHaveBeenCalledWith([{
250
+ type: "password",
251
+ name: "password",
252
+ message: chalk.yellow("Enter password:"),
253
+ mask: "*",
254
+ validate: expect.any(Function),
255
+ }]);
256
+ });
257
+
258
+ test("should validate password input is not empty", async () => {
259
+ vi.mocked(inquirer.prompt).mockResolvedValue({password: "valid-password"});
260
+
261
+ await baseAction["promptPassword"]("Enter password:");
262
+
263
+ const mockCall = vi.mocked(inquirer.prompt).mock.calls[0];
264
+ const questions = mockCall[0] as any;
265
+ const validateFn = questions[0].validate;
266
+ expect(validateFn("")).toBe("Password cannot be empty");
267
+ expect(validateFn("valid")).toBe(true);
268
+ });
269
+
270
+ test("should return private key when keystore exists and is valid", async () => {
271
+ vi.mocked(inquirer.prompt).mockResolvedValue({password: "correct-password"});
272
+
273
+ const account = await baseAction["getAccount"](false);
274
+
275
+ expect((account as any).privateKey).toBe(mockWallet.privateKey);
276
+ expect(existsSync).toHaveBeenCalledWith("/mocked/home/.genlayer/keystores/default.json");
277
+ expect(readFileSync).toHaveBeenCalledWith("/mocked/home/.genlayer/keystores/default.json", "utf-8");
278
+ });
279
+
280
+ test("should return address when called with readOnly=true", async () => {
281
+ const address = await baseAction["getAccount"](true);
282
+
283
+ expect(address).toBe(`0x${mockKeystoreData.address}`);
284
+ expect(existsSync).toHaveBeenCalledWith("/mocked/home/.genlayer/keystores/default.json");
285
+ expect(readFileSync).toHaveBeenCalledWith("/mocked/home/.genlayer/keystores/default.json", "utf-8");
286
+ });
287
+
288
+ test("should return 0x-prefixed address for readOnly even when keystore has no prefix", async () => {
289
+ const address = await baseAction["getAccount"](true);
290
+
291
+ expect(address).toMatch(/^0x/);
292
+ expect(address).toBe(`0x${mockKeystoreData.address}`);
293
+ });
294
+
295
+ test("should not double-prefix address that already has 0x", async () => {
296
+ const prefixedKeystoreData = {
297
+ ...mockKeystoreData,
298
+ address: "0x1234567890123456789012345678901234567890",
299
+ };
300
+ vi.mocked(readFileSync).mockReturnValue(JSON.stringify(prefixedKeystoreData));
301
+
302
+ const address = await baseAction["getAccount"](true);
303
+
304
+ expect(address).toBe("0x1234567890123456789012345678901234567890");
305
+ });
306
+
307
+ test("should create new keypair when keystore file does not exist", async () => {
308
+ vi.mocked(existsSync).mockReturnValue(false);
309
+ vi.mocked(inquirer.prompt)
310
+ .mockResolvedValueOnce({confirmAction: true}) // confirm create new
311
+ .mockResolvedValueOnce({password: "new-password"}) // encrypt password
312
+ .mockResolvedValueOnce({password: "new-password"}); // confirm password
313
+
314
+ const account = await baseAction["getAccount"](false);
315
+
316
+ expect((account as any).privateKey).toBe(mockWallet.privateKey);
317
+ expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
318
+ expect.objectContaining({message: chalk.yellow("Account 'default' not found. Would you like to create it?")})
319
+ ]));
320
+ });
321
+
322
+ test("should fail when keystore format is invalid and user declines", async () => {
323
+ vi.mocked(readFileSync).mockReturnValue('{"invalid": "format"}');
324
+ vi.mocked(inquirer.prompt).mockResolvedValue({confirmAction: false});
325
+
326
+ await expect(baseAction["getAccount"](false)).rejects.toThrow("process exited");
327
+ expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Invalid keystore format. Expected encrypted keystore file."));
328
+ });
329
+
330
+ test("should use cached key when available", async () => {
331
+ vi.spyOn(baseAction["keychainManager"], "isKeychainAvailable").mockResolvedValue(true);
332
+ vi.spyOn(baseAction["keychainManager"], "getPrivateKey").mockResolvedValue(mockWallet.privateKey);
333
+
334
+ const account = await baseAction["getAccount"](false);
335
+
336
+ expect((account as any).privateKey).toBe(mockWallet.privateKey);
337
+ expect(baseAction["keychainManager"].getPrivateKey).toHaveBeenCalledWith("default");
338
+ expect(inquirer.prompt).not.toHaveBeenCalled();
339
+ });
340
+
341
+ test("should create new keypair when keystore format is invalid and user confirms", async () => {
342
+ vi.mocked(readFileSync).mockReturnValue('{"invalid": "format"}');
343
+ vi.mocked(inquirer.prompt)
344
+ .mockResolvedValueOnce({confirmAction: true})
345
+ .mockResolvedValueOnce({password: "new-password"})
346
+ .mockResolvedValueOnce({password: "new-password"});
347
+
348
+ const account = await baseAction["getAccount"](false);
349
+
350
+ expect((account as any).privateKey).toBe(mockWallet.privateKey);
351
+ expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Invalid keystore format. Expected encrypted keystore file."));
352
+ expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
353
+ expect.objectContaining({message: "Would you like to recreate account 'default'?"})
354
+ ]));
355
+ });
356
+
357
+ test("should decrypt keystore successfully on first attempt", async () => {
358
+ vi.mocked(inquirer.prompt).mockResolvedValue({password: "correct-password"});
359
+
360
+ const result = await baseAction["decryptKeystore"](JSON.stringify(mockKeystoreData));
361
+
362
+ expect(result).toBe(mockWallet.privateKey);
363
+ expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
364
+ expect.objectContaining({message: chalk.yellow("Enter password to decrypt keystore:")})
365
+ ]));
366
+ });
367
+
368
+ test("should retry on wrong password and succeed on second attempt", async () => {
369
+ vi.mocked(ethers.Wallet.fromEncryptedJson)
370
+ .mockRejectedValueOnce(new Error("Incorrect password"))
371
+ .mockResolvedValueOnce(mockWallet as any);
372
+
373
+ vi.mocked(inquirer.prompt)
374
+ .mockResolvedValueOnce({password: "wrong-password"})
375
+ .mockResolvedValueOnce({password: "correct-password"});
376
+
377
+ const result = await baseAction["decryptKeystore"](JSON.stringify(mockKeystoreData));
378
+
379
+ expect(result).toBe(mockWallet.privateKey);
380
+ expect(inquirer.prompt).toHaveBeenCalledTimes(2);
381
+ expect(inquirer.prompt).toHaveBeenNthCalledWith(2, expect.arrayContaining([
382
+ expect.objectContaining({message: chalk.yellow("Invalid password. Attempt 2/3 - Enter password to decrypt keystore:")})
383
+ ]));
384
+ });
385
+
386
+ test("should exit after 3 failed password attempts", async () => {
387
+ vi.mocked(ethers.Wallet.fromEncryptedJson).mockRejectedValue(new Error("Incorrect password"));
388
+ vi.mocked(inquirer.prompt).mockResolvedValue({password: "wrong-password"});
389
+
390
+ await expect(baseAction["decryptKeystore"](JSON.stringify(mockKeystoreData))).rejects.toThrow("process exited");
391
+
392
+ expect(inquirer.prompt).toHaveBeenCalledTimes(3);
393
+ expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Maximum password attempts exceeded (3/3)."));
394
+ expect(processExitSpy).toHaveBeenCalledWith(1);
395
+ });
396
+
397
+ test("should create new keypair successfully", async () => {
398
+ vi.mocked(existsSync).mockReturnValue(false);
399
+ vi.mocked(inquirer.prompt)
400
+ .mockResolvedValueOnce({password: "test-password"})
401
+ .mockResolvedValueOnce({password: "test-password"});
402
+
403
+ const result = await baseAction["createKeypairByName"]("test-account", false);
404
+
405
+ expect(result).toBe(mockWallet.privateKey);
406
+ expect(ethers.Wallet.createRandom).toHaveBeenCalled();
407
+ expect(mockWallet.encrypt).toHaveBeenCalledWith("test-password");
408
+ expect(writeFileSync).toHaveBeenCalled();
409
+ expect(baseAction["keychainManager"].removePrivateKey).toHaveBeenCalledWith("test-account");
410
+ });
411
+
412
+ test("should fail when account exists and overwrite is false", async () => {
413
+ vi.mocked(existsSync).mockReturnValue(true);
414
+
415
+ await expect(baseAction["createKeypairByName"]("test-account", false)).rejects.toThrow("process exited");
416
+
417
+ expect(mockSpinner.fail).toHaveBeenCalledWith(
418
+ chalk.red("Account 'test-account' already exists. Use '--overwrite' to replace it.")
419
+ );
420
+ });
421
+
422
+ test("should fail when passwords do not match", async () => {
423
+ vi.mocked(existsSync).mockReturnValue(false);
424
+ vi.mocked(inquirer.prompt)
425
+ .mockResolvedValueOnce({password: "password1"})
426
+ .mockResolvedValueOnce({password: "password2"});
427
+
428
+ await expect(baseAction["createKeypairByName"]("test-account", false)).rejects.toThrow("process exited");
429
+
430
+ expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Passwords do not match"));
431
+ });
432
+
433
+ test("should fail when password is too short", async () => {
434
+ vi.mocked(existsSync).mockReturnValue(false);
435
+ vi.mocked(inquirer.prompt)
436
+ .mockResolvedValueOnce({password: "short"})
437
+ .mockResolvedValueOnce({password: "short"});
438
+
439
+ await expect(baseAction["createKeypairByName"]("test-account", false)).rejects.toThrow("process exited");
440
+
441
+ expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Password must be at least 8 characters long"));
442
+ });
443
+
444
+ test("should overwrite existing account when overwrite is true", async () => {
445
+ vi.mocked(existsSync).mockReturnValue(true);
446
+ vi.mocked(inquirer.prompt)
447
+ .mockResolvedValueOnce({password: "test-password"})
448
+ .mockResolvedValueOnce({password: "test-password"});
449
+
450
+ const result = await baseAction["createKeypairByName"]("test-account", true);
451
+
452
+ expect(result).toBe(mockWallet.privateKey);
453
+ expect(writeFileSync).toHaveBeenCalled();
454
+ expect(baseAction["keychainManager"].removePrivateKey).toHaveBeenCalledWith("test-account");
455
+ });
456
+
457
+ test("should return true for valid keystore format", () => {
458
+ // Standard web3 keystore format
459
+ const validKeystore = {
460
+ address: "1234567890123456789012345678901234567890",
461
+ crypto: {cipher: "aes-128-ctr", ciphertext: "test"},
462
+ version: 3,
463
+ };
464
+
465
+ const result = baseAction["isValidKeystoreFormat"](validKeystore);
466
+ expect(result).toBe(true);
467
+ });
468
+
469
+ test("should return true for keystore with uppercase Crypto field", () => {
470
+ // Some tools use uppercase 'Crypto'
471
+ const validKeystore = {
472
+ address: "1234567890123456789012345678901234567890",
473
+ Crypto: {cipher: "aes-128-ctr", ciphertext: "test"},
474
+ version: 3,
475
+ };
476
+
477
+ const result = baseAction["isValidKeystoreFormat"](validKeystore);
478
+ expect(result).toBe(true);
479
+ });
480
+
481
+ test("should return false for keystore missing crypto field", () => {
482
+ const invalidKeystore = {
483
+ address: "1234567890123456789012345678901234567890",
484
+ version: 3,
485
+ };
486
+
487
+ const result = baseAction["isValidKeystoreFormat"](invalidKeystore);
488
+ expect(result).toBe(false);
489
+ });
490
+
491
+ test("should return false for keystore missing address", () => {
492
+ const invalidKeystore = {
493
+ crypto: {cipher: "aes-128-ctr"},
494
+ version: 3,
495
+ };
496
+
497
+ const result = baseAction["isValidKeystoreFormat"](invalidKeystore);
498
+ expect(result).toBe(false);
499
+ });
500
+
501
+ test("should return false for null or undefined keystore", () => {
502
+ expect(baseAction["isValidKeystoreFormat"](null)).toBe(false);
503
+ expect(baseAction["isValidKeystoreFormat"](undefined)).toBe(false);
504
+ });
505
+
506
+ describe("formatOutput", () => {
507
+ test("should return string as is", () => {
508
+ expect((baseAction as any).formatOutput("Hello")).toBe("Hello");
509
+ });
510
+
511
+ test("should format an object", () => {
512
+ const data = {key: "value", num: 42};
513
+ const result = (baseAction as any).formatOutput(data);
514
+ expect(result).toBe("{ key: 'value', num: 42 }");
515
+ });
516
+
517
+ test("should format an error object", () => {
518
+ const error = new Error("Test Error");
519
+ const result = (baseAction as any).formatOutput(error);
520
+ expect(result).toContain("Error: Test Error");
521
+ });
522
+
523
+ test("should format a Map object", () => {
524
+ const testMap = new Map([["key1", "value1"]]);
525
+ const result = (baseAction as any).formatOutput(testMap);
526
+ expect(result).toBe("Map(1) { 'key1' => 'value1' }");
527
+ });
528
+
529
+ test("should format a BigInt object", () => {
530
+ const bigIntValue = BigInt(9007199254740991);
531
+ const result = (baseAction as any).formatOutput(bigIntValue);
532
+ expect(result).toBe("9007199254740991n");
533
+ });
534
+ });
535
+ });