genlayer 0.18.5 → 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 CHANGED
@@ -1,5 +1,17 @@
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
+
9
+ ## 0.19.0 (2025-07-09)
10
+
11
+ ### Features
12
+
13
+ * appeal command ([#233](https://github.com/yeagerai/genlayer-cli/issues/233)) ([a33f882](https://github.com/yeagerai/genlayer-cli/commit/a33f882ec7f1e7758bbf6daef67b425be10e87f2))
14
+
3
15
  ## 0.18.5 (2025-07-09)
4
16
 
5
17
  ## 0.18.4 (2025-06-27)
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.18.5";
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
- this.keypairManager = new KeypairManager();
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 privateKey = this.keypairManager.getPrivateKey();
40961
- if (privateKey) {
40962
- return privateKey;
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);
40956
+ }
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);
40963
40962
  }
40964
- await this.confirmPrompt("Keypair file not found. Would you like to create a new keypair?");
40965
- this.keypairManager.createKeypair();
40966
- return this.keypairManager.getPrivateKey();
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 keypair...`);
42129
- this.keypairManager.createKeypair(options.output, options.overwrite);
42130
- this.succeedSpinner(`Keypair successfully created and saved to: ${options.output}`);
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 keypair", error);
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 keypair and saves it to a file").option("--output <path>", "Path to save the keypair", "./keypair.json").option("--overwrite", "Overwrite the existing file if it already exists", false).action((options) => {
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
  }
@@ -42779,6 +42815,43 @@ function initializeNetworkCommands(program2) {
42779
42815
  return program2;
42780
42816
  }
42781
42817
 
42818
+ // src/commands/transactions/appeal.ts
42819
+ var AppealAction = class extends BaseAction {
42820
+ constructor() {
42821
+ super();
42822
+ }
42823
+ async appeal({
42824
+ txId,
42825
+ rpc
42826
+ }) {
42827
+ const client = await this.getClient(rpc);
42828
+ await client.initializeConsensusSmartContract();
42829
+ this.startSpinner(`Appealing transaction ${txId}...`);
42830
+ try {
42831
+ const hash2 = await client.appealTransaction({
42832
+ txId
42833
+ });
42834
+ const result = await client.waitForTransactionReceipt({
42835
+ hash: hash2,
42836
+ retries: 100,
42837
+ interval: 5e3
42838
+ });
42839
+ this.succeedSpinner("Appeal operation successfully executed", result);
42840
+ } catch (error) {
42841
+ this.failSpinner("Error during appeal operation", error);
42842
+ }
42843
+ }
42844
+ };
42845
+
42846
+ // src/commands/transactions/index.ts
42847
+ function initializeTransactionsCommands(program2) {
42848
+ program2.command("appeal <txId>").description("Appeal a transaction by its hash").option("--rpc <rpcUrl>", "RPC URL for the network").action(async (txId, options) => {
42849
+ const appealAction = new AppealAction();
42850
+ await appealAction.appeal({ txId, ...options });
42851
+ });
42852
+ return program2;
42853
+ }
42854
+
42782
42855
  // src/index.ts
42783
42856
  function initializeCLI() {
42784
42857
  program.version(version).description(CLI_DESCRIPTION);
@@ -42790,6 +42863,7 @@ function initializeCLI() {
42790
42863
  initializeValidatorCommands(program);
42791
42864
  initializeScaffoldCommands(program);
42792
42865
  initializeNetworkCommands(program);
42866
+ initializeTransactionsCommands(program);
42793
42867
  program.parse(process.argv);
42794
42868
  }
42795
42869
  initializeCLI();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genlayer",
3
- "version": "0.18.5",
3
+ "version": "0.20.0",
4
4
  "description": "GenLayer Command Line Tool",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -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 keypair...`);
