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.
- package/CHANGELOG.md +2 -0
- package/README.md +1 -1
- package/dist/index.js +1379 -221
- package/docs/delegator-guide.md +6 -6
- package/docs/validator-guide.md +49 -18
- package/package.json +2 -2
- package/src/commands/account/create.ts +10 -4
- package/src/commands/account/export.ts +106 -0
- package/src/commands/account/import.ts +85 -31
- package/src/commands/account/index.ts +77 -18
- package/src/commands/account/list.ts +34 -0
- package/src/commands/account/lock.ts +16 -7
- package/src/commands/account/remove.ts +30 -0
- package/src/commands/account/send.ts +14 -8
- package/src/commands/account/show.ts +22 -8
- package/src/commands/account/unlock.ts +20 -10
- package/src/commands/account/use.ts +21 -0
- package/src/commands/network/index.ts +18 -3
- package/src/commands/network/setNetwork.ts +38 -22
- package/src/commands/staking/StakingAction.ts +51 -19
- package/src/commands/staking/delegatorJoin.ts +2 -0
- package/src/commands/staking/index.ts +29 -2
- package/src/commands/staking/setIdentity.ts +5 -0
- package/src/commands/staking/stakingInfo.ts +29 -21
- package/src/commands/staking/wizard.ts +802 -0
- package/src/lib/actions/BaseAction.ts +71 -45
- package/src/lib/config/ConfigFileManager.ts +143 -0
- package/src/lib/config/KeychainManager.ts +23 -7
- package/tests/actions/create.test.ts +30 -10
- package/tests/actions/deploy.test.ts +7 -0
- package/tests/actions/lock.test.ts +28 -8
- package/tests/actions/unlock.test.ts +44 -26
- package/tests/commands/account.test.ts +43 -18
- package/tests/commands/network.test.ts +10 -10
- package/tests/commands/staking.test.ts +122 -0
- package/tests/libs/baseAction.test.ts +64 -41
- package/tests/libs/configFileManager.test.ts +8 -1
- package/tests/libs/keychainManager.test.ts +56 -16
- 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
|
|
49
|
+
private static readonly DEFAULT_ACCOUNT_NAME = "default";
|
|
51
50
|
private static readonly MAX_PASSWORD_ATTEMPTS = 3;
|
|
52
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
77
|
+
return await this.decryptKeystore(keystoreJson, attempt + 1);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
protected isValidKeystoreFormat(data: any):
|
|
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.
|
|
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
|
-
|
|
123
|
+
const accountName = this.resolveAccountName();
|
|
124
|
+
const keystorePath = this.getKeystorePath(accountName);
|
|
112
125
|
let decryptedPrivateKey;
|
|
113
|
-
let
|
|
126
|
+
let keystoreJson: string;
|
|
127
|
+
let keystoreData: any;
|
|
114
128
|
|
|
115
|
-
if (!
|
|
116
|
-
await this.confirmPrompt(
|
|
117
|
-
decryptedPrivateKey = await this.
|
|
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
|
-
|
|
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(
|
|
126
|
-
decryptedPrivateKey = await this.
|
|
127
|
-
|
|
128
|
-
keystoreData = JSON.parse(
|
|
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
|
-
|
|
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:
|
|
170
|
+
private getAddress(keystoreData: any): Address {
|
|
143
171
|
return keystoreData.address as Address;
|
|
144
172
|
}
|
|
145
173
|
|
|
146
|
-
protected async
|
|
147
|
-
const
|
|
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(
|
|
151
|
-
this.failSpinner(`
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
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
|
|
27
|
-
|
|
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, "
|
|
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 = {
|
|
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
|
|
28
|
-
expect(createAction["
|
|
29
|
-
|
|
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
|
-
|
|
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, "
|
|
50
|
+
vi.spyOn(createAction as any, "createKeypairByName").mockRejectedValue(mockError);
|
|
40
51
|
|
|
41
|
-
await createAction.execute({
|
|
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).
|
|
34
|
-
expect(lockAction["setSpinnerText"]).toHaveBeenCalledWith("Removing private key from OS keychain...");
|
|
35
|
-
expect(lockAction["keychainManager"].removePrivateKey).
|
|
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
|
});
|