genlayer 0.32.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 (39) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +1 -1
  3. package/dist/index.js +1379 -221
  4. package/docs/delegator-guide.md +6 -6
  5. package/docs/validator-guide.md +49 -18
  6. package/package.json +2 -2
  7. package/src/commands/account/create.ts +10 -4
  8. package/src/commands/account/export.ts +106 -0
  9. package/src/commands/account/import.ts +85 -31
  10. package/src/commands/account/index.ts +77 -18
  11. package/src/commands/account/list.ts +34 -0
  12. package/src/commands/account/lock.ts +16 -7
  13. package/src/commands/account/remove.ts +30 -0
  14. package/src/commands/account/send.ts +14 -8
  15. package/src/commands/account/show.ts +22 -8
  16. package/src/commands/account/unlock.ts +20 -10
  17. package/src/commands/account/use.ts +21 -0
  18. package/src/commands/network/index.ts +18 -3
  19. package/src/commands/network/setNetwork.ts +38 -22
  20. package/src/commands/staking/StakingAction.ts +51 -19
  21. package/src/commands/staking/delegatorJoin.ts +2 -0
  22. package/src/commands/staking/index.ts +29 -2
  23. package/src/commands/staking/setIdentity.ts +5 -0
  24. package/src/commands/staking/stakingInfo.ts +29 -21
  25. package/src/commands/staking/wizard.ts +802 -0
  26. package/src/lib/actions/BaseAction.ts +71 -45
  27. package/src/lib/config/ConfigFileManager.ts +143 -0
  28. package/src/lib/config/KeychainManager.ts +23 -7
  29. package/tests/actions/create.test.ts +30 -10
  30. package/tests/actions/deploy.test.ts +7 -0
  31. package/tests/actions/lock.test.ts +28 -8
  32. package/tests/actions/unlock.test.ts +44 -26
  33. package/tests/commands/account.test.ts +43 -18
  34. package/tests/commands/network.test.ts +10 -10
  35. package/tests/commands/staking.test.ts +122 -0
  36. package/tests/libs/baseAction.test.ts +64 -41
  37. package/tests/libs/configFileManager.test.ts +8 -1
  38. package/tests/libs/keychainManager.test.ts +56 -16
  39. package/src/lib/interfaces/KeystoreData.ts +0 -5
@@ -44,16 +44,16 @@ export function resolveNetwork(stored: string | undefined): GenLayerChain {
44
44
  }
45
45
  import { ethers } from "ethers";
46
46
  import { writeFileSync, existsSync, readFileSync } from "fs";
47
- import { KeystoreData } from "../interfaces/KeystoreData";
48
47
 
