genlayer 0.19.0 → 0.21.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/.env.example CHANGED
@@ -20,10 +20,8 @@ JSONRPC_REPLICAS='1' # number of JsonRPC container replicas to run, used
20
20
 
21
21
 
22
22
  # WebRequest Server Configuration
23
- WEBREQUESTPORT=5000
24
- WEBREQUESTSELENIUMPORT=5001
25
- WEBREQUESTPROTOCOL='http'
26
- WEBREQUESTHOST='webrequest'
23
+ WEBDRIVERHOST=webdriver
24
+ WEBDRIVERPORT=4444
27
25
 
28
26
 
29
27
  # Ollama server details
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0 (2025-07-14)
4
+
5
+ ### Features
6
+
7
+ * update docker compose ([#237](https://github.com/yeagerai/genlayer-cli/issues/237)) ([18ea508](https://github.com/yeagerai/genlayer-cli/commit/18ea508a155bc9112612430add49c4c0e23ad3d5))
8
+
9
+ ## 0.20.0 (2025-07-09)
10
+
11
+ ### Features
12
+
13
+ * use keystore to store private keys ([#235](https://github.com/yeagerai/genlayer-cli/issues/235)) ([f18f2af](https://github.com/yeagerai/genlayer-cli/commit/f18f2afbc35c2dd988908d51ffce6b0523cfee81))
14
+
3
15
  ## 0.19.0 (2025-07-09)
4
16
 
5
17
  ### 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.19.0";
17726
+ var version = "0.21.0";
17727
17727
  var package_default = {
17728
17728
  name: "genlayer",
17729
17729
  version,
@@ -17814,7 +17814,7 @@ var CLI_DESCRIPTION = "GenLayer CLI is a development environment for the GenLaye
17814
17814
  import inquirer2 from "inquirer";
17815
17815
 
17816
17816
  // src/lib/config/simulator.ts
17817
- var localnetCompatibleVersion = "v0.55.0";
17817
+ var localnetCompatibleVersion = "v0.65.0";
17818
17818
  var DEFAULT_JSON_RPC_URL = "http://localhost:4000/api";
17819
17819
  var CONTAINERS_NAME_PREFIX = "/genlayer-";
17820
17820
  var IMAGES_NAME_PREFIX = "yeagerai";
@@ -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);
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);
40963
40978
  }
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();
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}`));
@@ -41846,6 +41882,19 @@ Run npm install -g genlayer to update
41846
41882
  }
41847
41883
  return version5;
41848
41884
  }
41885
+ compareVersions(version1, version22) {
41886
+ const v1 = version1.replace(/^v/, "");
41887
+ const v2 = version22.replace(/^v/, "");
41888
+ const parts1 = v1.split("-")[0].split(".").map(Number);
41889
+ const parts2 = v2.split("-")[0].split(".").map(Number);
41890
+ for (let i2 = 0; i2 < Math.max(parts1.length, parts2.length); i2++) {
41891
+ const part1 = parts1[i2] || 0;
41892
+ const part2 = parts2[i2] || 0;
41893
+ if (part1 < part2) return -1;
41894
+ if (part1 > part2) return 1;
41895
+ }
41896
+ return 0;
41897
+ }
41849
41898
  async isLocalnetRunning() {
41850
41899
  const genlayerContainers = await this.getGenlayerContainers();
41851
41900
  const runningContainers = genlayerContainers.filter((container) => container.State === "running");
@@ -41889,6 +41938,10 @@ var InitAction = class extends BaseAction {
41889
41938
  let localnetVersion = options.localnetVersion;
41890
41939
  if (localnetVersion !== "latest") {
41891
41940
  localnetVersion = this.simulatorService.normalizeLocalnetVersion(localnetVersion);
41941
+ if (this.simulatorService.compareVersions(localnetVersion, localnetCompatibleVersion) < 0) {
41942
+ this.failSpinner(`Localnet version ${localnetVersion} is not supported. Minimum required version is ${localnetCompatibleVersion}. Please use a newer version.`);
41943
+ return;
41944
+ }
41892
41945
  }
41893
41946
  this.startSpinner("Checking CLI version...");
41894
41947
  await this.simulatorService.checkCliVersion();
@@ -42103,7 +42156,7 @@ var StopAction = class extends BaseAction {
42103
42156
 
42104
42157
  // src/commands/general/index.ts
42105
42158
  function initializeGeneralCommands(program2) {
42106
- program2.command("init").description("Initialize the GenLayer Environment").option("--numValidators <numValidators>", "Number of validators", "5").option("--headless", "Headless mode", false).option("--reset-db", "Reset Database", false).option("--localnet-version <localnetVersion>", "Select a specific localnet version", localnetCompatibleVersion).option("--ollama", "Enable Ollama container", false).action(async (options) => {
42159
+ program2.command("init").description("Initialize the GenLayer Environment").option("--numValidators <numValidators>", "Number of validators", "5").option("--headless", "Headless mode", false).option("--reset-db", "Reset Database", false).option("--localnet-version <localnetVersion>", `Select a specific localnet version (minimum: ${localnetCompatibleVersion})`, localnetCompatibleVersion).option("--ollama", "Enable Ollama container", false).action(async (options) => {
42107
42160
  const initAction = new InitAction();
42108
42161
  await initAction.execute(options);
42109
42162
  });
@@ -42123,13 +42176,13 @@ var KeypairCreator = class extends BaseAction {
42123
42176
  constructor() {
42124
42177
  super();
42125
42178
  }
42126
- createKeypairAction(options) {
42179
+ async createKeypairAction(options) {
42127
42180
  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}`);
