genlayer 0.24.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 +6 -0
- package/dist/index.js +102 -17
- package/package.json +1 -1
- package/src/commands/contracts/call.ts +1 -1
- package/src/lib/actions/BaseAction.ts +43 -14
- package/src/lib/config/ConfigFileManager.ts +78 -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/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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.24.0 (2025-07-16)
|
|
4
10
|
|
|
5
11
|
### 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 {
|
|
@@ -42418,7 +42503,7 @@ var CallAction = class extends BaseAction {
|
|
|
42418
42503
|
args,
|
|
42419
42504
|
rpc
|
|
42420
42505
|
}) {
|
|
42421
|
-
const client = await this.getClient(rpc);
|
|
42506
|
+
const client = await this.getClient(rpc, true);
|
|
42422
42507
|
await client.initializeConsensusSmartContract();
|
|
42423
42508
|
this.startSpinner(`Calling method ${method} on contract at ${contractAddress}...`);
|
|
42424
42509
|
try {
|
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
|
|
|
@@ -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
|
}
|
|
@@ -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(() => {});
|
|
@@ -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
|
});
|