49
48
  export class BaseAction extends ConfigFileManager {
50
- private static readonly DEFAULT_KEYSTORE_PATH = "./keypair.json";
49
+ private static readonly DEFAULT_ACCOUNT_NAME = "default";
51
50
  private static readonly MAX_PASSWORD_ATTEMPTS = 3;
52
- private static readonly MIN_PASSWORD_LENGTH = 8;
51
+ protected static readonly MIN_PASSWORD_LENGTH = 8;
53
52
 
54
53
  private spinner: Ora;
55
54
  private _genlayerClient: GenLayerClient<GenLayerChain> | null = null;
56
55
  protected keychainManager: KeychainManager;
56
+ protected accountOverride: string | null = null;
57
57
 
58
58
  constructor() {
59
59
  super();
@@ -61,28 +61,28 @@ export class BaseAction extends ConfigFileManager {
61
61
  this.keychainManager = new KeychainManager();
62
62
  }
63
63
 
64
- private async decryptKeystore(keystoreData: KeystoreData, attempt: number = 1): Promise<string> {
64
+ private async decryptKeystore(keystoreJson: string, attempt: number = 1): Promise<string> {
65
65
  try {
66
- const message = attempt === 1
67
- ? "Enter password to decrypt keystore:"
66
+ const message = attempt === 1
67
+ ? "Enter password to decrypt keystore:"
68
68
  : `Invalid password. Attempt ${attempt}/${BaseAction.MAX_PASSWORD_ATTEMPTS} - Enter password to decrypt keystore:`;
69
69
  const password = await this.promptPassword(message);
70
- const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password);
71
-
70
+ const wallet = await ethers.Wallet.fromEncryptedJson(keystoreJson, password);
71
+
72
72
  return wallet.privateKey;
73
73
  } catch (error) {
74
74
  if (attempt >= BaseAction.MAX_PASSWORD_ATTEMPTS) {
75
75
  this.failSpinner(`Maximum password attempts exceeded (${BaseAction.MAX_PASSWORD_ATTEMPTS}/${BaseAction.MAX_PASSWORD_ATTEMPTS}).`);
76
76
  }
77
- return await this.decryptKeystore(keystoreData, attempt + 1);
77
+ return await this.decryptKeystore(keystoreJson, attempt + 1);
78
78
  }
79
79
  }
80
80
 
81
- protected isValidKeystoreFormat(data: any): data is KeystoreData {
81
+ protected isValidKeystoreFormat(data: any): boolean {
82
+ // Standard web3 keystore format has 'crypto' (or 'Crypto') and 'address' fields
82
83
  return Boolean(
83
- data &&
84
- data.version === 1 &&
85
- typeof data.encrypted === "string" &&
84
+ data &&
85
+ (data.crypto || data.Crypto) &&
86
86
  typeof data.address === "string"
87
87
  );
88
88
  }
@@ -107,52 +107,80 @@ export class BaseAction extends ConfigFileManager {
107
107
  return this._genlayerClient;
108
108
  }
109
109
 
110
+ protected resolveAccountName(): string {
111
+ // Priority: explicit override > config active account > default
112
+ if (this.accountOverride) {
113
+ return this.accountOverride;
114
+ }
115
+ const activeAccount = this.getActiveAccount();
116
+ if (activeAccount) {
117
+ return activeAccount;
118
+ }
119
+ return BaseAction.DEFAULT_ACCOUNT_NAME;
120
+ }
121
+
110
122
  private async getAccount(readOnly: boolean = false): Promise<Account | Address> {
111
- let keypairPath = this.getConfigByKey("keyPairPath");
123
+ const accountName = this.resolveAccountName();
124
+ const keystorePath = this.getKeystorePath(accountName);
112
125
  let decryptedPrivateKey;
113
- let keystoreData;
126
+ let keystoreJson: string;
127
+ let keystoreData: any;
114
128
 
115
- if (!keypairPath || !existsSync(keypairPath)) {
116
- await this.confirmPrompt("Keypair file not found. Would you like to create a new keypair?");
117
- decryptedPrivateKey = await this.createKeypair(BaseAction.DEFAULT_KEYSTORE_PATH, false);
118
- keypairPath = this.getConfigByKey("keyPairPath")!;
129
+ if (!existsSync(keystorePath)) {
130
+ await this.confirmPrompt(`Account '${accountName}' not found. Would you like to create it?`);
131
+ decryptedPrivateKey = await this.createKeypairByName(accountName, false);
119
132
  }
120
133
 
121
- keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
134
+ keystoreJson = readFileSync(keystorePath, "utf-8");
135
+ keystoreData = JSON.parse(keystoreJson);
122
136
 
123
137
  if (!this.isValidKeystoreFormat(keystoreData)) {
124
138
  this.failSpinner("Invalid keystore format. Expected encrypted keystore file.", undefined, false);
125
- await this.confirmPrompt("Would you like to create a new keypair?");
126
- decryptedPrivateKey = await this.createKeypair(BaseAction.DEFAULT_KEYSTORE_PATH, true);
127
- keypairPath = this.getConfigByKey("keyPairPath")!;
128
- keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
139
+ await this.confirmPrompt(`Would you like to recreate account '${accountName}'?`);
140
+ decryptedPrivateKey = await this.createKeypairByName(accountName, true);
141
+ keystoreJson = readFileSync(keystorePath, "utf-8");
142
+ keystoreData = JSON.parse(keystoreJson);
129
143
  }
130
144
 
131
145
  if (readOnly) {
132
146
  return this.getAddress(keystoreData);
133
147
  }
134
-
148
+
135
149
  if (!decryptedPrivateKey) {
136
- const cachedKey = await this.keychainManager.getPrivateKey();
137
- decryptedPrivateKey = cachedKey ? cachedKey : await this.decryptKeystore(keystoreData);
150
+ const cachedKey = await this.keychainManager.getPrivateKey(accountName);
151
+ if (cachedKey) {
152
+ // Verify cached key matches keystore address
153
+ const tempAccount = createAccount(cachedKey as Hash);
154
+ const cachedAddress = tempAccount.address.toLowerCase();
155
+ const keystoreAddress = `0x${keystoreData.address.toLowerCase().replace(/^0x/, '')}`;
156
+ if (cachedAddress === keystoreAddress) {
157
+ decryptedPrivateKey = cachedKey;
158
+ } else {
159
+ // Cached key doesn't match keystore - invalidate it
160
+ await this.keychainManager.removePrivateKey(accountName);
161
+ decryptedPrivateKey = await this.decryptKeystore(keystoreJson);
162
+ }
163
+ } else {
164
+ decryptedPrivateKey = await this.decryptKeystore(keystoreJson);
165
+ }
138
166
  }
139
167
  return createAccount(decryptedPrivateKey as Hash);
140
168
  }
141
169
 
142
- private getAddress(keystoreData: KeystoreData): Address {
170
+ private getAddress(keystoreData: any): Address {
143
171
  return keystoreData.address as Address;
144
172
  }
145
173
 
146
- protected async createKeypair(outputPath: string, overwrite: boolean): Promise<string> {
147
- const finalOutputPath = this.getFilePath(outputPath);
174
+ protected async createKeypairByName(accountName: string, overwrite: boolean): Promise<string> {
175
+ const keystorePath = this.getKeystorePath(accountName);
148
176
  this.stopSpinner();
149
177
 
150
- if (existsSync(finalOutputPath) && !overwrite) {
151
- this.failSpinner(`The file at ${finalOutputPath} already exists. Use the '--overwrite' option to replace it.`);
178
+ if (existsSync(keystorePath) && !overwrite) {
179
+ this.failSpinner(`Account '${accountName}' already exists. Use '--overwrite' to replace it.`);
152
180
  }
153
181
 
154
182
  const wallet = ethers.Wallet.createRandom();
155
-
183
+
156
184
  const password = await this.promptPassword("Enter a password to encrypt your keystore (minimum 8 characters):");
157
185
  const confirmPassword = await this.promptPassword("Confirm password:");
158
186
 
@@ -164,19 +192,17 @@ export class BaseAction extends ConfigFileManager {
164
192
  this.failSpinner(`Password must be at least ${BaseAction.MIN_PASSWORD_LENGTH} characters long`);
165
193
  }
166
194
 
195
+ // Write standard web3 keystore format directly
167
196
  const encryptedJson = await wallet.encrypt(password);
168
-
169
- const keystoreData: KeystoreData = {
170
- version: 1,
171
- encrypted: encryptedJson,
172
- address: wallet.address,
173
- };
174
-
175
- writeFileSync(finalOutputPath, JSON.stringify(keystoreData, null, 2));
176
- this.writeConfig('keyPairPath', finalOutputPath);
177
-
178
- await this.keychainManager.removePrivateKey();
179
-
197
+ writeFileSync(keystorePath, encryptedJson);
198
+
199
+ // Set as active account if no active account exists
200
+ if (!this.getActiveAccount()) {
201
+ this.setActiveAccount(accountName);
202
+ }
203
+
204
+ await this.keychainManager.removePrivateKey(accountName);
205
+
180
206
  return wallet.privateKey;
181
207
  }
182
208
 
@@ -2,15 +2,26 @@ import path from "path";
2
2
  import os from "os";
3
3
  import fs from "fs";
4
4
 
5
+ export interface AccountInfo {
6
+ name: string;
7
+ address: string;
8
+ path: string;
9
+ }
10
+
5
11
  export class ConfigFileManager {
6
12
  private folderPath: string;
7
13
  private configFilePath: string;
14
+ private keystoresPath: string;
8
15
 
9
16
  constructor(baseFolder: string = ".genlayer/", configFileName: string = "genlayer-config.json") {
10
17
  this.folderPath = path.resolve(os.homedir(), baseFolder);
11
18
  this.configFilePath = path.resolve(this.folderPath, configFileName);
19
+ this.keystoresPath = path.resolve(this.folderPath, "keystores");
12
20
  this.ensureFolderExists();
21
+ this.ensureKeystoresDirExists();
13
22
  this.ensureConfigFileExists();
23
+ this.migrateOldConfig();
24
+ this.migrateKeystoreFormats();
14
25
  }
15
26
 
16
27
  private ensureFolderExists(): void {
@@ -19,12 +30,76 @@ export class ConfigFileManager {
19
30
  }
20
31
  }
21
32
 
33
+ private ensureKeystoresDirExists(): void {
34
+ if (!fs.existsSync(this.keystoresPath)) {
35
+ fs.mkdirSync(this.keystoresPath, { recursive: true });
36
+ }
37
+ }
38
+
22
39
  private ensureConfigFileExists(): void {
23
40
  if (!fs.existsSync(this.configFilePath)) {
24
41
  fs.writeFileSync(this.configFilePath, JSON.stringify({}, null, 2));
25
42
  }
26
43
  }
27
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
+
28
103
  getFolderPath(): string {
29
104
  return this.folderPath;
30
105
  }
@@ -48,4 +123,72 @@ export class ConfigFileManager {
48
123
  config[key] = value;
49
124
  fs.writeFileSync(this.configFilePath, JSON.stringify(config, null, 2));
50
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
+ }
51
194
  }
@@ -2,10 +2,13 @@ import {default as keytar} from 'keytar';
2
2
 
3
3
  export class KeychainManager {
4
4
  private static readonly SERVICE = 'genlayer-cli';
5
- private static readonly ACCOUNT = 'default-user';
6
5
 
7
6
  constructor() {}
8
7
 
8
+ private getKeychainAccount(accountName: string): string {
9
+ return `account:${accountName}`;
10
+ }
11
+
9
12
  async isKeychainAvailable(): Promise<boolean> {
10
13
  try {
11
14
  await keytar.findCredentials('test-service');
@@ -15,15 +18,28 @@ export class KeychainManager {
15
18
  }
16
19
  }
17
20
 
18
- async storePrivateKey(privateKey: string): Promise<void> {
19
- return await keytar.setPassword(KeychainManager.SERVICE, KeychainManager.ACCOUNT, privateKey);
21
+ async storePrivateKey(accountName: string, privateKey: string): Promise<void> {
22
+ return await keytar.setPassword(KeychainManager.SERVICE, this.getKeychainAccount(accountName), privateKey);
23
+ }
24
+
25
+ async getPrivateKey(accountName: string): Promise<string | null> {
26
+ return await keytar.getPassword(KeychainManager.SERVICE, this.getKeychainAccount(accountName));
27
+ }
28
+
29
+ async removePrivateKey(accountName: string): Promise<boolean> {
30
+ return await keytar.deletePassword(KeychainManager.SERVICE, this.getKeychainAccount(accountName));
20
31
  }
21
32
 
22
- async getPrivateKey(): Promise<string | null> {
23
- return await keytar.getPassword(KeychainManager.SERVICE, KeychainManager.ACCOUNT);
33
+ async listUnlockedAccounts(): Promise<string[]> {
34
+ const credentials = await keytar.findCredentials(KeychainManager.SERVICE);
35
+ return credentials
36
+ .map(c => c.account)
37
+ .filter(a => a.startsWith('account:'))
38
+ .map(a => a.replace('account:', ''));
24
39
  }
25
40
 
26
- async removePrivateKey(): Promise<boolean> {
27
- return await keytar.deletePassword(KeychainManager.SERVICE, KeychainManager.ACCOUNT);
41
+ async isAccountUnlocked(accountName: string): Promise<boolean> {
42
+ const key = await this.getPrivateKey(accountName);
43
+ return key !== null;
28
44
  }
29
45
  }
@@ -1,18 +1,31 @@
1
1
  import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
2
2
  import {CreateAccountAction} from "../../src/commands/account/create";
3
+ import {readFileSync, existsSync} from "fs";
4
+ import os from "os";
5
+
6
+ vi.mock("fs");
7
+ vi.mock("os");
3
8
 
4
9
  describe("CreateAccountAction", () => {
5
10
  let createAction: CreateAccountAction;
11
+ const mockKeystorePath = "/mocked/home/.genlayer/keystores/main.json";
6
12
 
7
13
  beforeEach(() => {
8
14
  vi.clearAllMocks();
15
+ // Setup mocks before creating the action (needed for constructor)
16
+ vi.mocked(os.homedir).mockReturnValue("/mocked/home");
17
+ vi.mocked(existsSync).mockReturnValue(true);
18
+ vi.mocked(readFileSync).mockReturnValue(JSON.stringify({activeAccount: "default"}));
19
+
9
20
  createAction = new CreateAccountAction();
10
21
 
11
22
  // Mock the BaseAction methods
12
23
  vi.spyOn(createAction as any, "startSpinner").mockImplementation(() => {});
13
24
  vi.spyOn(createAction as any, "succeedSpinner").mockImplementation(() => {});
14
25
  vi.spyOn(createAction as any, "failSpinner").mockImplementation(() => {});
15
- vi.spyOn(createAction as any, "createKeypair").mockResolvedValue("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
26
+ vi.spyOn(createAction as any, "createKeypairByName").mockResolvedValue("0x1234567890abcdef");
27
+ vi.spyOn(createAction as any, "setActiveAccount").mockImplementation(() => {});
28
+ vi.spyOn(createAction as any, "getKeystorePath").mockReturnValue(mockKeystorePath);
16
29
  });
17
30
 
18
31
  afterEach(() => {
@@ -20,26 +33,33 @@ describe("CreateAccountAction", () => {
20
33
  });
21
34
 
22
35
  test("successfully creates and saves an encrypted keystore", async () => {
23
- const options = {output: "keypair.json", overwrite: false};
36
+ const options = {name: "main", overwrite: false, setActive: true};
24
37
 
25
38
  await createAction.execute(options);
26
39
 
27
- expect(createAction["startSpinner"]).toHaveBeenCalledWith("Creating encrypted keystore...");
28
- expect(createAction["createKeypair"]).toHaveBeenCalledWith(
29
- options.output,
30
- options.overwrite
31
- );
40
+ expect(createAction["startSpinner"]).toHaveBeenCalledWith("Creating account 'main'...");
41
+ expect(createAction["createKeypairByName"]).toHaveBeenCalledWith("main", false);
42
+ expect(createAction["setActiveAccount"]).toHaveBeenCalledWith("main");
32
43
  expect(createAction["succeedSpinner"]).toHaveBeenCalledWith(
33
- "Account created and saved to: keypair.json",
44
+ `Account 'main' created at: ${mockKeystorePath}`,
34
45
  );
35
46
  });
36
47
 
37
48
  test("handles errors during keystore creation", async () => {
38
49
  const mockError = new Error("Mocked creation error");
39
- vi.spyOn(createAction as any, "createKeypair").mockRejectedValue(mockError);
50
+ vi.spyOn(createAction as any, "createKeypairByName").mockRejectedValue(mockError);
40
51
 
41
- await createAction.execute({output: "keypair.json", overwrite: true});
52
+ await createAction.execute({name: "main", overwrite: true});
42
53
 
43
54
  expect(createAction["failSpinner"]).toHaveBeenCalledWith("Failed to create account", mockError);
44
55
  });
56
+
57
+ test("skips setting active account when setActive is false", async () => {
58
+ const options = {name: "validator", overwrite: false, setActive: false};
59
+
60
+ await createAction.execute(options);
61
+
62
+ expect(createAction["setActiveAccount"]).not.toHaveBeenCalled();
63
+ expect(createAction["succeedSpinner"]).toHaveBeenCalled();
64
+ });
45
65
  });
@@ -1,11 +1,13 @@
1
1
  import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
2
2
  import fs from "fs";
3
+ import os from "os";
3
4
  import {createClient, createAccount} from "genlayer-js";
4
5
  import {DeployAction, DeployOptions} from "../../src/commands/contracts/deploy";
5
6
  import {buildSync} from "esbuild";
6
7
  import {pathToFileURL} from "url";
7
8
 
8
9
  vi.mock("fs");
10
+ vi.mock("os");
9
11
  vi.mock("genlayer-js");
10
12
  vi.mock("esbuild", () => ({
11
13
  buildSync: vi.fn(),
@@ -23,6 +25,11 @@ describe("DeployAction", () => {
23
25
 
24
26
  beforeEach(() => {
25
27
  vi.clearAllMocks();
28
+ // Setup mocks before creating the action (needed for constructor)
29
+ vi.mocked(os.homedir).mockReturnValue("/mocked/home");
30
+ vi.mocked(fs.existsSync).mockReturnValue(true);
31
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({activeAccount: "default"}));
32
+
26
33
  vi.mocked(createClient).mockReturnValue(mockClient as any);
27
34
  vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any);
28
35
  deployer = new DeployAction();
@@ -1,19 +1,30 @@
1
1
  import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
2
2
  import {LockAccountAction} from "../../src/commands/account/lock";
3
+ import {readFileSync, existsSync} from "fs";
4
+ import os from "os";
5
+
6
+ vi.mock("fs");
7
+ vi.mock("os");
3
8
 
4
9
  describe("LockAccountAction", () => {
5
10
  let lockAction: LockAccountAction;
6
11
 
7
12
  beforeEach(() => {
8
13
  vi.clearAllMocks();
14
+ // Setup mocks before creating the action (needed for constructor)
15
+ vi.mocked(os.homedir).mockReturnValue("/mocked/home");
16
+ vi.mocked(existsSync).mockReturnValue(true);
17
+ vi.mocked(readFileSync).mockReturnValue(JSON.stringify({activeAccount: "default"}));
18
+
9
19
  lockAction = new LockAccountAction();
10
-
20
+
11
21
  // Mock the BaseAction methods
12
22
  vi.spyOn(lockAction as any, "startSpinner").mockImplementation(() => {});
13
23
  vi.spyOn(lockAction as any, "setSpinnerText").mockImplementation(() => {});
14
24
  vi.spyOn(lockAction as any, "succeedSpinner").mockImplementation(() => {});
15
25
  vi.spyOn(lockAction as any, "failSpinner").mockImplementation(() => {});
16
-
26
+ vi.spyOn(lockAction as any, "resolveAccountName").mockReturnValue("default");
27
+
17
28
  // Mock keychainManager
18
29
  vi.spyOn(lockAction["keychainManager"], "isKeychainAvailable").mockResolvedValue(true);
19
30
  vi.spyOn(lockAction["keychainManager"], "getPrivateKey").mockResolvedValue("test-private-key");
@@ -29,11 +40,11 @@ describe("LockAccountAction", () => {
29
40
 
30
41
  expect(lockAction["startSpinner"]).toHaveBeenCalledWith("Checking keychain availability...");
31
42
  expect(lockAction["keychainManager"].isKeychainAvailable).toHaveBeenCalled();
32
- expect(lockAction["setSpinnerText"]).toHaveBeenCalledWith("Checking for cached private key...");
33
- expect(lockAction["keychainManager"].getPrivateKey).toHaveBeenCalled();
34
- expect(lockAction["setSpinnerText"]).toHaveBeenCalledWith("Removing private key from OS keychain...");
35
- expect(lockAction["keychainManager"].removePrivateKey).toHaveBeenCalled();
36
- expect(lockAction["succeedSpinner"]).toHaveBeenCalledWith("Account locked! Private key removed from OS keychain.");
43
+ expect(lockAction["setSpinnerText"]).toHaveBeenCalledWith("Checking for cached private key for 'default'...");
44
+ expect(lockAction["keychainManager"].getPrivateKey).toHaveBeenCalledWith("default");
45
+ expect(lockAction["setSpinnerText"]).toHaveBeenCalledWith("Removing private key for 'default' from OS keychain...");
46
+ expect(lockAction["keychainManager"].removePrivateKey).toHaveBeenCalledWith("default");
47
+ expect(lockAction["succeedSpinner"]).toHaveBeenCalledWith("Account 'default' locked! Private key removed from OS keychain.");
37
48
  });
38
49
 
39
50
  test("fails when keychain is not available", async () => {
@@ -51,7 +62,7 @@ describe("LockAccountAction", () => {
51
62
 
52
63
  await lockAction.execute();
53
64
 
54
- expect(lockAction["succeedSpinner"]).toHaveBeenCalledWith("Account is already locked.");
65
+ expect(lockAction["succeedSpinner"]).toHaveBeenCalledWith("Account 'default' is already locked.");
55
66
  expect(lockAction["keychainManager"].removePrivateKey).not.toHaveBeenCalled();
56
67
  });
57
68
 
@@ -63,4 +74,13 @@ describe("LockAccountAction", () => {
63
74
 
64
75
  expect(lockAction["failSpinner"]).toHaveBeenCalledWith("Failed to lock account.", mockError);
65
76
  });
77
+
78
+ test("uses account option when provided", async () => {
79
+ vi.spyOn(lockAction as any, "resolveAccountName").mockReturnValue("validator");
80
+
81
+ await lockAction.execute({account: "validator"});
82
+
83
+ expect(lockAction["accountOverride"]).toBe("validator");
84
+ expect(lockAction["setSpinnerText"]).toHaveBeenCalledWith("Checking for cached private key for 'validator'...");
85
+ });
66
86
  });