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 +2 -4
- package/CHANGELOG.md +12 -0
- package/dist/index.js +110 -57
- package/docker-compose.yml +10 -27
- package/package.json +1 -1
- package/src/commands/general/index.ts +1 -1
- package/src/commands/general/init.ts +6 -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/config/simulator.ts +1 -1
- package/src/lib/interfaces/ISimulatorService.ts +1 -0
- package/src/lib/interfaces/KeystoreData.ts +5 -0
- package/src/lib/services/simulator.ts +18 -0
- package/tests/actions/create.test.ts +15 -37
- package/tests/actions/init.test.ts +48 -4
- package/tests/commands/init.test.ts +10 -0
- package/tests/libs/baseAction.test.ts +229 -24
- package/tests/services/simulator.test.ts +60 -0
- package/src/lib/accounts/KeypairManager.ts +0 -43
- package/tests/libs/accounts/KeypairManager.test.ts +0 -110
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
|
-
|
|
24
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
40965
|
-
|
|
40966
|
-
|
|
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>",
|
|
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
|
|
42129
|
-
this.
|
|
42130
|
-
this.succeedSpinner(`
|
|
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
|
|
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
|
|
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
|
}
|
package/docker-compose.yml
CHANGED
|
@@ -32,9 +32,8 @@ services:
|
|
|
32
32
|
- FLASK_SERVER_PORT=${RPCPORT:-5000}
|
|
33
33
|
- PYTHONUNBUFFERED=1
|
|
34
34
|
- RPCDEBUGPORT=${RPCDEBUGPORT:-5001}
|
|
35
|
-
-
|
|
36
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
69
|
+
|
|
70
|
+
webdriver:
|
|
71
|
+
image: yeagerai/genlayer-genvm-webdriver:0.0.3
|
|
72
72
|
shm_size: 2gb
|
|
73
73
|
environment:
|
|
74
|
-
-
|
|
75
|
-
- WEBREQUESTSELENIUMPORT=${WEBREQUESTSELENIUMPORT:-4444}
|
|
76
|
-
- PYTHONUNBUFFERED=1
|
|
77
|
-
- WEBREQUESTPORT=${WEBREQUESTPORT}
|
|
78
|
-
- WEBREQUESTHOST=${WEBREQUESTHOST}
|
|
79
|
-
- WEBREQUESTPROTOCOL=${WEBREQUESTPROTOCOL}
|
|
74
|
+
- PORT=${WEBDRIVERPORT:-4444}
|
|
80
75
|
expose:
|
|
81
|
-
- "${
|
|
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
|
@@ -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>",
|
|
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
|
|
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,4 +1,4 @@
|
|
|
1
|
-
export const localnetCompatibleVersion = "v0.
|
|
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
|
|
|
@@ -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");
|