genlayer 0.23.0 → 0.25.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 +12 -0
- package/dist/index.js +115 -19
- package/docker-compose.yml +6 -1
- package/package.json +1 -1
- package/src/commands/contracts/call.ts +1 -1
- package/src/commands/general/init.ts +5 -4
- package/src/lib/actions/BaseAction.ts +43 -14
- package/src/lib/config/ConfigFileManager.ts +78 -0
- package/src/lib/interfaces/ISimulatorService.ts +1 -0
- package/src/lib/services/simulator.ts +12 -0
- package/tests/actions/appeal.test.ts +1 -1
- package/tests/actions/call.test.ts +1 -1
- package/tests/actions/deploy.test.ts +1 -1
- package/tests/actions/init.test.ts +7 -2
- package/tests/actions/receipt.test.ts +1 -1
- package/tests/actions/write.test.ts +1 -1
- package/tests/libs/baseAction.test.ts +51 -7
- package/tests/libs/configFileManager.test.ts +210 -8
- package/tests/services/simulator.test.ts +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.0 (2025-07-23)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* Add Temporary Key Caching System with Read-Only Client Support ([#241](https://github.com/yeagerai/genlayer-cli/issues/241)) ([89d57f9](https://github.com/yeagerai/genlayer-cli/commit/89d57f9e35a331ef22edcdbd17384807c6fa18ac))
|
|
8
|
+
|
|
9
|
+
## 0.24.0 (2025-07-16)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* Add Docker Volume Cleanup to Init Flow ([#240](https://github.com/yeagerai/genlayer-cli/issues/240)) ([e61b82c](https://github.com/yeagerai/genlayer-cli/commit/e61b82c3d31aa71860eccad510141cd3f932e4aa))
|
|
14
|
+
|
|
3
15
|
## 0.23.0 (2025-07-15)
|
|
4
16
|
|
|
5
17
|
### Features
|
package/dist/index.js
CHANGED
|
@@ -17856,7 +17856,7 @@ var require_semver2 = __commonJS({
|
|
|
17856
17856
|
import { program } from "commander";
|
|
17857
17857
|
|
|
17858
17858
|
// package.json
|
|
17859
|
-
var version = "0.
|
|
17859
|
+
var version = "0.25.0";
|
|
17860
17860
|
var package_default = {
|
|
17861
17861
|
name: "genlayer",
|
|
17862
17862
|
version,
|
|
@@ -19372,14 +19372,18 @@ var rpcClient = new JsonRpcClient(DEFAULT_JSON_RPC_URL);
|
|
|
19372
19372
|
import path from "path";
|
|
19373
19373
|
import os from "os";
|
|
19374
19374
|
import fs2 from "fs";
|
|
19375
|
-
var
|
|
19375
|
+
var _ConfigFileManager = class _ConfigFileManager {
|
|
19376
|
+
// 5 minutes
|
|
19376
19377
|
constructor(baseFolder = ".genlayer/", configFileName = "genlayer-config.json") {
|
|
19377
19378
|
__publicField(this, "folderPath");
|
|
19378
19379
|
__publicField(this, "configFilePath");
|
|
19380
|
+
__publicField(this, "tempFolderPath");
|
|
19379
19381
|
this.folderPath = path.resolve(os.homedir(), baseFolder);
|
|
19380
19382
|
this.configFilePath = path.resolve(this.folderPath, configFileName);
|
|
19383
|
+
this.tempFolderPath = path.resolve(os.tmpdir(), "genlayer-temp");
|
|
19381
19384
|
this.ensureFolderExists();
|
|
19382
19385
|
this.ensureConfigFileExists();
|
|
19386
|
+
this.ensureTempFolderExists();
|
|
19383
19387
|
}
|
|
19384
19388
|
ensureFolderExists() {
|
|
19385
19389
|
if (!fs2.existsSync(this.folderPath)) {
|
|
@@ -19391,6 +19395,11 @@ var ConfigFileManager = class {
|
|
|
19391
19395
|
fs2.writeFileSync(this.configFilePath, JSON.stringify({}, null, 2));
|
|
19392
19396
|
}
|
|
19393
19397
|
}
|
|
19398
|
+
ensureTempFolderExists() {
|
|
19399
|
+
if (!fs2.existsSync(this.tempFolderPath)) {
|
|
19400
|
+
fs2.mkdirSync(this.tempFolderPath, { recursive: true, mode: 448 });
|
|
19401
|
+
}
|
|
19402
|
+
}
|
|
19394
19403
|
getFolderPath() {
|
|
19395
19404
|
return this.folderPath;
|
|
19396
19405
|
}
|
|
@@ -19410,7 +19419,59 @@ var ConfigFileManager = class {
|
|
|
19410
19419
|
config[key] = value;
|
|
19411
19420
|
fs2.writeFileSync(this.configFilePath, JSON.stringify(config, null, 2));
|
|
19412
19421
|
}
|
|
19422
|
+
storeTempFile(fileName, content) {
|
|
19423
|
+
this.ensureTempFolderExists();
|
|
19424
|
+
const filePath = path.resolve(this.tempFolderPath, fileName);
|
|
19425
|
+
const tempData = {
|
|
19426
|
+
content,
|
|
19427
|
+
timestamp: Date.now()
|
|
19428
|
+
};
|
|
19429
|
+
fs2.writeFileSync(filePath, JSON.stringify(tempData), { mode: 384 });
|
|
19430
|
+
}
|
|
19431
|
+
getTempFile(fileName) {
|
|
19432
|
+
const filePath = path.resolve(this.tempFolderPath, fileName);
|
|
19433
|
+
if (!fs2.existsSync(filePath)) {
|
|
19434
|
+
return null;
|
|
19435
|
+
}
|
|
19436
|
+
const fileContent = fs2.readFileSync(filePath, "utf-8");
|
|
19437
|
+
const tempData = JSON.parse(fileContent);
|
|
19438
|
+
if (Date.now() - tempData.timestamp > _ConfigFileManager.TEMP_FILE_EXPIRATION_MS) {
|
|
19439
|
+
this.clearTempFile(fileName);
|
|
19440
|
+
return null;
|
|
19441
|
+
}
|
|
19442
|
+
return tempData.content;
|
|
19443
|
+
}
|
|
19444
|
+
hasTempFile(fileName) {
|
|
19445
|
+
return this.getTempFile(fileName) !== null;
|
|
19446
|
+
}
|
|
19447
|
+
clearTempFile(fileName) {
|
|
19448
|
+
const filePath = path.resolve(this.tempFolderPath, fileName);
|
|
19449
|
+
if (fs2.existsSync(filePath)) {
|
|
19450
|
+
fs2.unlinkSync(filePath);
|
|
19451
|
+
}
|
|
19452
|
+
}
|
|
19453
|
+
cleanupExpiredTempFiles() {
|
|
19454
|
+
if (!fs2.existsSync(this.tempFolderPath)) {
|
|
19455
|
+
return;
|
|
19456
|
+
}
|
|
19457
|
+
const files = fs2.readdirSync(this.tempFolderPath);
|
|
19458
|
+
const now = Date.now();
|
|
19459
|
+
for (const file of files) {
|
|
19460
|
+
const filePath = path.resolve(this.tempFolderPath, file);
|
|
19461
|
+
try {
|
|
19462
|
+
const fileContent = fs2.readFileSync(filePath, "utf-8");
|
|
19463
|
+
const tempData = JSON.parse(fileContent);
|
|
19464
|
+
if (now - tempData.timestamp > _ConfigFileManager.TEMP_FILE_EXPIRATION_MS) {
|
|
19465
|
+
fs2.unlinkSync(filePath);
|
|
19466
|
+
}
|
|
19467
|
+
} catch (error) {
|
|
19468
|
+
fs2.unlinkSync(filePath);
|
|
19469
|
+
}
|
|
19470
|
+
}
|
|
19471
|
+
}
|
|
19413
19472
|
};
|
|
19473
|
+
__publicField(_ConfigFileManager, "TEMP_FILE_EXPIRATION_MS", 5 * 60 * 1e3);
|
|
19474
|
+
var ConfigFileManager = _ConfigFileManager;
|
|
19414
19475
|
|
|
19415
19476
|
// node_modules/ora/index.js
|
|
19416
19477
|
import process9 from "node:process";
|
|
@@ -40952,22 +41013,24 @@ var createAccount = (accountPrivateKey) => {
|
|
|
40952
41013
|
// src/lib/actions/BaseAction.ts
|
|
40953
41014
|
import { ethers } from "ethers";
|
|
40954
41015
|
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
40955
|
-
var
|
|
41016
|
+
var _BaseAction = class _BaseAction extends ConfigFileManager {
|
|
40956
41017
|
constructor() {
|
|
40957
41018
|
super();
|
|
40958
41019
|
__publicField(this, "spinner");
|
|
40959
41020
|
__publicField(this, "_genlayerClient", null);
|
|
40960
41021
|
this.spinner = ora({ text: "", spinner: "dots" });
|
|
41022
|
+
this.cleanupExpiredTempFiles();
|
|
40961
41023
|
}
|
|
40962
41024
|
async decryptKeystore(keystoreData, attempt = 1) {
|
|
40963
41025
|
try {
|
|
40964
|
-
const message = attempt === 1 ? "Enter password to decrypt keystore:" : `Invalid password. Attempt ${attempt}
|
|
41026
|
+
const message = attempt === 1 ? "Enter password to decrypt keystore:" : `Invalid password. Attempt ${attempt}/${_BaseAction.MAX_PASSWORD_ATTEMPTS} - Enter password to decrypt keystore:`;
|
|
40965
41027
|
const password = await this.promptPassword(message);
|
|
40966
41028
|
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password);
|
|
41029
|
+
this.storeTempFile(_BaseAction.TEMP_KEY_FILENAME, wallet.privateKey);
|
|
40967
41030
|
return wallet.privateKey;
|
|
40968
41031
|
} catch (error) {
|
|
40969
|
-
if (attempt >=
|
|
40970
|
-
this.failSpinner(
|
|
41032
|
+
if (attempt >= _BaseAction.MAX_PASSWORD_ATTEMPTS) {
|
|
41033
|
+
this.failSpinner(`Maximum password attempts exceeded (${_BaseAction.MAX_PASSWORD_ATTEMPTS}/${_BaseAction.MAX_PASSWORD_ATTEMPTS}).`);
|
|
40971
41034
|
process.exit(1);
|
|
40972
41035
|
}
|
|
40973
41036
|
return await this.decryptKeystore(keystoreData, attempt + 1);
|
|
@@ -40984,31 +41047,47 @@ var BaseAction = class extends ConfigFileManager {
|
|
|
40984
41047
|
}
|
|
40985
41048
|
return inspect(data, { depth: null, colors: false });
|
|
40986
41049
|
}
|
|
40987
|
-
async getClient(rpcUrl) {
|
|
41050
|
+
async getClient(rpcUrl, readOnly = false) {
|
|
40988
41051
|
if (!this._genlayerClient) {
|
|
40989
41052
|
const networkConfig = this.getConfig().network;
|
|
40990
41053
|
const network = networkConfig ? JSON.parse(networkConfig) : localnet;
|
|
41054
|
+
const account = await this.getAccount(readOnly);
|
|
40991
41055
|
this._genlayerClient = createClient2({
|
|
40992
41056
|
chain: network,
|
|
40993
41057
|
endpoint: rpcUrl,
|
|
40994
|
-
account
|
|
41058
|
+
account
|
|
40995
41059
|
});
|
|
40996
41060
|
}
|
|
40997
41061
|
return this._genlayerClient;
|
|
40998
41062
|
}
|
|
40999
|
-
async
|
|
41000
|
-
|
|
41063
|
+
async getAccount(readOnly = false) {
|
|
41064
|
+
let keypairPath = this.getConfigByKey("keyPairPath");
|
|
41065
|
+
let decryptedPrivateKey;
|
|
41066
|
+
let keystoreData;
|
|
41001
41067
|
if (!keypairPath || !existsSync(keypairPath)) {
|
|
41002
41068
|
await this.confirmPrompt("Keypair file not found. Would you like to create a new keypair?");
|
|
41003
|
-
|
|
41069
|
+
decryptedPrivateKey = await this.createKeypair(_BaseAction.DEFAULT_KEYSTORE_PATH, false);
|
|
41070
|
+
keypairPath = this.getConfigByKey("keyPairPath");
|
|
41004
41071
|
}
|
|
41005
|
-
|
|
41072
|
+
keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
41006
41073
|
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
41007
41074
|
this.failSpinner("Invalid keystore format. Expected encrypted keystore file.");
|
|
41008
41075
|
await this.confirmPrompt("Would you like to create a new keypair?");
|
|
41009
|
-
|
|
41076
|
+
decryptedPrivateKey = await this.createKeypair(_BaseAction.DEFAULT_KEYSTORE_PATH, true);
|
|
41077
|
+
keypairPath = this.getConfigByKey("keyPairPath");
|
|
41078
|
+
keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
41079
|
+
}
|
|
41080
|
+
if (readOnly) {
|
|
41081
|
+
return this.getAddress(keystoreData);
|
|
41010
41082
|
}
|
|
41011
|
-
|
|
41083
|
+
if (!decryptedPrivateKey) {
|
|
41084
|
+
const cachedKey = this.getTempFile(_BaseAction.TEMP_KEY_FILENAME);
|
|
41085
|
+
decryptedPrivateKey = cachedKey ? cachedKey : await this.decryptKeystore(keystoreData);
|
|
41086
|
+
}
|
|
41087
|
+
return createAccount(decryptedPrivateKey);
|
|
41088
|
+
}
|
|
41089
|
+
getAddress(keystoreData) {
|
|
41090
|
+
return keystoreData.address;
|
|
41012
41091
|
}
|
|
41013
41092
|
async createKeypair(outputPath, overwrite) {
|
|
41014
41093
|
const finalOutputPath = this.getFilePath(outputPath);
|
|
@@ -41024,8 +41103,8 @@ var BaseAction = class extends ConfigFileManager {
|
|
|
41024
41103
|
this.failSpinner("Passwords do not match");
|
|
41025
41104
|
process.exit(1);
|
|
41026
41105
|
}
|
|
41027
|
-
if (password.length <
|
|
41028
|
-
this.failSpinner(
|
|
41106
|
+
if (password.length < _BaseAction.MIN_PASSWORD_LENGTH) {
|
|
41107
|
+
this.failSpinner(`Password must be at least ${_BaseAction.MIN_PASSWORD_LENGTH} characters long`);
|
|
41029
41108
|
process.exit(1);
|
|
41030
41109
|
}
|
|
41031
41110
|
const encryptedJson = await wallet.encrypt(password);
|
|
@@ -41036,6 +41115,7 @@ var BaseAction = class extends ConfigFileManager {
|
|
|
41036
41115
|
};
|
|
41037
41116
|
writeFileSync(finalOutputPath, JSON.stringify(keystoreData, null, 2));
|
|
41038
41117
|
this.writeConfig("keyPairPath", finalOutputPath);
|
|
41118
|
+
this.clearTempFile(_BaseAction.TEMP_KEY_FILENAME);
|
|
41039
41119
|
return wallet.privateKey;
|
|
41040
41120
|
}
|
|
41041
41121
|
async promptPassword(message) {
|
|
@@ -41115,6 +41195,11 @@ ${message}`));
|
|
|
41115
41195
|
this.spinner.text = source_default.blue(message);
|
|
41116
41196
|
}
|
|
41117
41197
|
};
|
|
41198
|
+
__publicField(_BaseAction, "DEFAULT_KEYSTORE_PATH", "./keypair.json");
|
|
41199
|
+
__publicField(_BaseAction, "MAX_PASSWORD_ATTEMPTS", 3);
|
|
41200
|
+
__publicField(_BaseAction, "MIN_PASSWORD_LENGTH", 8);
|
|
41201
|
+
__publicField(_BaseAction, "TEMP_KEY_FILENAME", "decrypted_private_key");
|
|
41202
|
+
var BaseAction = _BaseAction;
|
|
41118
41203
|
|
|
41119
41204
|
// src/commands/update/ollama.ts
|
|
41120
41205
|
var OllamaAction = class extends BaseAction {
|
|
@@ -41939,6 +42024,16 @@ Run npm install -g genlayer to update
|
|
|
41939
42024
|
await image.remove({ force: true });
|
|
41940
42025
|
}
|
|
41941
42026
|
}
|
|
42027
|
+
async resetDockerVolumes() {
|
|
42028
|
+
const volumes = await this.docker.listVolumes();
|
|
42029
|
+
const genlayerVolumes = volumes.Volumes?.filter(
|
|
42030
|
+
(volume) => volume.Name.startsWith("genlayer_")
|
|
42031
|
+
) || [];
|
|
42032
|
+
for (const volumeInfo of genlayerVolumes) {
|
|
42033
|
+
const volume = this.docker.getVolume(volumeInfo.Name);
|
|
42034
|
+
await volume.remove({ force: true });
|
|
42035
|
+
}
|
|
42036
|
+
}
|
|
41942
42037
|
async cleanDatabase() {
|
|
41943
42038
|
try {
|
|
41944
42039
|
await rpcClient.request({ method: "sim_clearDbTables", params: [["current_state", "transactions"]] });
|
|
@@ -42038,12 +42133,13 @@ var InitAction = class extends BaseAction {
|
|
|
42038
42133
|
return;
|
|
42039
42134
|
}
|
|
42040
42135
|
this.stopSpinner();
|
|
42041
|
-
const confirmMessage = isRunning ? `GenLayer Localnet is already running and this command is going to reset GenLayer docker images and
|
|
42136
|
+
const confirmMessage = isRunning ? `GenLayer Localnet is already running and this command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?` : `This command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?`;
|
|
42042
42137
|
await this.confirmPrompt(confirmMessage);
|
|
42043
42138
|
this.logInfo(`Initializing GenLayer CLI with ${options.numValidators} validators`);
|
|
42044
|
-
this.startSpinner("Resetting Docker containers and
|
|
42139
|
+
this.startSpinner("Resetting Docker containers, images, and volumes...");
|
|
42045
42140
|
await this.simulatorService.resetDockerContainers();
|
|
42046
42141
|
await this.simulatorService.resetDockerImages();
|
|
42142
|
+
await this.simulatorService.resetDockerVolumes();
|
|
42047
42143
|
this.stopSpinner();
|
|
42048
42144
|
const llmQuestions = [
|
|
42049
42145
|
{
|
|
@@ -42407,7 +42503,7 @@ var CallAction = class extends BaseAction {
|
|
|
42407
42503
|
args,
|
|
42408
42504
|
rpc
|
|
42409
42505
|
}) {
|
|
42410
|
-
const client = await this.getClient(rpc);
|
|
42506
|
+
const client = await this.getClient(rpc, true);
|
|
42411
42507
|
await client.initializeConsensusSmartContract();
|
|
42412
42508
|
this.startSpinner(`Calling method ${method} on contract at ${contractAddress}...`);
|
|
42413
42509
|
try {
|
package/docker-compose.yml
CHANGED
|
@@ -143,7 +143,12 @@ services:
|
|
|
143
143
|
volumes:
|
|
144
144
|
- hardhat_artifacts:/app/artifacts
|
|
145
145
|
- hardhat_deployments:/app/deployments
|
|
146
|
+
- hardhat_cache:/app/cache
|
|
147
|
+
- hardhat_snapshots:/app/snapshots
|
|
146
148
|
|
|
147
149
|
volumes:
|
|
148
150
|
hardhat_artifacts:
|
|
149
|
-
hardhat_deployments:
|
|
151
|
+
hardhat_deployments:
|
|
152
|
+
hardhat_cache:
|
|
153
|
+
hardhat_snapshots:
|
|
154
|
+
|
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@ export class CallAction extends BaseAction {
|
|
|
21
21
|
args: any[];
|
|
22
22
|
rpc?: string;
|
|
23
23
|
}): Promise<void> {
|
|
24
|
-
const client = await this.getClient(rpc);
|
|
24
|
+
const client = await this.getClient(rpc, true);
|
|
25
25
|
await client.initializeConsensusSmartContract();
|
|
26
26
|
this.startSpinner(`Calling method ${method} on contract at ${contractAddress}...`);
|
|
27
27
|
|
|
@@ -76,17 +76,18 @@ export class InitAction extends BaseAction {
|
|
|
76
76
|
this.stopSpinner();
|
|
77
77
|
|
|
78
78
|
const confirmMessage = isRunning
|
|
79
|
-
? `GenLayer Localnet is already running and this command is going to reset GenLayer docker images and
|
|
80
|
-
: `This command is going to reset GenLayer docker images and
|
|
79
|
+
? `GenLayer Localnet is already running and this command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?`
|
|
80
|
+
: `This command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?`;
|
|
81
81
|
|
|
82
82
|
await this.confirmPrompt(confirmMessage);
|
|
83
83
|
|
|
84
84
|
this.logInfo(`Initializing GenLayer CLI with ${options.numValidators} validators`);
|
|
85
85
|
|
|
86
|
-
// Reset Docker containers and
|
|
87
|
-
this.startSpinner("Resetting Docker containers and
|
|
86
|
+
// Reset Docker containers, images, and volumes
|
|
87
|
+
this.startSpinner("Resetting Docker containers, images, and volumes...");
|
|
88
88
|
await this.simulatorService.resetDockerContainers();
|
|
89
89
|
await this.simulatorService.resetDockerImages();
|
|
90
|
+
await this.simulatorService.resetDockerVolumes();
|
|
90
91
|
this.stopSpinner();
|
|
91
92
|
|
|
92
93
|
const llmQuestions: DistinctQuestion[] = [
|
|
@@ -5,31 +5,40 @@ import inquirer from "inquirer";
|
|
|
5
5
|
import { inspect } from "util";
|
|
6
6
|
import {createClient, createAccount} from "genlayer-js";
|
|
7
7
|
import {localnet} from "genlayer-js/chains";
|
|
8
|
-
import type {GenLayerClient, GenLayerChain} from "genlayer-js/types";
|
|
8
|
+
import type {GenLayerClient, GenLayerChain, Hash, Address, Account} from "genlayer-js/types";
|
|
9
9
|
import { ethers } from "ethers";
|
|
10
10
|
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
11
11
|
import { KeystoreData } from "../interfaces/KeystoreData";
|
|
12
12
|
|
|
13
13
|
export class BaseAction extends ConfigFileManager {
|
|
14
|
+
private static readonly DEFAULT_KEYSTORE_PATH = "./keypair.json";
|
|
15
|
+
private static readonly MAX_PASSWORD_ATTEMPTS = 3;
|
|
16
|
+
private static readonly MIN_PASSWORD_LENGTH = 8;
|
|
17
|
+
private static readonly TEMP_KEY_FILENAME = "decrypted_private_key";
|
|
18
|
+
|
|
14
19
|
private spinner: Ora;
|
|
15
20
|
private _genlayerClient: GenLayerClient<GenLayerChain> | null = null;
|
|
16
21
|
|
|
17
22
|
constructor() {
|
|
18
23
|
super();
|
|
19
24
|
this.spinner = ora({text: "", spinner: "dots"});
|
|
25
|
+
this.cleanupExpiredTempFiles();
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
private async decryptKeystore(keystoreData: KeystoreData, attempt: number = 1): Promise<string> {
|
|
23
29
|
try {
|
|
24
30
|
const message = attempt === 1
|
|
25
31
|
? "Enter password to decrypt keystore:"
|
|
26
|
-
: `Invalid password. Attempt ${attempt}
|
|
32
|
+
: `Invalid password. Attempt ${attempt}/${BaseAction.MAX_PASSWORD_ATTEMPTS} - Enter password to decrypt keystore:`;
|
|
27
33
|
const password = await this.promptPassword(message);
|
|
28
34
|
const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password);
|
|
35
|
+
|
|
36
|
+
this.storeTempFile(BaseAction.TEMP_KEY_FILENAME, wallet.privateKey);
|
|
37
|
+
|
|
29
38
|
return wallet.privateKey;
|
|
30
39
|
} catch (error) {
|
|
31
|
-
if (attempt >=
|
|
32
|
-
this.failSpinner(
|
|
40
|
+
if (attempt >= BaseAction.MAX_PASSWORD_ATTEMPTS) {
|
|
41
|
+
this.failSpinner(`Maximum password attempts exceeded (${BaseAction.MAX_PASSWORD_ATTEMPTS}/${BaseAction.MAX_PASSWORD_ATTEMPTS}).`);
|
|
33
42
|
process.exit(1);
|
|
34
43
|
}
|
|
35
44
|
return await this.decryptKeystore(keystoreData, attempt + 1);
|
|
@@ -52,36 +61,54 @@ export class BaseAction extends ConfigFileManager {
|
|
|
52
61
|
return inspect(data, { depth: null, colors: false });
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
protected async getClient(rpcUrl?: string): Promise<GenLayerClient<GenLayerChain>> {
|
|
64
|
+
protected async getClient(rpcUrl?: string, readOnly: boolean = false): Promise<GenLayerClient<GenLayerChain>> {
|
|
56
65
|
if (!this._genlayerClient) {
|
|
57
66
|
const networkConfig = this.getConfig().network;
|
|
58
67
|
const network = networkConfig ? JSON.parse(networkConfig) : localnet;
|
|
68
|
+
const account = await this.getAccount(readOnly);
|
|
59
69
|
this._genlayerClient = createClient({
|
|
60
70
|
chain: network,
|
|
61
71
|
endpoint: rpcUrl,
|
|
62
|
-
account:
|
|
72
|
+
account: account,
|
|
63
73
|
});
|
|
64
74
|
}
|
|
65
75
|
return this._genlayerClient;
|
|
66
76
|
}
|
|
67
77
|
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
private async getAccount(readOnly: boolean = false): Promise<Account | Address> {
|
|
79
|
+
let keypairPath = this.getConfigByKey("keyPairPath");
|
|
80
|
+
let decryptedPrivateKey;
|
|
81
|
+
let keystoreData;
|
|
70
82
|
|
|
71
83
|
if (!keypairPath || !existsSync(keypairPath)) {
|
|
72
84
|
await this.confirmPrompt("Keypair file not found. Would you like to create a new keypair?");
|
|
73
|
-
|
|
85
|
+
decryptedPrivateKey = await this.createKeypair(BaseAction.DEFAULT_KEYSTORE_PATH, false);
|
|
86
|
+
keypairPath = this.getConfigByKey("keyPairPath")!;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
|
-
|
|
89
|
+
keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
77
90
|
|
|
78
91
|
if (!this.isValidKeystoreFormat(keystoreData)) {
|
|
79
92
|
this.failSpinner("Invalid keystore format. Expected encrypted keystore file.");
|
|
80
93
|
await this.confirmPrompt("Would you like to create a new keypair?");
|
|
81
|
-
|
|
94
|
+
decryptedPrivateKey = await this.createKeypair(BaseAction.DEFAULT_KEYSTORE_PATH, true);
|
|
95
|
+
keypairPath = this.getConfigByKey("keyPairPath")!;
|
|
96
|
+
keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
82
97
|
}
|
|
83
98
|
|
|
84
|
-
|
|
99
|
+
if (readOnly) {
|
|
100
|
+
return this.getAddress(keystoreData);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!decryptedPrivateKey) {
|
|
104
|
+
const cachedKey = this.getTempFile(BaseAction.TEMP_KEY_FILENAME);
|
|
105
|
+
decryptedPrivateKey = cachedKey ? cachedKey : await this.decryptKeystore(keystoreData);
|
|
106
|
+
}
|
|
107
|
+
return createAccount(decryptedPrivateKey as Hash);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private getAddress(keystoreData: KeystoreData): Address {
|
|
111
|
+
return keystoreData.address as Address;
|
|
85
112
|
}
|
|
86
113
|
|
|
87
114
|
protected async createKeypair(outputPath: string, overwrite: boolean): Promise<string> {
|
|
@@ -103,8 +130,8 @@ export class BaseAction extends ConfigFileManager {
|
|
|
103
130
|
process.exit(1);
|
|
104
131
|
}
|
|
105
132
|
|
|
106
|
-
if (password.length <
|
|
107
|
-
this.failSpinner(
|
|
133
|
+
if (password.length < BaseAction.MIN_PASSWORD_LENGTH) {
|
|
134
|
+
this.failSpinner(`Password must be at least ${BaseAction.MIN_PASSWORD_LENGTH} characters long`);
|
|
108
135
|
process.exit(1);
|
|
109
136
|
}
|
|
110
137
|
|
|
@@ -119,6 +146,8 @@ export class BaseAction extends ConfigFileManager {
|
|
|
119
146
|
writeFileSync(finalOutputPath, JSON.stringify(keystoreData, null, 2));
|
|
120
147
|
this.writeConfig('keyPairPath', finalOutputPath);
|
|
121
148
|
|
|
149
|
+
this.clearTempFile(BaseAction.TEMP_KEY_FILENAME);
|
|
150
|
+
|
|
122
151
|
return wallet.privateKey;
|
|
123
152
|
}
|
|
124
153
|
|
|
@@ -2,15 +2,24 @@ import path from "path";
|
|
|
2
2
|
import os from "os";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
|
|
5
|
+
interface TempFileData {
|
|
6
|
+
content: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export class ConfigFileManager {
|
|
6
11
|
private folderPath: string;
|
|
7
12
|
private configFilePath: string;
|
|
13
|
+
private tempFolderPath: string;
|
|
14
|
+
private static readonly TEMP_FILE_EXPIRATION_MS = 5 * 60 * 1000; // 5 minutes
|
|
8
15
|
|
|
9
16
|
constructor(baseFolder: string = ".genlayer/", configFileName: string = "genlayer-config.json") {
|
|
10
17
|
this.folderPath = path.resolve(os.homedir(), baseFolder);
|
|
11
18
|
this.configFilePath = path.resolve(this.folderPath, configFileName);
|
|
19
|
+
this.tempFolderPath = path.resolve(os.tmpdir(), "genlayer-temp");
|
|
12
20
|
this.ensureFolderExists();
|
|
13
21
|
this.ensureConfigFileExists();
|
|
22
|
+
this.ensureTempFolderExists();
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
private ensureFolderExists(): void {
|
|
@@ -25,6 +34,12 @@ export class ConfigFileManager {
|
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
36
|
|
|
37
|
+
private ensureTempFolderExists(): void {
|
|
38
|
+
if (!fs.existsSync(this.tempFolderPath)) {
|
|
39
|
+
fs.mkdirSync(this.tempFolderPath, { recursive: true, mode: 0o700 }); // Owner-only access
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
getFolderPath(): string {
|
|
29
44
|
return this.folderPath;
|
|
30
45
|
}
|
|
@@ -48,4 +63,67 @@ export class ConfigFileManager {
|
|
|
48
63
|
config[key] = value;
|
|
49
64
|
fs.writeFileSync(this.configFilePath, JSON.stringify(config, null, 2));
|
|
50
65
|
}
|
|
66
|
+
|
|
67
|
+
storeTempFile(fileName: string, content: string): void {
|
|
68
|
+
this.ensureTempFolderExists();
|
|
69
|
+
const filePath = path.resolve(this.tempFolderPath, fileName);
|
|
70
|
+
const tempData: TempFileData = {
|
|
71
|
+
content,
|
|
72
|
+
timestamp: Date.now()
|
|
73
|
+
};
|
|
74
|
+
fs.writeFileSync(filePath, JSON.stringify(tempData), { mode: 0o600 }); // Owner-only access
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getTempFile(fileName: string): string | null {
|
|
78
|
+
const filePath = path.resolve(this.tempFolderPath, fileName);
|
|
79
|
+
|
|
80
|
+
if (!fs.existsSync(filePath)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
85
|
+
const tempData: TempFileData = JSON.parse(fileContent);
|
|
86
|
+
|
|
87
|
+
if (Date.now() - tempData.timestamp > ConfigFileManager.TEMP_FILE_EXPIRATION_MS) {
|
|
88
|
+
this.clearTempFile(fileName);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return tempData.content;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
hasTempFile(fileName: string): boolean {
|
|
96
|
+
return this.getTempFile(fileName) !== null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
clearTempFile(fileName: string): void {
|
|
100
|
+
const filePath = path.resolve(this.tempFolderPath, fileName);
|
|
101
|
+
if (fs.existsSync(filePath)) {
|
|
102
|
+
fs.unlinkSync(filePath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
cleanupExpiredTempFiles(): void {
|
|
107
|
+
if (!fs.existsSync(this.tempFolderPath)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const files = fs.readdirSync(this.tempFolderPath);
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
const filePath = path.resolve(this.tempFolderPath, file);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const fileContent = fs.readFileSync(filePath, "utf-8");
|
|
119
|
+
const tempData: TempFileData = JSON.parse(fileContent);
|
|
120
|
+
|
|
121
|
+
if (now - tempData.timestamp > ConfigFileManager.TEMP_FILE_EXPIRATION_MS) {
|
|
122
|
+
fs.unlinkSync(filePath);
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
fs.unlinkSync(filePath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
51
129
|
}
|
|
@@ -15,6 +15,7 @@ export interface ISimulatorService {
|
|
|
15
15
|
stopDockerContainers(): Promise<void>;
|
|
16
16
|
resetDockerContainers(): Promise<void>;
|
|
17
17
|
resetDockerImages(): Promise<void>;
|
|
18
|
+
resetDockerVolumes(): Promise<void>;
|
|
18
19
|
checkCliVersion(): Promise<void>;
|
|
19
20
|
cleanDatabase(): Promise<boolean>;
|
|
20
21
|
addConfigToEnvFile(newConfig: Record<string, string>): void;
|
|
@@ -278,6 +278,18 @@ export class SimulatorService implements ISimulatorService {
|
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
public async resetDockerVolumes(): Promise<void> {
|
|
282
|
+
const volumes = await this.docker.listVolumes();
|
|
283
|
+
const genlayerVolumes = volumes.Volumes?.filter(volume =>
|
|
284
|
+
volume.Name.startsWith('genlayer_')
|
|
285
|
+
) || [];
|
|
286
|
+
|
|
287
|
+
for (const volumeInfo of genlayerVolumes) {
|
|
288
|
+
const volume = this.docker.getVolume(volumeInfo.Name);
|
|
289
|
+
await volume.remove({force: true});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
281
293
|
public async cleanDatabase(): Promise<boolean> {
|
|
282
294
|
|
|
283
295
|
try {
|
|
@@ -21,7 +21,7 @@ describe("AppealAction", () => {
|
|
|
21
21
|
vi.mocked(createClient).mockReturnValue(mockClient as any);
|
|
22
22
|
vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any);
|
|
23
23
|
appealAction = new AppealAction();
|
|
24
|
-
vi.spyOn(appealAction as any, "
|
|
24
|
+
vi.spyOn(appealAction as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey});
|
|
25
25
|
|
|
26
26
|
vi.spyOn(appealAction as any, "startSpinner").mockImplementation(() => {});
|
|
27
27
|
vi.spyOn(appealAction as any, "succeedSpinner").mockImplementation(() => {});
|
|
@@ -21,7 +21,7 @@ describe("CallAction", () => {
|
|
|
21
21
|
vi.mocked(createClient).mockReturnValue(mockClient as any);
|
|
22
22
|
vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any);
|
|
23
23
|
callActions = new CallAction();
|
|
24
|
-
vi.spyOn(callActions as any, "
|
|
24
|
+
vi.spyOn(callActions as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey});
|
|
25
25
|
|
|
26
26
|
vi.spyOn(callActions as any, "startSpinner").mockImplementation(() => {});
|
|
27
27
|
vi.spyOn(callActions as any, "succeedSpinner").mockImplementation(() => {});
|
|
@@ -26,7 +26,7 @@ describe("DeployAction", () => {
|
|
|
26
26
|
vi.mocked(createClient).mockReturnValue(mockClient as any);
|
|
27
27
|
vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any);
|
|
28
28
|
deployer = new DeployAction();
|
|
29
|
-
vi.spyOn(deployer as any, "
|
|
29
|
+
vi.spyOn(deployer as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey});
|
|
30
30
|
vi.spyOn(deployer as any, "getConfig").mockReturnValue({});
|
|
31
31
|
|
|
32
32
|
vi.spyOn(deployer as any, "startSpinner").mockImplementation(() => {});
|
|
@@ -13,6 +13,7 @@ describe("InitAction", () => {
|
|
|
13
13
|
let checkVersionRequirementsSpy: ReturnType<typeof vi.spyOn>;
|
|
14
14
|
let resetDockerContainersSpy: ReturnType<typeof vi.spyOn>;
|
|
15
15
|
let resetDockerImagesSpy: ReturnType<typeof vi.spyOn>;
|
|
16
|
+
let resetDockerVolumesSpy: ReturnType<typeof vi.spyOn>;
|
|
16
17
|
let addConfigToEnvFileSpy: ReturnType<typeof vi.spyOn>;
|
|
17
18
|
let runSimulatorSpy: ReturnType<typeof vi.spyOn>;
|
|
18
19
|
let waitForSimulatorSpy: ReturnType<typeof vi.spyOn>;
|
|
@@ -57,6 +58,9 @@ describe("InitAction", () => {
|
|
|
57
58
|
resetDockerImagesSpy = vi
|
|
58
59
|
.spyOn(SimulatorService.prototype, "resetDockerImages")
|
|
59
60
|
.mockResolvedValue(undefined);
|
|
61
|
+
resetDockerVolumesSpy = vi
|
|
62
|
+
.spyOn(SimulatorService.prototype, "resetDockerVolumes")
|
|
63
|
+
.mockResolvedValue(undefined);
|
|
60
64
|
addConfigToEnvFileSpy = vi.spyOn(SimulatorService.prototype, "addConfigToEnvFile").mockResolvedValue();
|
|
61
65
|
runSimulatorSpy = vi
|
|
62
66
|
.spyOn(SimulatorService.prototype, "runSimulator")
|
|
@@ -98,7 +102,7 @@ describe("InitAction", () => {
|
|
|
98
102
|
await initAction.execute(defaultOptions);
|
|
99
103
|
|
|
100
104
|
expect(confirmPromptSpy).toHaveBeenCalledWith(
|
|
101
|
-
"GenLayer Localnet is already running and this command is going to reset GenLayer docker images and
|
|
105
|
+
"GenLayer Localnet is already running and this command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?",
|
|
102
106
|
);
|
|
103
107
|
});
|
|
104
108
|
|
|
@@ -114,7 +118,7 @@ describe("InitAction", () => {
|
|
|
114
118
|
await initAction.execute(defaultOptions);
|
|
115
119
|
|
|
116
120
|
expect(confirmPromptSpy).toHaveBeenCalledWith(
|
|
117
|
-
"This command is going to reset GenLayer docker images and
|
|
121
|
+
"This command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?",
|
|
118
122
|
);
|
|
119
123
|
});
|
|
120
124
|
|
|
@@ -130,6 +134,7 @@ describe("InitAction", () => {
|
|
|
130
134
|
expect(checkVersionRequirementsSpy).toHaveBeenCalled();
|
|
131
135
|
expect(resetDockerContainersSpy).toHaveBeenCalled();
|
|
132
136
|
expect(resetDockerImagesSpy).toHaveBeenCalled();
|
|
137
|
+
expect(resetDockerVolumesSpy).toHaveBeenCalled();
|
|
133
138
|
expect(addConfigToEnvFileSpy).toHaveBeenCalledWith({
|
|
134
139
|
OPENAIKEY: "API_KEY_OPENAI",
|
|
135
140
|
HEURISTAIAPIKEY: "API_KEY_HEURIST",
|
|
@@ -23,7 +23,7 @@ describe("ReceiptAction", () => {
|
|
|
23
23
|
vi.mocked(createClient).mockReturnValue(mockClient as any);
|
|
24
24
|
vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any);
|
|
25
25
|
receiptAction = new ReceiptAction();
|
|
26
|
-
vi.spyOn(receiptAction as any, "
|
|
26
|
+
vi.spyOn(receiptAction as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey});
|
|
27
27
|
|
|
28
28
|
vi.spyOn(receiptAction as any, "startSpinner").mockImplementation(() => {});
|
|
29
29
|
vi.spyOn(receiptAction as any, "succeedSpinner").mockImplementation(() => {});
|
|
@@ -19,7 +19,7 @@ describe("WriteAction", () => {
|
|
|
19
19
|
vi.mocked(createClient).mockReturnValue(mockClient as any);
|
|
20
20
|
vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any);
|
|
21
21
|
writeAction = new WriteAction();
|
|
22
|
-
vi.spyOn(writeAction as any, "
|
|
22
|
+
vi.spyOn(writeAction as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey});
|
|
23
23
|
|
|
24
24
|
vi.spyOn(writeAction as any, "startSpinner").mockImplementation(() => {});
|
|
25
25
|
vi.spyOn(writeAction as any, "succeedSpinner").mockImplementation(() => {});
|
|
@@ -6,11 +6,20 @@ import chalk from "chalk";
|
|
|
6
6
|
import {inspect} from "util";
|
|
7
7
|
import { ethers } from "ethers";
|
|
8
8
|
import { writeFileSync, existsSync, readFileSync } from "fs";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import { createAccount } from "genlayer-js";
|
|
9
12
|
|
|
10
13
|
vi.mock("inquirer");
|
|
11
14
|
vi.mock("ora");
|
|
12
15
|
vi.mock("fs");
|
|
16
|
+
vi.mock("os");
|
|
13
17
|
vi.mock("ethers");
|
|
18
|
+
vi.mock("genlayer-js", () => ({
|
|
19
|
+
createAccount: vi.fn(),
|
|
20
|
+
createClient: vi.fn(),
|
|
21
|
+
localnet: {}
|
|
22
|
+
}));
|
|
14
23
|
|
|
15
24
|
describe("BaseAction", () => {
|
|
16
25
|
let baseAction: BaseAction;
|
|
@@ -47,14 +56,25 @@ describe("BaseAction", () => {
|
|
|
47
56
|
} as unknown as Ora;
|
|
48
57
|
|
|
49
58
|
(ora as unknown as Mock).mockReturnValue(mockSpinner);
|
|
59
|
+
|
|
60
|
+
vi.mocked(os.homedir).mockReturnValue("/mocked/home");
|
|
61
|
+
vi.mocked(os.tmpdir).mockReturnValue("/mocked/tmp");
|
|
62
|
+
|
|
50
63
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
51
64
|
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockKeystoreData));
|
|
52
65
|
vi.mocked(writeFileSync).mockImplementation(() => {});
|
|
66
|
+
vi.mocked(fs.readdirSync).mockReturnValue([] as any);
|
|
67
|
+
vi.mocked(fs.mkdirSync).mockImplementation(() => "/mocked/path");
|
|
53
68
|
|
|
54
69
|
// Mock ethers
|
|
55
70
|
vi.mocked(ethers.Wallet.createRandom).mockReturnValue(mockWallet as any);
|
|
56
71
|
vi.mocked(ethers.Wallet.fromEncryptedJson).mockResolvedValue(mockWallet as any);
|
|
57
72
|
|
|
73
|
+
vi.mocked(createAccount).mockReturnValue({
|
|
74
|
+
privateKey: mockWallet.privateKey,
|
|
75
|
+
address: mockWallet.address
|
|
76
|
+
} as any);
|
|
77
|
+
|
|
58
78
|
baseAction = new BaseAction();
|
|
59
79
|
|
|
60
80
|
// Mock config methods
|
|
@@ -62,6 +82,12 @@ describe("BaseAction", () => {
|
|
|
62
82
|
vi.spyOn(baseAction as any, "getFilePath").mockImplementation(() => "./test-keypair.json");
|
|
63
83
|
vi.spyOn(baseAction as any, "writeConfig").mockImplementation(() => {});
|
|
64
84
|
vi.spyOn(baseAction as any, "getConfig").mockReturnValue({});
|
|
85
|
+
|
|
86
|
+
// Mock temp file methods
|
|
87
|
+
vi.spyOn(baseAction as any, "storeTempFile").mockImplementation(() => {});
|
|
88
|
+
vi.spyOn(baseAction as any, "getTempFile").mockReturnValue(null);
|
|
89
|
+
vi.spyOn(baseAction as any, "clearTempFile").mockImplementation(() => {});
|
|
90
|
+
vi.spyOn(baseAction as any, "cleanupExpiredTempFiles").mockImplementation(() => {});
|
|
65
91
|
});
|
|
66
92
|
|
|
67
93
|
afterEach(() => {
|
|
@@ -226,9 +252,17 @@ describe("BaseAction", () => {
|
|
|
226
252
|
test("should return private key when keystore exists and is valid", async () => {
|
|
227
253
|
vi.mocked(inquirer.prompt).mockResolvedValue({password: "correct-password"});
|
|
228
254
|
|
|
229
|
-
const
|
|
255
|
+
const account = await baseAction["getAccount"](false);
|
|
230
256
|
|
|
231
|
-
expect(
|
|
257
|
+
expect((account as any).privateKey).toBe(mockWallet.privateKey);
|
|
258
|
+
expect(existsSync).toHaveBeenCalledWith("./test-keypair.json");
|
|
259
|
+
expect(readFileSync).toHaveBeenCalledWith("./test-keypair.json", "utf-8");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("should return address when called with readOnly=true", async () => {
|
|
263
|
+
const address = await baseAction["getAccount"](true);
|
|
264
|
+
|
|
265
|
+
expect(address).toBe(mockKeystoreData.address);
|
|
232
266
|
expect(existsSync).toHaveBeenCalledWith("./test-keypair.json");
|
|
233
267
|
expect(readFileSync).toHaveBeenCalledWith("./test-keypair.json", "utf-8");
|
|
234
268
|
});
|
|
@@ -240,9 +274,9 @@ describe("BaseAction", () => {
|
|
|
240
274
|
.mockResolvedValueOnce({password: "new-password"}) // encrypt password
|
|
241
275
|
.mockResolvedValueOnce({password: "new-password"}); // confirm password
|
|
242
276
|
|
|
243
|
-
const
|
|
277
|
+
const account = await baseAction["getAccount"](false);
|
|
244
278
|
|
|
245
|
-
expect(
|
|
279
|
+
expect((account as any).privateKey).toBe(mockWallet.privateKey);
|
|
246
280
|
expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
|
|
247
281
|
expect.objectContaining({message: chalk.yellow("Keypair file not found. Would you like to create a new keypair?")})
|
|
248
282
|
]));
|
|
@@ -252,10 +286,20 @@ describe("BaseAction", () => {
|
|
|
252
286
|
vi.mocked(readFileSync).mockReturnValue('{"invalid": "format"}');
|
|
253
287
|
vi.mocked(inquirer.prompt).mockResolvedValue({confirmAction: false});
|
|
254
288
|
|
|
255
|
-
await expect(baseAction["
|
|
289
|
+
await expect(baseAction["getAccount"](false)).rejects.toThrow("process exited");
|
|
256
290
|
expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Invalid keystore format. Expected encrypted keystore file."));
|
|
257
291
|
});
|
|
258
292
|
|
|
293
|
+
test("should use cached key when available", async () => {
|
|
294
|
+
vi.spyOn(baseAction as any, "getTempFile").mockReturnValue(mockWallet.privateKey);
|
|
295
|
+
|
|
296
|
+
const account = await baseAction["getAccount"](false);
|
|
297
|
+
|
|
298
|
+
expect((account as any).privateKey).toBe(mockWallet.privateKey);
|
|
299
|
+
expect(baseAction["getTempFile"]).toHaveBeenCalledWith("decrypted_private_key");
|
|
300
|
+
expect(inquirer.prompt).not.toHaveBeenCalled();
|
|
301
|
+
});
|
|
302
|
+
|
|
259
303
|
test("should create new keypair when keystore format is invalid and user confirms", async () => {
|
|
260
304
|
vi.mocked(readFileSync).mockReturnValue('{"invalid": "format"}');
|
|
261
305
|
vi.mocked(inquirer.prompt)
|
|
@@ -263,9 +307,9 @@ describe("BaseAction", () => {
|
|
|
263
307
|
.mockResolvedValueOnce({password: "new-password"})
|
|
264
308
|
.mockResolvedValueOnce({password: "new-password"});
|
|
265
309
|
|
|
266
|
-
const
|
|
310
|
+
const account = await baseAction["getAccount"](false);
|
|
267
311
|
|
|
268
|
-
expect(
|
|
312
|
+
expect((account as any).privateKey).toBe(mockWallet.privateKey);
|
|
269
313
|
expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Invalid keystore format. Expected encrypted keystore file."));
|
|
270
314
|
expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([
|
|
271
315
|
expect.objectContaining({message: chalk.yellow("Would you like to create a new keypair?")})
|
|
@@ -11,12 +11,14 @@ vi.mock("os")
|
|
|
11
11
|
describe("ConfigFileManager", () => {
|
|
12
12
|
const mockFolderPath = "/mocked/home/.genlayer";
|
|
13
13
|
const mockConfigFilePath = `${mockFolderPath}/genlayer-config.json`;
|
|
14
|
+
const mockTempFolderPath = "/mocked/tmp/genlayer-temp";
|
|
14
15
|
|
|
15
16
|
let configFileManager: ConfigFileManager;
|
|
16
17
|
|
|
17
18
|
beforeEach(() => {
|
|
18
19
|
vi.clearAllMocks();
|
|
19
20
|
vi.mocked(os.homedir).mockReturnValue("/mocked/home");
|
|
21
|
+
vi.mocked(os.tmpdir).mockReturnValue("/mocked/tmp");
|
|
20
22
|
configFileManager = new ConfigFileManager();
|
|
21
23
|
});
|
|
22
24
|
|
|
@@ -99,15 +101,215 @@ describe("ConfigFileManager", () => {
|
|
|
99
101
|
});
|
|
100
102
|
|
|
101
103
|
test("writeConfig overwrites an existing key in the config file", () => {
|
|
102
|
-
const
|
|
103
|
-
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(
|
|
104
|
+
const existingConfig = { existingKey: "existingValue" };
|
|
105
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(existingConfig));
|
|
104
106
|
|
|
105
|
-
configFileManager.writeConfig("existingKey", "
|
|
107
|
+
configFileManager.writeConfig("existingKey", "newValue");
|
|
106
108
|
|
|
107
|
-
const expectedConfig = { existingKey: "
|
|
108
|
-
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
const expectedConfig = { existingKey: "newValue" };
|
|
110
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(mockConfigFilePath, JSON.stringify(expectedConfig, null, 2));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("Temp File Operations", () => {
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
vi.clearAllMocks();
|
|
116
|
+
vi.mocked(os.homedir).mockReturnValue("/mocked/home");
|
|
117
|
+
vi.mocked(os.tmpdir).mockReturnValue("/mocked/tmp");
|
|
118
|
+
configFileManager = new ConfigFileManager();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("storeTempFile creates temp folder and stores file with timestamp", () => {
|
|
122
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
123
|
+
const mockTimestamp = 1234567890;
|
|
124
|
+
vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp);
|
|
125
|
+
|
|
126
|
+
configFileManager.storeTempFile("test.json", "test content");
|
|
127
|
+
|
|
128
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp", { recursive: true, mode: 0o700 });
|
|
129
|
+
|
|
130
|
+
const expectedData = {
|
|
131
|
+
content: "test content",
|
|
132
|
+
timestamp: mockTimestamp
|
|
133
|
+
};
|
|
134
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
135
|
+
"/mocked/tmp/genlayer-temp/test.json",
|
|
136
|
+
JSON.stringify(expectedData),
|
|
137
|
+
{ mode: 0o600 }
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("storeTempFile does not create temp folder when it already exists", () => {
|
|
142
|
+
vi.clearAllMocks();
|
|
143
|
+
vi.mocked(fs.existsSync).mockImplementation((path) => {
|
|
144
|
+
if (path === "/mocked/home/.genlayer") return true;
|
|
145
|
+
if (path === "/mocked/tmp/genlayer-temp") return true;
|
|
146
|
+
return false;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const testConfigManager = new ConfigFileManager();
|
|
150
|
+
|
|
151
|
+
const mockTimestamp = 1234567890;
|
|
152
|
+
vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp);
|
|
153
|
+
|
|
154
|
+
testConfigManager.storeTempFile("test.json", "test content");
|
|
155
|
+
|
|
156
|
+
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
|
157
|
+
|
|
158
|
+
const expectedData = {
|
|
159
|
+
content: "test content",
|
|
160
|
+
timestamp: mockTimestamp
|
|
161
|
+
};
|
|
162
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
163
|
+
"/mocked/tmp/genlayer-temp/test.json",
|
|
164
|
+
JSON.stringify(expectedData),
|
|
165
|
+
{ mode: 0o600 }
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("getTempFile returns content when file exists and is not expired", () => {
|
|
170
|
+
const mockTimestamp = Date.now() - 60000; // 1 minute ago
|
|
171
|
+
const mockFileData = {
|
|
172
|
+
content: "cached content",
|
|
173
|
+
timestamp: mockTimestamp
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
177
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockFileData));
|
|
178
|
+
|
|
179
|
+
const result = configFileManager.getTempFile("test.json");
|
|
180
|
+
|
|
181
|
+
expect(result).toBe("cached content");
|
|
182
|
+
expect(fs.existsSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/test.json");
|
|
183
|
+
expect(fs.readFileSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/test.json", "utf-8");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("getTempFile returns null when file does not exist", () => {
|
|
187
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
188
|
+
|
|
189
|
+
const result = configFileManager.getTempFile("nonexistent.json");
|
|
190
|
+
|
|
191
|
+
expect(result).toBeNull();
|
|
192
|
+
expect(fs.existsSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/nonexistent.json");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("getTempFile returns null and clears file when expired", () => {
|
|
196
|
+
const expiredTimestamp = Date.now() - (6 * 60 * 1000); // 6 minutes ago (expired)
|
|
197
|
+
const mockFileData = {
|
|
198
|
+
content: "expired content",
|
|
199
|
+
timestamp: expiredTimestamp
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
203
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockFileData));
|
|
204
|
+
|
|
205
|
+
const result = configFileManager.getTempFile("expired.json");
|
|
206
|
+
|
|
207
|
+
expect(result).toBeNull();
|
|
208
|
+
expect(fs.unlinkSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/expired.json");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("hasTempFile returns true when valid temp file exists", () => {
|
|
212
|
+
const mockTimestamp = Date.now() - 60000; // 1 minute ago
|
|
213
|
+
const mockFileData = {
|
|
214
|
+
content: "test content",
|
|
215
|
+
timestamp: mockTimestamp
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
219
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockFileData));
|
|
220
|
+
|
|
221
|
+
const result = configFileManager.hasTempFile("test.json");
|
|
222
|
+
|
|
223
|
+
expect(result).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("hasTempFile returns false when temp file is expired", () => {
|
|
227
|
+
const expiredTimestamp = Date.now() - (6 * 60 * 1000); // 6 minutes ago
|
|
228
|
+
const mockFileData = {
|
|
229
|
+
content: "expired content",
|
|
230
|
+
timestamp: expiredTimestamp
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
234
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockFileData));
|
|
235
|
+
|
|
236
|
+
const result = configFileManager.hasTempFile("expired.json");
|
|
237
|
+
|
|
238
|
+
expect(result).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("clearTempFile removes specific temp file", () => {
|
|
242
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
243
|
+
|
|
244
|
+
configFileManager.clearTempFile("test.json");
|
|
245
|
+
|
|
246
|
+
expect(fs.existsSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/test.json");
|
|
247
|
+
expect(fs.unlinkSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/test.json");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("clearTempFile does nothing when file does not exist", () => {
|
|
251
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
252
|
+
|
|
253
|
+
configFileManager.clearTempFile("nonexistent.json");
|
|
254
|
+
|
|
255
|
+
expect(fs.existsSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/nonexistent.json");
|
|
256
|
+
expect(fs.unlinkSync).not.toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("cleanupExpiredTempFiles removes only expired files", () => {
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
const validTimestamp = now - 60000; // 1 minute ago (valid)
|
|
262
|
+
const expiredTimestamp = now - (6 * 60 * 1000); // 6 minutes ago (expired)
|
|
263
|
+
|
|
264
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
265
|
+
vi.mocked(fs.readdirSync).mockReturnValue(['valid.json', 'expired.json'] as any);
|
|
266
|
+
|
|
267
|
+
vi.mocked(fs.readFileSync)
|
|
268
|
+
.mockReturnValueOnce(JSON.stringify({content: "valid", timestamp: validTimestamp}))
|
|
269
|
+
.mockReturnValueOnce(JSON.stringify({content: "expired", timestamp: expiredTimestamp}));
|
|
270
|
+
|
|
271
|
+
vi.spyOn(Date, 'now').mockReturnValue(now);
|
|
272
|
+
|
|
273
|
+
configFileManager.cleanupExpiredTempFiles();
|
|
274
|
+
|
|
275
|
+
expect(fs.readdirSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp");
|
|
276
|
+
expect(fs.readFileSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/valid.json", "utf-8");
|
|
277
|
+
expect(fs.readFileSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/expired.json", "utf-8");
|
|
278
|
+
expect(fs.unlinkSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/expired.json");
|
|
279
|
+
expect(fs.unlinkSync).toHaveBeenCalledTimes(1); // Only expired file should be deleted
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("cleanupExpiredTempFiles removes corrupted files that cannot be parsed", () => {
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
const validTimestamp = now - 60000; // 1 minute ago (valid)
|
|
285
|
+
|
|
286
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
287
|
+
vi.mocked(fs.readdirSync).mockReturnValue(['valid.json', 'corrupted.json'] as any);
|
|
288
|
+
|
|
289
|
+
vi.mocked(fs.readFileSync)
|
|
290
|
+
.mockReturnValueOnce(JSON.stringify({content: "valid", timestamp: validTimestamp}))
|
|
291
|
+
.mockReturnValueOnce("invalid json content");
|
|
292
|
+
|
|
293
|
+
vi.spyOn(Date, 'now').mockReturnValue(now);
|
|
294
|
+
|
|
295
|
+
configFileManager.cleanupExpiredTempFiles();
|
|
296
|
+
|
|
297
|
+
expect(fs.readdirSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp");
|
|
298
|
+
expect(fs.readFileSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/valid.json", "utf-8");
|
|
299
|
+
expect(fs.readFileSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/corrupted.json", "utf-8");
|
|
300
|
+
|
|
301
|
+
// The corrupted file should be deleted due to JSON.parse error
|
|
302
|
+
expect(fs.unlinkSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/corrupted.json");
|
|
303
|
+
expect(fs.unlinkSync).toHaveBeenCalledTimes(1); // Only corrupted file should be deleted
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("cleanupExpiredTempFiles does nothing when temp folder does not exist", () => {
|
|
307
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
308
|
+
|
|
309
|
+
configFileManager.cleanupExpiredTempFiles();
|
|
310
|
+
|
|
311
|
+
expect(fs.existsSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp");
|
|
312
|
+
expect(fs.readdirSync).not.toHaveBeenCalled();
|
|
313
|
+
});
|
|
112
314
|
});
|
|
113
315
|
});
|
|
@@ -499,6 +499,46 @@ describe("SimulatorService - Docker Tests", () => {
|
|
|
499
499
|
expect(mockRemove).toHaveBeenCalledWith({force: true});
|
|
500
500
|
});
|
|
501
501
|
|
|
502
|
+
test("should remove Docker volumes with genlayer_ prefix", async () => {
|
|
503
|
+
const mockVolumes = {
|
|
504
|
+
Volumes: [
|
|
505
|
+
{ Name: "genlayer_volume1" },
|
|
506
|
+
{ Name: "genlayer_postgres" },
|
|
507
|
+
{ Name: "genlayer_data" },
|
|
508
|
+
{ Name: "unrelated_volume" },
|
|
509
|
+
{ Name: "another_volume" },
|
|
510
|
+
{ Name: "hardhat_artifacts" },
|
|
511
|
+
],
|
|
512
|
+
Warnings: [],
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const mockListVolumes = vi.mocked(Docker.prototype.listVolumes);
|
|
516
|
+
const mockGetVolume = vi.mocked(Docker.prototype.getVolume);
|
|
517
|
+
|
|
518
|
+
mockListVolumes.mockResolvedValue(mockVolumes as any);
|
|
519
|
+
|
|
520
|
+
const mockRemove = vi.fn().mockResolvedValue(undefined);
|
|
521
|
+
mockGetVolume.mockImplementation(
|
|
522
|
+
() =>
|
|
523
|
+
({
|
|
524
|
+
remove: mockRemove,
|
|
525
|
+
}) as unknown as Docker.Volume,
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const result = await simulatorService.resetDockerVolumes();
|
|
529
|
+
|
|
530
|
+
expect(result).toBe(undefined);
|
|
531
|
+
expect(mockListVolumes).toHaveBeenCalled();
|
|
532
|
+
expect(mockGetVolume).toHaveBeenCalledWith("genlayer_volume1");
|
|
533
|
+
expect(mockGetVolume).toHaveBeenCalledWith("genlayer_postgres");
|
|
534
|
+
expect(mockGetVolume).toHaveBeenCalledWith("genlayer_data");
|
|
535
|
+
expect(mockGetVolume).not.toHaveBeenCalledWith("unrelated_volume");
|
|
536
|
+
expect(mockGetVolume).not.toHaveBeenCalledWith("another_volume");
|
|
537
|
+
expect(mockGetVolume).not.toHaveBeenCalledWith("hardhat_artifacts");
|
|
538
|
+
expect(mockRemove).toHaveBeenCalledTimes(3);
|
|
539
|
+
expect(mockRemove).toHaveBeenCalledWith({force: true});
|
|
540
|
+
});
|
|
541
|
+
|
|
502
542
|
test("should execute command when docker is installed but is not available", async () => {
|
|
503
543
|
vi.mocked(checkCommand).mockResolvedValueOnce(undefined);
|
|
504
544
|
|