42181
+ this.startSpinner(`Creating encrypted keystore...`);
42182
+ await this.createKeypair(options.output, options.overwrite);
42183
+ this.succeedSpinner(`Encrypted keystore successfully created and saved to: ${options.output}`);
42131
42184
  } catch (error) {
42132
- this.failSpinner("Failed to generate keypair", error);
42185
+ this.failSpinner("Failed to generate keystore", error);
42133
42186
  }
42134
42187
  }
42135
42188
  };
@@ -42137,9 +42190,9 @@ var KeypairCreator = class extends BaseAction {
42137
42190
  // src/commands/keygen/index.ts
42138
42191
  function initializeKeygenCommands(program2) {
42139
42192
  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) => {
42193
+ 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
42194
  const keypairCreator = new KeypairCreator();
42142
- keypairCreator.createKeypairAction(options);
42195
+ await keypairCreator.createKeypairAction(options);
42143
42196
  });
42144
42197
  return program2;
42145
42198
  }
@@ -32,9 +32,8 @@ services:
32
32
  - FLASK_SERVER_PORT=${RPCPORT:-5000}
33
33
  - PYTHONUNBUFFERED=1
34
34
  - RPCDEBUGPORT=${RPCDEBUGPORT:-5001}
35
- - WEBREQUESTPORT=${WEBREQUESTPORT:-5002}
36
- - WEBREQUESTHOST=${WEBREQUESTHOST:-localhost}
37
- - WEBREQUESTPROTOCOL=${WEBREQUESTPROTOCOL:-http}
35
+ - WEBDRIVERHOST=${WEBDRIVERHOST:-webdriver}
36
+ - WEBDRIVERPORT=${WEBDRIVERPORT:-4444}
38
37
  ports:
39
38
  - "${RPCPORT:-5000}:${RPCPORT:-5000}"
40
39
  - "${RPCDEBUGPORT:-5001}:${RPCDEBUGPORT:-5001}"
@@ -49,8 +48,8 @@ services:
49
48
  depends_on:
50
49
  database-migration:
51
50
  condition: service_completed_successfully
52
- webrequest:
53
- condition: service_healthy
51
+ webdriver:
52
+ condition: service_healthy
54
53
  expose:
55
54
  - "${RPCPORT:-5000}"
56
55
  restart: always
@@ -67,25 +66,14 @@ services:
67
66
  - hardhat_artifacts:/app/hardhat/artifacts
68
67
  - hardhat_deployments:/app/hardhat/deployments
69
68
 
70
- webrequest:
71
- image: yeagerai/simulator-webrequest:${LOCALNETVERSION:-latest}
69
+
70
+ webdriver:
71
+ image: yeagerai/genlayer-genvm-webdriver:0.0.3
72
72
  shm_size: 2gb
73
73
  environment:
74
- - FLASK_SERVER_PORT=${WEBREQUESTPORT:-5002}
75
- - WEBREQUESTSELENIUMPORT=${WEBREQUESTSELENIUMPORT:-4444}
76
- - PYTHONUNBUFFERED=1
77
- - WEBREQUESTPORT=${WEBREQUESTPORT}
78
- - WEBREQUESTHOST=${WEBREQUESTHOST}
79
- - WEBREQUESTPROTOCOL=${WEBREQUESTPROTOCOL}
74
+ - PORT=${WEBDRIVERPORT:-4444}
80
75
  expose:
81
- - "${WEBREQUESTPORT:-5002}:${WEBREQUESTPORT:-5002}"
82
- - "${WEBREQUESTSELENIUMPORT:-4444}:${WEBREQUESTSELENIUMPORT:-4444}"
83
- env_file:
84
- - ./.env
85
- depends_on:
86
- ollama:
87
- condition: service_started
88
- required: false
76
+ - "${WEBDRIVERPORT:-4444}"
89
77
  restart: always
90
78
  security_opt:
91
79
  - "no-new-privileges=true"
@@ -94,7 +82,7 @@ services:
94
82
  options:
95
83
  max-size: "10m"
96
84
  max-file: "3"
97
-
85
+
98
86
  ollama:
99
87
  image: ollama/ollama:0.6.6
100
88
  ports:
@@ -142,14 +130,9 @@ services:
142
130
  image: yeagerai/simulator-database-migration:${LOCALNETVERSION:-latest}
