genlayer 0.19.0 → 0.20.0
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 +6 -0
- package/dist/index.js +91 -55
- package/package.json +1 -1
- package/src/commands/keygen/create.ts +6 -5
- package/src/commands/keygen/index.ts +4 -4
- package/src/lib/actions/BaseAction.ts +108 -17
- package/src/lib/interfaces/KeystoreData.ts +5 -0
- package/tests/actions/create.test.ts +15 -37
- package/tests/libs/baseAction.test.ts +229 -24
- package/src/lib/accounts/KeypairManager.ts +0 -43
- package/tests/libs/accounts/KeypairManager.test.ts +0 -110
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.0 (2025-07-09)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* use keystore to store private keys ([#235](https://github.com/yeagerai/genlayer-cli/issues/235)) ([f18f2af](https://github.com/yeagerai/genlayer-cli/commit/f18f2afbc35c2dd988908d51ffce6b0523cfee81))
|
|
8
|
+
|
|
3
9
|
## 0.19.0 (2025-07-09)
|
|
4
10
|
|
|
5
11
|
### Features
|
package/dist/index.js
CHANGED
|
@@ -17723,7 +17723,7 @@ var require_semver2 = __commonJS({
|
|
|
17723
17723
|
import { program } from "commander";
|
|
17724
17724
|
|
|
17725
17725
|
// package.json
|
|
17726
|
-
var version = "0.
|
|
17726
|
+
var version = "0.20.0";
|
|
17727
17727
|
var package_default = {
|
|
17728
17728
|
name: "genlayer",
|
|
17729
17729
|
version,
|
|
@@ -20660,39 +20660,6 @@ function ora(options) {
|
|
|
20660
20660
|
import inquirer from "inquirer";
|
|
20661
20661
|
import { inspect } from "util";
|
|
20662
20662
|
|
|
20663
|
-
// src/lib/accounts/KeypairManager.ts
|
|
20664
|
-
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
20665
|
-
import { ethers } from "ethers";
|
|
20666
|
-
var KeypairManager = class extends ConfigFileManager {
|
|
20667
|
-
constructor() {
|
|
20668
|
-
super();
|
|
20669
|
-
}
|
|
20670
|
-
getPrivateKey() {
|
|
20671
|
-
const keypairPath = this.getConfigByKey("keyPairPath");
|
|
20672
|
-
if (!keypairPath || !existsSync(keypairPath)) {
|
|
20673
|
-
return "";
|
|
20674
|
-
}
|
|
20675
|
-
const keypairData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
20676
|
-
if (!keypairData.privateKey) {
|
|
20677
|
-
return "";
|
|
20678
|
-
}
|
|
20679
|
-
return keypairData.privateKey;
|
|
20680
|
-
}
|
|
20681
|
-
createKeypair(outputPath = "./keypair.json", overwrite = false) {
|
|
20682
|
-
const finalOutputPath = this.getFilePath(outputPath);
|
|
20683
|
-
if (existsSync(finalOutputPath) && !overwrite) {
|
|
20684
|
-
throw new Error(`The file at ${finalOutputPath} already exists. Use the '--overwrite' option to replace it.`);
|
|
20685
|
-
}
|
|
20686
|
-
const wallet = ethers.Wallet.createRandom();
|
|
20687
|
-
const keypairData = {
|
|
20688
|
-
address: wallet.address,
|
|
20689
|
-
privateKey: wallet.privateKey
|
|
20690
|
-
};
|
|
20691
|
-
writeFileSync(finalOutputPath, JSON.stringify(keypairData, null, 2));
|
|
20692
|
-
this.writeConfig("keyPairPath", finalOutputPath);
|
|
20693
|
-
}
|
|
20694
|
-
};
|
|
20695
|
-
|
|
20696
20663
|
// node_modules/genlayer-js/dist/chunk-MLKGABMK.js
|
|
20697
20664
|
var __defProp2 = Object.defineProperty;
|
|
20698
20665
|
var __export2 = (target, all) => {
|
|
@@ -40935,14 +40902,39 @@ var createAccount = (accountPrivateKey) => {
|
|
|
40935
40902
|
};
|
|
40936
40903
|
|
|
40937
40904
|
// src/lib/actions/BaseAction.ts
|
|
40905
|
+
import { ethers } from "ethers";
|
|
40906
|
+
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
40938
40907
|
var BaseAction = class extends ConfigFileManager {
|
|
40939
40908
|
constructor() {
|
|
40940
40909
|
super();
|
|
40941
|
-
__publicField(this, "keypairManager");
|
|
40942
40910
|
__publicField(this, "spinner");
|
|
40943
40911
|
__publicField(this, "_genlayerClient", null);
|
|
40944
40912
|
this.spinner = ora({ text: "", spinner: "dots" });
|
|
40945
|
-
|
|
40913
|
+
}
|
|
40914
|
+
async decryptKeystore(keystoreData, attempt = 1) {
|
|
40915
|
+
try {
|
|
40916
|
+
const message = attempt === 1 ? "Enter password to decrypt keystore:" : `Invalid password. Attempt ${attempt}/3 - Enter password to decrypt keystore:`;
|
|
40917
|
+
const password = await this.promptPassword(message);
|
|
40918
|
+
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password);
|
|
40919
|
+
return wallet.privateKey;
|
|
40920
|
+
} catch (error) {
|
|
40921
|
+
if (attempt >= 3) {
|
|
40922
|
+
this.failSpinner("Maximum password attempts exceeded (3/3).");
|
|
40923
|
+
process.exit(1);
|
|
40924
|
+
}
|
|
40925
|
+
return await this.decryptKeystore(keystoreData, attempt + 1);
|
|
40926
|
+
}
|
|
40927
|
+
}
|
|
40928
|
+
isValidKeystoreFormat(data) {
|
|
40929
|
+
return Boolean(
|
|
40930
|
+
data && data.version === 1 && typeof data.encrypted === "string" && typeof data.address === "string"
|
|
40931
|
+
);
|
|
40932
|
+
}
|
|
40933
|
+
formatOutput(data) {
|
|
40934
|
+
if (typeof data === "string") {
|
|
40935
|
+
return data;
|
|
40936
|
+
}
|
|
40937
|
+
return inspect(data, { depth: null, colors: false });
|
|
40946
40938
|
}
|
|
40947
40939
|
async getClient(rpcUrl) {
|
|
40948
40940
|
if (!this._genlayerClient) {
|
|
@@ -40957,13 +40949,63 @@ var BaseAction = class extends ConfigFileManager {
|
|
|
40957
40949
|
return this._genlayerClient;
|
|
40958
40950
|
}
|
|
40959
40951
|
async getPrivateKey() {
|
|
40960
|
-
const
|
|
40961
|
-
if (
|
|
40962
|
-
|
|
40952
|
+
const keypairPath = this.getConfigByKey("keyPairPath");
|
|
40953
|
+
if (!keypairPath || !existsSync(keypairPath)) {
|
|
40954
|
+
await this.confirmPrompt("Keypair file not found. Would you like to create a new keypair?");
|
|
40955
|
+
return await this.createKeypair("./keypair.json", false);
|
|
40963
40956
|
}
|
|
40964
|
-
|
|
40965
|
-
this.
|
|
40966
|
-
|
|
40957
|
+
const keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
40958
|
+
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
40959
|
+
this.failSpinner("Invalid keystore format. Expected encrypted keystore file.");
|
|
40960
|
+
await this.confirmPrompt("Would you like to create a new keypair?");
|
|
40961
|
+
return await this.createKeypair("./keypair.json", true);
|
|
40962
|
+
}
|
|
40963
|
+
return await this.decryptKeystore(keystoreData);
|
|
40964
|
+
}
|
|
40965
|
+
async createKeypair(outputPath, overwrite) {
|
|
40966
|
+
const finalOutputPath = this.getFilePath(outputPath);
|
|
40967
|
+
this.stopSpinner();
|
|
40968
|
+
if (existsSync(finalOutputPath) && !overwrite) {
|
|
40969
|
+
this.failSpinner(`The file at ${finalOutputPath} already exists. Use the '--overwrite' option to replace it.`);
|
|
40970
|
+
process.exit(1);
|
|
40971
|
+
}
|
|
40972
|
+
const wallet = ethers.Wallet.createRandom();
|
|
40973
|
+
const password = await this.promptPassword("Enter a password to encrypt your keystore (minimum 8 characters):");
|
|
40974
|
+
const confirmPassword = await this.promptPassword("Confirm password:");
|
|
40975
|
+
if (password !== confirmPassword) {
|
|
40976
|
+
this.failSpinner("Passwords do not match");
|
|
40977
|
+
process.exit(1);
|
|
40978
|
+
}
|
|
40979
|
+
if (password.length < 8) {
|
|
40980
|
+
this.failSpinner("Password must be at least 8 characters long");
|
|
40981
|
+
process.exit(1);
|
|
40982
|
+
}
|
|
40983
|
+
const encryptedJson = await wallet.encrypt(password);
|
|
40984
|
+
const keystoreData = {
|
|
40985
|
+
version: 1,
|
|
40986
|
+
encrypted: encryptedJson,
|
|
40987
|
+
address: wallet.address
|
|
40988
|
+
};
|
|
40989
|
+
writeFileSync(finalOutputPath, JSON.stringify(keystoreData, null, 2));
|
|
40990
|
+
this.writeConfig("keyPairPath", finalOutputPath);
|
|
40991
|
+
return wallet.privateKey;
|
|
40992
|
+
}
|
|
40993
|
+
async promptPassword(message) {
|
|
40994
|
+
const answer = await inquirer.prompt([
|
|
40995
|
+
{
|
|
40996
|
+
type: "password",
|
|
40997
|
+
name: "password",
|
|
40998
|
+
message: source_default.yellow(message),
|
|
40999
|
+
mask: "*",
|
|
41000
|
+
validate: (input) => {
|
|
41001
|
+
if (!input) {
|
|
41002
|
+
return "Password cannot be empty";
|
|
41003
|
+
}
|
|
41004
|
+
return true;
|
|
41005
|
+
}
|
|
41006
|
+
}
|
|
41007
|
+
]);
|
|
41008
|
+
return answer.password;
|
|
40967
41009
|
}
|
|
40968
41010
|
async confirmPrompt(message) {
|
|
40969
41011
|
const answer = await inquirer.prompt([
|
|
@@ -40979,12 +41021,6 @@ var BaseAction = class extends ConfigFileManager {
|
|
|
40979
41021
|
process.exit(0);
|
|
40980
41022
|
}
|
|
40981
41023
|
}
|
|
40982
|
-
formatOutput(data) {
|
|
40983
|
-
if (typeof data === "string") {
|
|
40984
|
-
return data;
|
|
40985
|
-
}
|
|
40986
|
-
return inspect(data, { depth: null, colors: false });
|
|
40987
|
-
}
|
|
40988
41024
|
log(message, data) {
|
|
40989
41025
|
console.log(source_default.white(`
|
|
40990
41026
|
${message}`));
|
|
@@ -42123,13 +42159,13 @@ var KeypairCreator = class extends BaseAction {
|
|
|
42123
42159
|
constructor() {
|
|
42124
42160
|
super();
|
|
42125
42161
|
}
|
|
42126
|
-
createKeypairAction(options) {
|
|
42162
|
+
async createKeypairAction(options) {
|
|
42127
42163
|
try {
|
|
42128
|
-
this.startSpinner(`Creating
|
|
42129
|
-
this.
|
|
42130
|
-
this.succeedSpinner(`
|
|
42164
|
+
this.startSpinner(`Creating encrypted keystore...`);
|
|
42165
|
+
await this.createKeypair(options.output, options.overwrite);
|
|
42166
|
+
this.succeedSpinner(`Encrypted keystore successfully created and saved to: ${options.output}`);
|
|
42131
42167
|
} catch (error) {
|
|
42132
|
-
this.failSpinner("Failed to generate
|
|
42168
|
+
this.failSpinner("Failed to generate keystore", error);
|
|
42133
42169
|
}
|
|
42134
42170
|
}
|
|
42135
42171
|
};
|
|
@@ -42137,9 +42173,9 @@ var KeypairCreator = class extends BaseAction {
|
|
|
42137
42173
|
// src/commands/keygen/index.ts
|
|
42138
42174
|
function initializeKeygenCommands(program2) {
|
|
42139
42175
|
const keygenCommand = program2.command("keygen").description("Manage keypair generation");
|
|
42140
|
-
keygenCommand.command("create").description("Generates a new
|
|
42176
|
+
keygenCommand.command("create").description("Generates a new encrypted keystore and saves it to a file").option("--output <path>", "Path to save the keystore", "./keypair.json").option("--overwrite", "Overwrite the existing file if it already exists", false).action(async (options) => {
|
|
42141
42177
|
const keypairCreator = new KeypairCreator();
|
|
42142
|
-
keypairCreator.createKeypairAction(options);
|
|
42178
|
+
await keypairCreator.createKeypairAction(options);
|
|
42143
42179
|
});
|
|
42144
42180
|
return program2;
|
|
42145
42181
|
}
|
package/package.json
CHANGED
|
@@ -10,13 +10,14 @@ export class KeypairCreator extends BaseAction {
|
|
|
10
10
|
super();
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
createKeypairAction(options: CreateKeypairOptions) {
|
|
13
|
+
async createKeypairAction(options: CreateKeypairOptions) {
|
|
14
14
|
try {
|
|
15
|
-
this.startSpinner(`Creating
|
|
16
|
-
this.
|
|
17
|
-
|
|
15
|
+
this.startSpinner(`Creating encrypted keystore...`);
|
|
16
|
+
await this.createKeypair(options.output, options.overwrite);
|
|
17
|
+
|
|
18
|
+
this.succeedSpinner(`Encrypted keystore successfully created and saved to: ${options.output}`);
|
|
18
19
|
} catch (error) {
|
|
19
|
-
this.failSpinner("Failed to generate
|
|
20
|
+
this.failSpinner("Failed to generate keystore", error);
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
}
|
|
@@ -9,12 +9,12 @@ export function initializeKeygenCommands(program: Command) {
|
|
|
9
9
|
|
|
10
10
|
keygenCommand
|
|
11
11
|
.command("create")
|
|
12
|
-
.description("Generates a new
|
|
13
|
-
.option("--output <path>", "Path to save the
|
|
12
|
+
.description("Generates a new encrypted keystore and saves it to a file")
|
|
13
|
+
.option("--output <path>", "Path to save the keystore", "./keypair.json")
|
|
14
14
|
.option("--overwrite", "Overwrite the existing file if it already exists", false)
|
|
15
|
-
.action((options: CreateKeypairOptions) => {
|
|
15
|
+
.action(async (options: CreateKeypairOptions) => {
|
|
16
16
|
const keypairCreator = new KeypairCreator();
|
|
17
|
-
keypairCreator.createKeypairAction(options);
|
|
17
|
+
await keypairCreator.createKeypairAction(options);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
return program;
|
|
@@ -3,20 +3,53 @@ import ora, {Ora} from "ora";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import inquirer from "inquirer";
|
|
5
5
|
import { inspect } from "util";
|
|
6
|
-
import {KeypairManager} from "../accounts/KeypairManager";
|
|
7
6
|
import {createClient, createAccount} from "genlayer-js";
|
|
8
7
|
import {localnet} from "genlayer-js/chains";
|
|
9
8
|
import type {GenLayerClient, GenLayerChain} from "genlayer-js/types";
|
|
9
|
+
import { ethers } from "ethers";
|
|
10
|
+
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
11
|
+
import { KeystoreData } from "../interfaces/KeystoreData";
|
|
10
12
|
|
|
11
13
|
export class BaseAction extends ConfigFileManager {
|
|
12
|
-
protected keypairManager: KeypairManager;
|
|
13
14
|
private spinner: Ora;
|
|
14
15
|
private _genlayerClient: GenLayerClient<GenLayerChain> | null = null;
|
|
15
16
|
|
|
16
17
|
constructor() {
|
|
17
18
|
super();
|
|
18
19
|
this.spinner = ora({text: "", spinner: "dots"});
|
|
19
|
-
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private async decryptKeystore(keystoreData: KeystoreData, attempt: number = 1): Promise<string> {
|
|
23
|
+
try {
|
|
24
|
+
const message = attempt === 1
|
|
25
|
+
? "Enter password to decrypt keystore:"
|
|
26
|
+
: `Invalid password. Attempt ${attempt}/3 - Enter password to decrypt keystore:`;
|
|
27
|
+
const password = await this.promptPassword(message);
|
|
28
|
+
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password);
|
|
29
|
+
return wallet.privateKey;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (attempt >= 3) {
|
|
32
|
+
this.failSpinner("Maximum password attempts exceeded (3/3).");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
return await this.decryptKeystore(keystoreData, attempt + 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private isValidKeystoreFormat(data: any): data is KeystoreData {
|
|
40
|
+
return Boolean(
|
|
41
|
+
data &&
|
|
42
|
+
data.version === 1 &&
|
|
43
|
+
typeof data.encrypted === "string" &&
|
|
44
|
+
typeof data.address === "string"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private formatOutput(data: any): string {
|
|
49
|
+
if (typeof data === "string") {
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
return inspect(data, { depth: null, colors: false });
|
|
20
53
|
}
|
|
21
54
|
|
|
22
55
|
protected async getClient(rpcUrl?: string): Promise<GenLayerClient<GenLayerChain>> {
|
|
@@ -32,14 +65,79 @@ export class BaseAction extends ConfigFileManager {
|
|
|
32
65
|
return this._genlayerClient;
|
|
33
66
|
}
|
|
34
67
|
|
|
35
|
-
protected async getPrivateKey() {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
68
|
+
protected async getPrivateKey(): Promise<string> {
|
|
69
|
+
const keypairPath = this.getConfigByKey("keyPairPath");
|
|
70
|
+
|
|
71
|
+
if (!keypairPath || !existsSync(keypairPath)) {
|
|
72
|
+
await this.confirmPrompt("Keypair file not found. Would you like to create a new keypair?");
|
|
73
|
+
return await this.createKeypair("./keypair.json", false);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
77
|
+
|
|
78
|
+
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
79
|
+
this.failSpinner("Invalid keystore format. Expected encrypted keystore file.");
|
|
80
|
+
await this.confirmPrompt("Would you like to create a new keypair?");
|
|
81
|
+
return await this.createKeypair("./keypair.json", true);
|
|
39
82
|
}
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
83
|
+
|
|
84
|
+
return await this.decryptKeystore(keystoreData);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected async createKeypair(outputPath: string, overwrite: boolean): Promise<string> {
|
|
88
|
+
const finalOutputPath = this.getFilePath(outputPath);
|
|
89
|
+
this.stopSpinner();
|
|
90
|
+
|
|
91
|
+
if (existsSync(finalOutputPath) && !overwrite) {
|
|
92
|
+
this.failSpinner(`The file at ${finalOutputPath} already exists. Use the '--overwrite' option to replace it.`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const wallet = ethers.Wallet.createRandom();
|
|
97
|
+
|
|
98
|
+
const password = await this.promptPassword("Enter a password to encrypt your keystore (minimum 8 characters):");
|
|
99
|
+
const confirmPassword = await this.promptPassword("Confirm password:");
|
|
100
|
+
|
|
101
|
+
if (password !== confirmPassword) {
|
|
102
|
+
this.failSpinner("Passwords do not match");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (password.length < 8) {
|
|
107
|
+
this.failSpinner("Password must be at least 8 characters long");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const encryptedJson = await wallet.encrypt(password);
|
|
112
|
+
|
|
113
|
+
const keystoreData: KeystoreData = {
|
|
114
|
+
version: 1,
|
|
115
|
+
encrypted: encryptedJson,
|
|
116
|
+
address: wallet.address,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
writeFileSync(finalOutputPath, JSON.stringify(keystoreData, null, 2));
|
|
120
|
+
this.writeConfig('keyPairPath', finalOutputPath);
|
|
121
|
+
|
|
122
|
+
return wallet.privateKey;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
protected async promptPassword(message: string): Promise<string> {
|
|
126
|
+
const answer = await inquirer.prompt([
|
|
127
|
+
{
|
|
128
|
+
type: "password",
|
|
129
|
+
name: "password",
|
|
130
|
+
message: chalk.yellow(message),
|
|
131
|
+
mask: "*",
|
|
132
|
+
validate: (input: string) => {
|
|
133
|
+
if (!input) {
|
|
134
|
+
return "Password cannot be empty";
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
return answer.password;
|
|
43
141
|
}
|
|
44
142
|
|
|
45
143
|
protected async confirmPrompt(message: string): Promise<void> {
|
|
@@ -58,13 +156,6 @@ export class BaseAction extends ConfigFileManager {
|
|
|
58
156
|
}
|
|
59
157
|
}
|
|
60
158
|
|
|
61
|
-
private formatOutput(data: any): string {
|
|
62
|
-
if (typeof data === "string") {
|
|
63
|
-
return data;
|
|
64
|
-
}
|
|
65
|
-
return inspect(data, { depth: null, colors: false });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
159
|
protected log(message: string, data?: any): void {
|
|
69
160
|
console.log(chalk.white(`\n${message}`));
|
|
70
161
|
if (data !== undefined) console.log(this.formatOutput(data));
|
|
@@ -1,67 +1,45 @@
|
|
|
1
1
|
import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
|
|
2
2
|
import {KeypairCreator} from "../../src/commands/keygen/create";
|
|
3
|
-
import {writeFileSync, existsSync} from "fs";
|
|
4
|
-
import {ethers} from "ethers";
|
|
5
|
-
|
|
6
|
-
vi.mock("fs");
|
|
7
|
-
|
|
8
|
-
vi.mock("ethers", () => ({
|
|
9
|
-
ethers: {
|
|
10
|
-
Wallet: {
|
|
11
|
-
createRandom: vi.fn(),
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
}));
|
|
15
3
|
|
|
16
4
|
describe("KeypairCreator", () => {
|
|
17
5
|
let keypairCreator: KeypairCreator;
|
|
18
6
|
|
|
19
|
-
const mockWallet: any = {
|
|
20
|
-
address: "0xMockedAddress",
|
|
21
|
-
privateKey: "0xMockedPrivateKey",
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const mockKeypairManager = {
|
|
25
|
-
createKeypair: vi.fn(),
|
|
26
|
-
};
|
|
27
|
-
|
|
28
7
|
beforeEach(() => {
|
|
29
8
|
vi.clearAllMocks();
|
|
30
9
|
keypairCreator = new KeypairCreator();
|
|
31
|
-
|
|
10
|
+
|
|
11
|
+
// Mock the BaseAction methods
|
|
32
12
|
vi.spyOn(keypairCreator as any, "startSpinner").mockImplementation(() => {});
|
|
33
13
|
vi.spyOn(keypairCreator as any, "succeedSpinner").mockImplementation(() => {});
|
|
34
14
|
vi.spyOn(keypairCreator as any, "failSpinner").mockImplementation(() => {});
|
|
35
|
-
vi.spyOn(keypairCreator as any, "
|
|
36
|
-
vi.spyOn(keypairCreator as any, "getFilePath").mockImplementation(fileName => `/mocked/path/${fileName}`);
|
|
37
|
-
vi.mocked(ethers.Wallet.createRandom).mockReturnValue(mockWallet);
|
|
15
|
+
vi.spyOn(keypairCreator as any, "createKeypair").mockResolvedValue("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
|
|
38
16
|
});
|
|
39
17
|
|
|
40
18
|
afterEach(() => {
|
|
41
19
|
vi.restoreAllMocks();
|
|
42
20
|
});
|
|
43
21
|
|
|
44
|
-
test("successfully creates and saves
|
|
45
|
-
vi.mocked(existsSync).mockReturnValue(false);
|
|
22
|
+
test("successfully creates and saves an encrypted keystore", async () => {
|
|
46
23
|
const options = {output: "keypair.json", overwrite: false};
|
|
47
24
|
|
|
48
|
-
keypairCreator.createKeypairAction(options);
|
|
25
|
+
await keypairCreator.createKeypairAction(options);
|
|
49
26
|
|
|
50
|
-
expect(keypairCreator["startSpinner"]).toHaveBeenCalledWith("Creating
|
|
51
|
-
expect(
|
|
27
|
+
expect(keypairCreator["startSpinner"]).toHaveBeenCalledWith("Creating encrypted keystore...");
|
|
28
|
+
expect(keypairCreator["createKeypair"]).toHaveBeenCalledWith(
|
|
29
|
+
options.output,
|
|
30
|
+
options.overwrite
|
|
31
|
+
);
|
|
52
32
|
expect(keypairCreator["succeedSpinner"]).toHaveBeenCalledWith(
|
|
53
|
-
"
|
|
33
|
+
"Encrypted keystore successfully created and saved to: keypair.json",
|
|
54
34
|
);
|
|
55
35
|
});
|
|
56
36
|
|
|
57
|
-
test("handles errors during
|
|
37
|
+
test("handles errors during keystore creation", async () => {
|
|
58
38
|
const mockError = new Error("Mocked creation error");
|
|
59
|
-
|
|
60
|
-
throw mockError;
|
|
61
|
-
});
|
|
39
|
+
vi.spyOn(keypairCreator as any, "createKeypair").mockRejectedValue(mockError);
|
|
62
40
|
|
|
63
|
-
keypairCreator.createKeypairAction({output: "keypair.json", overwrite: true});
|
|
41
|
+
await keypairCreator.createKeypairAction({output: "keypair.json", overwrite: true});
|
|
64
42
|
|
|
65
|
-
expect(keypairCreator["failSpinner"]).toHaveBeenCalledWith("Failed to generate
|
|
43
|
+
expect(keypairCreator["failSpinner"]).toHaveBeenCalledWith("Failed to generate keystore", mockError);
|
|
66
44
|
});
|
|
67
45
|
});
|
|
@@ -4,20 +4,40 @@ import inquirer from "inquirer";
|
|
|
4
4
|
import ora, {Ora} from "ora";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import {inspect} from "util";
|
|
7
|
+
import { ethers } from "ethers";
|
|
8
|
+
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
7
9
|
|
|
8
10
|
vi.mock("inquirer");
|
|
9
11
|
vi.mock("ora");
|
|
12
|
+
vi.mock("fs");
|
|
13
|
+
vi.mock("ethers");
|
|
10
14
|
|
|
11
15
|
describe("BaseAction", () => {
|
|
12
16
|
let baseAction: BaseAction;
|
|
13
17
|
let mockSpinner: Ora;
|
|
14
18
|
let consoleSpy: any;
|
|
15
19
|
let consoleErrorSpy: any;
|
|
20
|
+
let processExitSpy: any;
|
|
21
|
+
|
|
22
|
+
const mockKeystoreData = {
|
|
23
|
+
version: 1,
|
|
24
|
+
encrypted: '{"address":"test","crypto":{"cipher":"aes-128-ctr"}}',
|
|
25
|
+
address: "0x1234567890123456789012345678901234567890",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mockWallet = {
|
|
29
|
+
privateKey: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
|
30
|
+
address: "0x1234567890123456789012345678901234567890",
|
|
31
|
+
encrypt: vi.fn().mockResolvedValue('{"address":"test","crypto":{"cipher":"aes-128-ctr"}}'),
|
|
32
|
+
};
|
|
16
33
|
|
|
17
34
|
beforeEach(() => {
|
|
18
35
|
vi.clearAllMocks();
|
|
19
36
|
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
20
37
|
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
38
|
+
processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
39
|
+
throw new Error("process exited");
|
|
40
|
+
});
|
|
21
41
|
mockSpinner = {
|
|
22
42
|
start: vi.fn(),
|
|
23
43
|
stop: vi.fn(),
|
|
@@ -27,8 +47,21 @@ describe("BaseAction", () => {
|
|
|
27
47
|
} as unknown as Ora;
|
|
28
48
|
|
|
29
49
|
(ora as unknown as Mock).mockReturnValue(mockSpinner);
|
|
50
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
51
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockKeystoreData));
|
|
52
|
+
vi.mocked(writeFileSync).mockImplementation(() => {});
|
|
53
|
+
|
|
54
|
+
// Mock ethers
|
|
55
|
+
vi.mocked(ethers.Wallet.createRandom).mockReturnValue(mockWallet as any);
|
|
56
|
+
vi.mocked(ethers.Wallet.fromEncryptedJson).mockResolvedValue(mockWallet as any);
|
|
30
57
|
|
|
31
58
|
baseAction = new BaseAction();
|
|
59
|
+
|
|
60
|
+
// Mock config methods
|
|
61
|
+
vi.spyOn(baseAction as any, "getConfigByKey").mockReturnValue("./test-keypair.json");
|
|
62
|
+
vi.spyOn(baseAction as any, "getFilePath").mockImplementation(() => "./test-keypair.json");
|
|
63
|
+
vi.spyOn(baseAction as any, "writeConfig").mockImplementation(() => {});
|
|
64
|
+
vi.spyOn(baseAction as any, "getConfig").mockReturnValue({});
|
|
32
65
|
});
|
|
33
66
|
|
|
34
67
|
afterEach(() => {
|
|
@@ -162,44 +195,216 @@ describe("BaseAction", () => {
|
|
|
162
195
|
expect((baseAction as any).formatOutput(true)).toBe("true");
|
|
163
196
|
});
|
|
164
197
|
|
|
165
|
-
|
|
198
|
+
test("should prompt for password successfully", async () => {
|
|
199
|
+
const mockPassword = "test-password";
|
|
200
|
+
vi.mocked(inquirer.prompt).mockResolvedValue({password: mockPassword});
|
|
201
|
+
|
|
202
|
+
const result = await baseAction["promptPassword"]("Enter password:");
|
|
203
|
+
|
|
204
|
+
expect(result).toBe(mockPassword);
|
|
205
|
+
expect(inquirer.prompt).toHaveBeenCalledWith([{
|
|
206
|
+
type: "password",
|
|
207
|
+
name: "password",
|
|
208
|
+
message: chalk.yellow("Enter password:"),
|
|
209
|
+
mask: "*",
|
|
210
|
+
validate: expect.any(Function),
|
|
211
|
+
}]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("should validate password input is not empty", async () => {
|
|
215
|
+
vi.mocked(inquirer.prompt).mockResolvedValue({password: "valid-password"});
|
|
166
216
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
217
|
+
await baseAction["promptPassword"]("Enter password:");
|
|
218
|
+
|
|
219
|
+
const mockCall = vi.mocked(inquirer.prompt).mock.calls[0];
|
|
220
|
+
const questions = mockCall[0] as any;
|
|
221
|
+
const validateFn = questions[0].validate;
|
|
222
|
+
expect(validateFn("")).toBe("Password cannot be empty");
|
|
223
|
+
expect(validateFn("valid")).toBe(true);
|
|
174
224
|
});
|
|
175
225
|
|
|
176
|
-
test("should return private key when
|
|
177
|
-
vi.mocked(
|
|
226
|
+
test("should return private key when keystore exists and is valid", async () => {
|
|
227
|
+
vi.mocked(inquirer.prompt).mockResolvedValue({password: "correct-password"});
|
|
178
228
|
|
|
179
229
|
const result = await baseAction["getPrivateKey"]();
|
|
180
230
|
|
|
181
|
-
expect(result).toBe(
|
|
182
|
-
expect(
|
|
231
|
+
expect(result).toBe(mockWallet.privateKey);
|
|
232
|
+
expect(existsSync).toHaveBeenCalledWith("./test-keypair.json");
|
|
233
|
+
expect(readFileSync).toHaveBeenCalledWith("./test-keypair.json", "utf-8");
|
|
183
234
|
});
|
|
184
235
|
|
|
185
|
-
test("should create new keypair when
|
|
186
|
-
vi.mocked(
|
|
187
|
-
|
|
188
|
-
.
|
|
189
|
-
|
|
190
|
-
|
|
236
|
+
test("should create new keypair when keystore file does not exist", async () => {
|
|
237
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
238
|
+
vi.mocked(inquirer.prompt)
|
|
239
|
+
.mockResolvedValueOnce({confirmAction: true}) // confirm create new
|
|
240
|
+
.mockResolvedValueOnce({password: "new-password"}) // encrypt password
|
|
241
|
+
.mockResolvedValueOnce({password: "new-password"}); // confirm password
|
|
191
242
|
|
|
192
|
-
|
|
243
|
+
const result = await baseAction["getPrivateKey"]();
|
|
244
|
+
|
|
245
|
+
expect(result).toBe(mockWallet.privateKey);
|
|
246
|
+
expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
|
|
247
|
+
expect.objectContaining({message: chalk.yellow("Keypair file not found. Would you like to create a new keypair?")})
|
|
248
|
+
]));
|
|
193
249
|
});
|
|
194
250
|
|
|
195
|
-
test("should
|
|
196
|
-
vi.mocked(
|
|
251
|
+
test("should fail when keystore format is invalid and user declines", async () => {
|
|
252
|
+
vi.mocked(readFileSync).mockReturnValue('{"invalid": "format"}');
|
|
197
253
|
vi.mocked(inquirer.prompt).mockResolvedValue({confirmAction: false});
|
|
198
|
-
vi.spyOn(process, "exit").mockImplementation(() => {
|
|
199
|
-
throw new Error("process exited");
|
|
200
|
-
});
|
|
201
254
|
|
|
202
255
|
await expect(baseAction["getPrivateKey"]()).rejects.toThrow("process exited");
|
|
256
|
+
expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Invalid keystore format. Expected encrypted keystore file."));
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("should create new keypair when keystore format is invalid and user confirms", async () => {
|
|
260
|
+
vi.mocked(readFileSync).mockReturnValue('{"invalid": "format"}');
|
|
261
|
+
vi.mocked(inquirer.prompt)
|
|
262
|
+
.mockResolvedValueOnce({confirmAction: true})
|
|
263
|
+
.mockResolvedValueOnce({password: "new-password"})
|
|
264
|
+
.mockResolvedValueOnce({password: "new-password"});
|
|
265
|
+
|
|
266
|
+
const result = await baseAction["getPrivateKey"]();
|
|
267
|
+
|
|
268
|
+
expect(result).toBe(mockWallet.privateKey);
|
|
269
|
+
expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Invalid keystore format. Expected encrypted keystore file."));
|
|
270
|
+
expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
|
|
271
|
+
expect.objectContaining({message: chalk.yellow("Would you like to create a new keypair?")})
|
|
272
|
+
]));
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("should decrypt keystore successfully on first attempt", async () => {
|
|
276
|
+
vi.mocked(inquirer.prompt).mockResolvedValue({password: "correct-password"});
|
|
277
|
+
|
|
278
|
+
const result = await baseAction["decryptKeystore"](mockKeystoreData);
|
|
279
|
+
|
|
280
|
+
expect(result).toBe(mockWallet.privateKey);
|
|
281
|
+
expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
|
|
282
|
+
expect.objectContaining({message: chalk.yellow("Enter password to decrypt keystore:")})
|
|
283
|
+
]));
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("should retry on wrong password and succeed on second attempt", async () => {
|
|
287
|
+
vi.mocked(ethers.Wallet.fromEncryptedJson)
|
|
288
|
+
.mockRejectedValueOnce(new Error("Incorrect password"))
|
|
289
|
+
.mockResolvedValueOnce(mockWallet as any);
|
|
290
|
+
|
|
291
|
+
vi.mocked(inquirer.prompt)
|
|
292
|
+
.mockResolvedValueOnce({password: "wrong-password"})
|
|
293
|
+
.mockResolvedValueOnce({password: "correct-password"});
|
|
294
|
+
|
|
295
|
+
const result = await baseAction["decryptKeystore"](mockKeystoreData);
|
|
296
|
+
|
|
297
|
+
expect(result).toBe(mockWallet.privateKey);
|
|
298
|
+
expect(inquirer.prompt).toHaveBeenCalledTimes(2);
|
|
299
|
+
expect(inquirer.prompt).toHaveBeenNthCalledWith(2, expect.arrayContaining([
|
|
300
|
+
expect.objectContaining({message: chalk.yellow("Invalid password. Attempt 2/3 - Enter password to decrypt keystore:")})
|
|
301
|
+
]));
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("should exit after 3 failed password attempts", async () => {
|
|
305
|
+
vi.mocked(ethers.Wallet.fromEncryptedJson).mockRejectedValue(new Error("Incorrect password"));
|
|
306
|
+
vi.mocked(inquirer.prompt).mockResolvedValue({password: "wrong-password"});
|
|
307
|
+
|
|
308
|
+
await expect(baseAction["decryptKeystore"](mockKeystoreData)).rejects.toThrow("process exited");
|
|
309
|
+
|
|
310
|
+
expect(inquirer.prompt).toHaveBeenCalledTimes(3);
|
|
311
|
+
expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Maximum password attempts exceeded (3/3)."));
|
|
312
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("should create new keypair successfully", async () => {
|
|
316
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
317
|
+
vi.mocked(inquirer.prompt)
|
|
318
|
+
.mockResolvedValueOnce({password: "test-password"})
|
|
319
|
+
.mockResolvedValueOnce({password: "test-password"});
|
|
320
|
+
|
|
321
|
+
const result = await baseAction["createKeypair"]("./new-keypair.json", false);
|
|
322
|
+
|
|
323
|
+
expect(result).toBe(mockWallet.privateKey);
|
|
324
|
+
expect(ethers.Wallet.createRandom).toHaveBeenCalled();
|
|
325
|
+
expect(mockWallet.encrypt).toHaveBeenCalledWith("test-password");
|
|
326
|
+
expect(writeFileSync).toHaveBeenCalled();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("should fail when file exists and overwrite is false", async () => {
|
|
330
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
331
|
+
|
|
332
|
+
await expect(baseAction["createKeypair"]("./test-keypair.json", false)).rejects.toThrow("process exited");
|
|
333
|
+
|
|
334
|
+
expect(mockSpinner.fail).toHaveBeenCalledWith(
|
|
335
|
+
chalk.red("The file at ./test-keypair.json already exists. Use the '--overwrite' option to replace it.")
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("should fail when passwords do not match", async () => {
|
|
340
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
341
|
+
vi.mocked(inquirer.prompt)
|
|
342
|
+
.mockResolvedValueOnce({password: "password1"})
|
|
343
|
+
.mockResolvedValueOnce({password: "password2"});
|
|
344
|
+
|
|
345
|
+
await expect(baseAction["createKeypair"]("./new-keypair.json", false)).rejects.toThrow("process exited");
|
|
346
|
+
|
|
347
|
+
expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Passwords do not match"));
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("should fail when password is too short", async () => {
|
|
351
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
352
|
+
vi.mocked(inquirer.prompt)
|
|
353
|
+
.mockResolvedValueOnce({password: "short"})
|
|
354
|
+
.mockResolvedValueOnce({password: "short"});
|
|
355
|
+
|
|
356
|
+
await expect(baseAction["createKeypair"]("./new-keypair.json", false)).rejects.toThrow("process exited");
|
|
357
|
+
|
|
358
|
+
expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Password must be at least 8 characters long"));
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("should overwrite existing file when overwrite is true", async () => {
|
|
362
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
363
|
+
vi.mocked(inquirer.prompt)
|
|
364
|
+
.mockResolvedValueOnce({password: "test-password"})
|
|
365
|
+
.mockResolvedValueOnce({password: "test-password"});
|
|
366
|
+
|
|
367
|
+
const result = await baseAction["createKeypair"]("./existing.json", true);
|
|
368
|
+
|
|
369
|
+
expect(result).toBe(mockWallet.privateKey);
|
|
370
|
+
expect(writeFileSync).toHaveBeenCalled();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("should return true for valid keystore format", () => {
|
|
374
|
+
const validKeystore = {
|
|
375
|
+
version: 1,
|
|
376
|
+
encrypted: "encrypted-data",
|
|
377
|
+
address: "0x1234567890123456789012345678901234567890",
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const result = baseAction["isValidKeystoreFormat"](validKeystore);
|
|
381
|
+
expect(result).toBe(true);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("should return false for invalid keystore version", () => {
|
|
385
|
+
const invalidKeystore = {
|
|
386
|
+
version: 2,
|
|
387
|
+
encrypted: "encrypted-data",
|
|
388
|
+
address: "0x1234567890123456789012345678901234567890",
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const result = baseAction["isValidKeystoreFormat"](invalidKeystore);
|
|
392
|
+
expect(result).toBe(false);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("should return false for keystore missing fields", () => {
|
|
396
|
+
const invalidKeystore = {
|
|
397
|
+
version: 1,
|
|
398
|
+
encrypted: "encrypted-data",
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const result = baseAction["isValidKeystoreFormat"](invalidKeystore);
|
|
402
|
+
expect(result).toBe(false);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("should return false for null or undefined keystore", () => {
|
|
406
|
+
expect(baseAction["isValidKeystoreFormat"](null)).toBe(false);
|
|
407
|
+
expect(baseAction["isValidKeystoreFormat"](undefined)).toBe(false);
|
|
203
408
|
});
|
|
204
409
|
|
|
205
410
|
describe("formatOutput", () => {
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
2
|
-
import { ethers } from "ethers";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { ConfigFileManager } from "../config/ConfigFileManager";
|
|
5
|
-
|
|
6
|
-
export class KeypairManager extends ConfigFileManager {
|
|
7
|
-
constructor() {
|
|
8
|
-
super();
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
getPrivateKey(): string | undefined {
|
|
12
|
-
const keypairPath = this.getConfigByKey("keyPairPath");
|
|
13
|
-
|
|
14
|
-
if (!keypairPath || !existsSync(keypairPath)) {
|
|
15
|
-
return ""
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const keypairData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
19
|
-
|
|
20
|
-
if (!keypairData.privateKey) {
|
|
21
|
-
return ""
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return keypairData.privateKey;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
createKeypair(outputPath = "./keypair.json", overwrite: boolean = false): void {
|
|
28
|
-
const finalOutputPath = this.getFilePath(outputPath);
|
|
29
|
-
|
|
30
|
-
if(existsSync(finalOutputPath) && !overwrite) {
|
|
31
|
-
throw new Error(`The file at ${finalOutputPath} already exists. Use the '--overwrite' option to replace it.`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const wallet = ethers.Wallet.createRandom();
|
|
35
|
-
const keypairData = {
|
|
36
|
-
address: wallet.address,
|
|
37
|
-
privateKey: wallet.privateKey,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
writeFileSync(finalOutputPath, JSON.stringify(keypairData, null, 2));
|
|
41
|
-
this.writeConfig('keyPairPath', finalOutputPath);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { describe, test, vi, beforeEach, afterEach, expect } from "vitest";
|
|
2
|
-
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
3
|
-
import { ethers } from "ethers";
|
|
4
|
-
import { KeypairManager } from "../../../src/lib/accounts/KeypairManager";
|
|
5
|
-
|
|
6
|
-
vi.mock("fs");
|
|
7
|
-
vi.mock("ethers");
|
|
8
|
-
|
|
9
|
-
describe("KeypairManager", () => {
|
|
10
|
-
let keypairManager: KeypairManager;
|
|
11
|
-
const mockPrivateKey = "0xMockedPrivateKey";
|
|
12
|
-
const mockAddress = "0xMockedAddress";
|
|
13
|
-
const mockKeypairPath = "/mocked/path/keypair.json";
|
|
14
|
-
const mockKeypairData = {
|
|
15
|
-
address: mockAddress,
|
|
16
|
-
privateKey: mockPrivateKey,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
vi.clearAllMocks();
|
|
21
|
-
keypairManager = new KeypairManager();
|
|
22
|
-
vi.spyOn(keypairManager as any, "getConfigByKey").mockReturnValue(mockKeypairPath);
|
|
23
|
-
vi.spyOn(keypairManager as any, "writeConfig").mockImplementation(() => {});
|
|
24
|
-
vi.spyOn(keypairManager as any, "getFilePath").mockReturnValue(mockKeypairPath);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(() => {
|
|
28
|
-
vi.restoreAllMocks();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("getPrivateKey", () => {
|
|
32
|
-
test("should return private key when keypair file exists and contains private key", () => {
|
|
33
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
34
|
-
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockKeypairData));
|
|
35
|
-
|
|
36
|
-
const result = keypairManager.getPrivateKey();
|
|
37
|
-
|
|
38
|
-
expect(result).toBe(mockPrivateKey);
|
|
39
|
-
expect(existsSync).toHaveBeenCalledWith(mockKeypairPath);
|
|
40
|
-
expect(readFileSync).toHaveBeenCalledWith(mockKeypairPath, "utf-8");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("should return empty string when keypair file does not exist", () => {
|
|
44
|
-
vi.mocked(existsSync).mockReturnValue(false);
|
|
45
|
-
|
|
46
|
-
const result = keypairManager.getPrivateKey();
|
|
47
|
-
|
|
48
|
-
expect(result).toBe("");
|
|
49
|
-
expect(existsSync).toHaveBeenCalledWith(mockKeypairPath);
|
|
50
|
-
expect(readFileSync).not.toHaveBeenCalled();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("should return empty string when keypair file exists but has no private key", () => {
|
|
54
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
55
|
-
vi.mocked(readFileSync).mockReturnValue(JSON.stringify({ address: mockAddress }));
|
|
56
|
-
|
|
57
|
-
const result = keypairManager.getPrivateKey();
|
|
58
|
-
|
|
59
|
-
expect(result).toBe("");
|
|
60
|
-
expect(existsSync).toHaveBeenCalledWith(mockKeypairPath);
|
|
61
|
-
expect(readFileSync).toHaveBeenCalledWith(mockKeypairPath, "utf-8");
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe("createKeypair", () => {
|
|
66
|
-
test("should create new keypair and save it to file", () => {
|
|
67
|
-
const mockWallet = {
|
|
68
|
-
address: mockAddress,
|
|
69
|
-
privateKey: mockPrivateKey,
|
|
70
|
-
};
|
|
71
|
-
vi.mocked(ethers.Wallet.createRandom).mockReturnValue(mockWallet as any);
|
|
72
|
-
vi.mocked(existsSync).mockReturnValue(false);
|
|
73
|
-
|
|
74
|
-
keypairManager.createKeypair();
|
|
75
|
-
|
|
76
|
-
expect(ethers.Wallet.createRandom).toHaveBeenCalled();
|
|
77
|
-
expect(writeFileSync).toHaveBeenCalledWith(
|
|
78
|
-
mockKeypairPath,
|
|
79
|
-
JSON.stringify(mockKeypairData, null, 2)
|
|
80
|
-
);
|
|
81
|
-
expect(keypairManager["writeConfig"]).toHaveBeenCalledWith("keyPairPath", mockKeypairPath);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("should throw error when file exists and overwrite is false", () => {
|
|
85
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
86
|
-
|
|
87
|
-
expect(() => keypairManager.createKeypair()).toThrow(
|
|
88
|
-
`The file at ${mockKeypairPath} already exists. Use the '--overwrite' option to replace it.`
|
|
89
|
-
);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("should overwrite existing file when overwrite is true", () => {
|
|
93
|
-
const mockWallet = {
|
|
94
|
-
address: mockAddress,
|
|
95
|
-
privateKey: mockPrivateKey,
|
|
96
|
-
};
|
|
97
|
-
vi.mocked(ethers.Wallet.createRandom).mockReturnValue(mockWallet as any);
|
|
98
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
99
|
-
|
|
100
|
-
keypairManager.createKeypair("./keypair.json", true);
|
|
101
|
-
|
|
102
|
-
expect(ethers.Wallet.createRandom).toHaveBeenCalled();
|
|
103
|
-
expect(writeFileSync).toHaveBeenCalledWith(
|
|
104
|
-
mockKeypairPath,
|
|
105
|
-
JSON.stringify(mockKeypairData, null, 2)
|
|
106
|
-
);
|
|
107
|
-
expect(keypairManager["writeConfig"]).toHaveBeenCalledWith("keyPairPath", mockKeypairPath);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
});
|