16
- this.keypairManager.createKeypair(options.output, options.overwrite);
17
- this.succeedSpinner(`Keypair successfully created and saved to: ${options.output}`);
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 keypair", error);
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 keypair and saves it to a file")
13
- .option("--output <path>", "Path to save the keypair", "./keypair.json")
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;
@@ -0,0 +1,39 @@
1
+ import {TransactionHash} from "genlayer-js/types";
2
+ import {BaseAction} from "../../lib/actions/BaseAction";
3
+
4
+ export interface AppealOptions {
5
+ rpc?: string;
6
+ }
7
+
8
+ export class AppealAction extends BaseAction {
9
+ constructor() {
10
+ super();
11
+ }
12
+
13
+ async appeal({
14
+ txId,
15
+ rpc,
16
+ }: {
17
+ txId: TransactionHash;
18
+ rpc?: string;
19
+ }): Promise<void> {
20
+ const client = await this.getClient(rpc);
21
+ await client.initializeConsensusSmartContract();
22
+ this.startSpinner(`Appealing transaction ${txId}...`);
23
+
24
+ try {
25
+ const hash = await client.appealTransaction({
26
+ txId,
27
+ });
28
+
29
+ const result = await client.waitForTransactionReceipt({
30
+ hash,
31
+ retries: 100,
32
+ interval: 5000,
33
+ });
34
+ this.succeedSpinner("Appeal operation successfully executed", result);
35
+ } catch (error) {
36
+ this.failSpinner("Error during appeal operation", error);
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,16 @@
1
+ import {Command} from "commander";
2
+ import {TransactionHash} from "genlayer-js/types";
3
+ import {AppealAction, AppealOptions} from "./appeal";
4
+
5
+ export function initializeTransactionsCommands(program: Command) {
6
+ program
7
+ .command("appeal <txId>")
8
+ .description("Appeal a transaction by its hash")
9
+ .option("--rpc <rpcUrl>", "RPC URL for the network")
10
+ .action(async (txId: TransactionHash, options: AppealOptions) => {
11
+ const appealAction = new AppealAction();
12
+ await appealAction.appeal({txId, ...options});
13
+ });
14
+
15
+ return program;
16
+ }
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import {initializeValidatorCommands} from "../src/commands/validators";
10
10
  import {initializeUpdateCommands} from "../src/commands/update";
11
11
  import {initializeScaffoldCommands} from "../src/commands/scaffold";
12
12
  import {initializeNetworkCommands} from "../src/commands/network";
13
+ import {initializeTransactionsCommands} from "../src/commands/transactions";
13
14
 
14
15
  export function initializeCLI() {
15
16
  program.version(version).description(CLI_DESCRIPTION);
@@ -21,6 +22,7 @@ export function initializeCLI() {
21
22
  initializeValidatorCommands(program);
22
23
  initializeScaffoldCommands(program);
23
24
  initializeNetworkCommands(program);
25
+ initializeTransactionsCommands(program);
24
26
  program.parse(process.argv);
25
27
  }
26
28
 
@@ -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
- this.keypairManager = new KeypairManager();
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 privateKey = this.keypairManager.getPrivateKey();
37
- if (privateKey) {
38
- return privateKey;
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
- await this.confirmPrompt("Keypair file not found. Would you like to create a new keypair?");
41
- this.keypairManager.createKeypair();
42
- return this.keypairManager.getPrivateKey();
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));
@@ -0,0 +1,5 @@
1
+ export interface KeystoreData {
2
+ version: number;
3
+ encrypted: string;
4
+ address: string;
5
+ }
@@ -0,0 +1,99 @@
1
+ import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
2
+ import {createClient, createAccount} from "genlayer-js";
3
+ import type {TransactionHash} from "genlayer-js/types";
4
+ import {AppealAction} from "../../src/commands/transactions/appeal";
5
+
6
+ vi.mock("genlayer-js");
7
+
8
+ describe("AppealAction", () => {
9
+ let appealAction: AppealAction;
10
+ const mockClient = {
11
+ appealTransaction: vi.fn(),
12
+ waitForTransactionReceipt: vi.fn(),
13
+ initializeConsensusSmartContract: vi.fn(),
14
+ };
15
+
16
+ const mockPrivateKey = "mocked_private_key";
17
+ const mockTxId = "0x1234567890123456789012345678901234567890123456789012345678901234" as TransactionHash;
18
+
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ vi.mocked(createClient).mockReturnValue(mockClient as any);
22
+ vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any);
23
+ appealAction = new AppealAction();
24
+ vi.spyOn(appealAction as any, "getPrivateKey").mockResolvedValue(mockPrivateKey);
25
+
26
+ vi.spyOn(appealAction as any, "startSpinner").mockImplementation(() => {});
27
+ vi.spyOn(appealAction as any, "succeedSpinner").mockImplementation(() => {});
28
+ vi.spyOn(appealAction as any, "failSpinner").mockImplementation(() => {});
29
+ });
30
+
31
+ afterEach(() => {
32
+ vi.restoreAllMocks();
33
+ });
34
+
35
+ test("calls appealTransaction successfully", async () => {
36
+ const mockReceipt = {status: "success"};
37
+
38
+ vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt);
39
+
40
+ await appealAction.appeal({
41
+ txId: mockTxId,
42
+ });
43
+
44
+ expect(mockClient.appealTransaction).toHaveBeenCalledWith({
45
+ txId: mockTxId,
46
+ });
47
+ expect(appealAction["succeedSpinner"]).toHaveBeenCalledWith(
48
+ "Appeal operation successfully executed",
49
+ mockReceipt,
50
+ );
51
+ });
52
+
53
+ test("handles appealTransaction errors", async () => {
54
+ vi.mocked(mockClient.appealTransaction).mockRejectedValue(new Error("Mocked appeal error"));
55
+
56
+ await appealAction.appeal({txId: mockTxId});
57
+
58
+ expect(appealAction["failSpinner"]).toHaveBeenCalledWith(
59
+ "Error during appeal operation",
60
+ expect.any(Error),
61
+ );
62
+ });
63
+
64
+ test("uses custom RPC URL for appeal operations", async () => {
65
+ const rpcUrl = "https://custom-rpc-url.com";
66
+ const mockReceipt = {status: "success"};
67
+
68
+ vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt);
69
+
70
+ await appealAction.appeal({
71
+ txId: mockTxId,
72
+ rpc: rpcUrl,
73
+ });
74
+
75
+ expect(createClient).toHaveBeenCalledWith(
76
+ expect.objectContaining({
77
+ endpoint: rpcUrl,
78
+ }),
79
+ );
80
+ expect(mockClient.appealTransaction).toHaveBeenCalledWith({
81
+ txId: mockTxId,
82
+ });
83
+ expect(appealAction["succeedSpinner"]).toHaveBeenCalledWith(
84
+ "Appeal operation successfully executed",
85
+ mockReceipt,
86
+ );
87
+ });
88
+
89
+ test("initializes consensus smart contract before appeal", async () => {
90
+ const mockReceipt = {status: "success"};
91
+
92
+ vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt);
93
+
94
+ await appealAction.appeal({txId: mockTxId});
95
+
96
+ expect(mockClient.initializeConsensusSmartContract).toHaveBeenCalledTimes(1);
97
+ expect(mockClient.appealTransaction).toHaveBeenCalled();
98
+ });
99
+ });
@@ -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
- (keypairCreator as any).keypairManager = mockKeypairManager;
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, "writeConfig").mockImplementation(() => {});
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 a keypair", () => {
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 keypair...");
51
- expect(mockKeypairManager.createKeypair).toHaveBeenCalledWith(options.output, options.overwrite);
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
- "Keypair successfully created and saved to: keypair.json",
33
+ "Encrypted keystore successfully created and saved to: keypair.json",
54
34
  );