143
131
  environment:
144
132
  - DB_URL=postgresql://${DBUSER:-postgres}:${DBPASSWORD:-postgres}@postgres/${DBNAME:-simulator_db}
145
- - WEBREQUESTPORT=${WEBREQUESTPORT:-5002}
146
- - WEBREQUESTHOST=${WEBREQUESTHOST:-localhost}
147
- - WEBREQUESTPROTOCOL=${WEBREQUESTPROTOCOL:-http}
148
133
  depends_on:
149
134
  postgres:
150
135
  condition: service_healthy
151
- webrequest:
152
- condition: service_healthy
153
136
 
154
137
  hardhat:
155
138
  image: yeagerai/simulator-hardhat:${LOCALNETVERSION:-latest}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genlayer",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "GenLayer Command Line Tool",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -13,7 +13,7 @@ export function initializeGeneralCommands(program: Command) {
13
13
  .option("--numValidators <numValidators>", "Number of validators", "5")
14
14
  .option("--headless", "Headless mode", false)
15
15
  .option("--reset-db", "Reset Database", false)
16
- .option("--localnet-version <localnetVersion>", "Select a specific localnet version", localnetCompatibleVersion)
16
+ .option("--localnet-version <localnetVersion>", `Select a specific localnet version (minimum: ${localnetCompatibleVersion})`, localnetCompatibleVersion)
17
17
  .option("--ollama", "Enable Ollama container", false)
18
18
  .action(async (options: InitActionOptions) => {
19
19
  const initAction = new InitAction();
@@ -1,7 +1,7 @@
1
1
  import inquirer from "inquirer";
2
2
  import {DistinctQuestion} from "inquirer";
3
3
  import {ISimulatorService} from "../../lib/interfaces/ISimulatorService";
4
- import {AI_PROVIDERS_CONFIG, AiProviders} from "../../lib/config/simulator";
4
+ import {AI_PROVIDERS_CONFIG, AiProviders, localnetCompatibleVersion} from "../../lib/config/simulator";
5
5
  import {OllamaAction} from "../update/ollama";
6
6
  import {BaseAction} from "../../lib/actions/BaseAction";
7
7
  import {SimulatorService} from "../../lib/services/simulator";
@@ -46,6 +46,11 @@ export class InitAction extends BaseAction {
46
46
  let localnetVersion = options.localnetVersion;
47
47
  if (localnetVersion !== "latest") {
48
48
  localnetVersion = this.simulatorService.normalizeLocalnetVersion(localnetVersion);
49
+
50
+ if (this.simulatorService.compareVersions(localnetVersion, localnetCompatibleVersion) < 0) {
51
+ this.failSpinner(`Localnet version ${localnetVersion} is not supported. Minimum required version is ${localnetCompatibleVersion}. Please use a newer version.`);
52
+ return;
53
+ }
49
54
  }
50
55
 
51
56
  this.startSpinner("Checking CLI version...");
@@ -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;
@@ -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));
@@ -1,4 +1,4 @@
1
- export const localnetCompatibleVersion = "v0.55.0";
1
+ export const localnetCompatibleVersion = "v0.65.0";
2
2
  export const DEFAULT_JSON_RPC_URL = "http://localhost:4000/api";
3
3
  export const CONTAINERS_NAME_PREFIX = "/genlayer-";
4
4
  export const IMAGES_NAME_PREFIX = "yeagerai";
@@ -19,6 +19,7 @@ export interface ISimulatorService {
19
19
  cleanDatabase(): Promise<boolean>;
20
20
  addConfigToEnvFile(newConfig: Record<string, string>): void;
21
21
  normalizeLocalnetVersion(version: string): string;
22
+ compareVersions(version1: string, version2: string): number;
22
23
  isLocalnetRunning(): Promise<boolean>;
23
24
  }
24
25
 
@@ -0,0 +1,5 @@
1
+ export interface KeystoreData {
2
+ version: number;
3
+ encrypted: string;
4
+ address: string;
5
+ }
@@ -305,6 +305,24 @@ export class SimulatorService implements ISimulatorService {
305
305
  return version
306
306
  }
307
307
 
308
+ public compareVersions(version1: string, version2: string): number {
309
+ const v1 = version1.replace(/^v/, '');
310
+ const v2 = version2.replace(/^v/, '');
311
+
312
+ const parts1 = v1.split('-')[0].split('.').map(Number);
313
+ const parts2 = v2.split('-')[0].split('.').map(Number);
314
+
315
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
316
+ const part1 = parts1[i] || 0;
317
+ const part2 = parts2[i] || 0;
318
+
319
+ if (part1 < part2) return -1;
320
+ if (part1 > part2) return 1;
321
+ }
322
+
323
+ return 0;
324
+ }
325
+
308
326
  public async isLocalnetRunning(): Promise<boolean> {
309
327
  const genlayerContainers = await this.getGenlayerContainers();
310
328
  const runningContainers = genlayerContainers.filter(container => container.State === "running");