55
35
  });
56
36
 
57
- test("handles errors during keypair creation", () => {
37
+ test("handles errors during keystore creation", async () => {
58
38
  const mockError = new Error("Mocked creation error");
59
- mockKeypairManager.createKeypair.mockImplementation(() => {
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 keypair", mockError);
43
+ expect(keypairCreator["failSpinner"]).toHaveBeenCalledWith("Failed to generate keystore", mockError);
66
44
  });
67
45
  });
@@ -0,0 +1,58 @@
1
+ import {Command} from "commander";
2
+ import {AppealAction} from "../../src/commands/transactions/appeal";
3
+ import {vi, describe, beforeEach, afterEach, test, expect} from "vitest";
4
+ import {initializeTransactionsCommands} from "../../src/commands/transactions";
5
+
6
+ vi.mock("../../src/commands/transactions/appeal");
7
+
8
+ describe("appeal command", () => {
9
+ let program: Command;
10
+ const mockTxId = "0x1234567890123456789012345678901234567890123456789012345678901234";
11
+
12
+ beforeEach(() => {
13
+ program = new Command();
14
+ initializeTransactionsCommands(program);
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ afterEach(() => {
19
+ vi.restoreAllMocks();
20
+ });
21
+
22
+ test("AppealAction.appeal is called with default options", async () => {
23
+ program.parse(["node", "test", "appeal", mockTxId]);
24
+ expect(AppealAction).toHaveBeenCalledTimes(1);
25
+ expect(AppealAction.prototype.appeal).toHaveBeenCalledWith({
26
+ txId: mockTxId,
27
+ });
28
+ });
29
+
30
+ test("AppealAction.appeal is called with custom RPC URL", async () => {
31
+ program.parse([
32
+ "node",
33
+ "test",
34
+ "appeal",
35
+ mockTxId,
36
+ "--rpc",
37
+ "https://custom-rpc-url-for-appeal.com",
38
+ ]);
39
+ expect(AppealAction).toHaveBeenCalledTimes(1);
40
+ expect(AppealAction.prototype.appeal).toHaveBeenCalledWith({
41
+ txId: mockTxId,
42
+ rpc: "https://custom-rpc-url-for-appeal.com",
43
+ });
44
+ });
45
+
46
+ test("AppealAction is instantiated when the appeal command is executed", async () => {
47
+ program.parse(["node", "test", "appeal", mockTxId]);
48
+ expect(AppealAction).toHaveBeenCalledTimes(1);
49
+ });
50
+
51
+ test("throws error for unrecognized options", async () => {
52
+ const appealCommand = program.commands.find(cmd => cmd.name() === "appeal");
53
+ appealCommand?.exitOverride();
54
+ expect(() =>
55
+ program.parse(["node", "test", "appeal", mockTxId, "--invalid-option"]),
56
+ ).toThrowError("error: unknown option '--invalid-option'");
57
+ });
58
+ });
@@ -41,6 +41,10 @@ vi.mock("../src/commands/network", () => ({
41
41
  initializeNetworkCommands: vi.fn(),
42
42
  }));
43
43
 
44
+ vi.mock("../src/commands/transactions", () => ({
45
+ initializeTransactionsCommands: vi.fn(),
46
+ }));
47
+
44
48
  describe("CLI", () => {
45
49
  it("should initialize CLI", () => {
46
50
  expect(initializeCLI).not.toThrow();
@@ -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
- const mockPrivateKey = "mocked_private_key";
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
- beforeEach(() => {
168
- baseAction["keypairManager"] = {
169
- getPrivateKey: vi.fn(),
170
- createKeypair: vi.fn(),
171
- getKeypairPath: vi.fn(),
172
- setKeypairPath: vi.fn(),
173
- } as any;
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 it exists", async () => {
177
- vi.mocked(baseAction["keypairManager"].getPrivateKey).mockReturnValue(mockPrivateKey);
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(mockPrivateKey);
182
- expect(baseAction["keypairManager"].createKeypair).not.toHaveBeenCalled();
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 private key doesn't exist and user confirms", async () => {
186
- vi.mocked(baseAction["keypairManager"].getPrivateKey)
187
- .mockReturnValueOnce(undefined)
188
- .mockReturnValueOnce(mockPrivateKey);
189
- vi.mocked(inquirer.prompt).mockResolvedValue({confirmAction: true});
190
- await baseAction["getPrivateKey"]();
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
- expect(baseAction["keypairManager"].createKeypair).toHaveBeenCalled();
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 exit when private key doesn't exist and user declines", async () => {
196
- vi.mocked(baseAction["keypairManager"].getPrivateKey).mockReturnValueOnce(undefined);
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
- });