genlayer 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.0 (2025-08-07)
4
+
5
+ ### Features
6
+
7
+ * add schema command to inspect deployed contract schema ([#244](https://github.com/yeagerai/genlayer-cli/issues/244)) ([4d66a7b](https://github.com/yeagerai/genlayer-cli/commit/4d66a7b9b2ac813cf0d47f12fc82cbc06ed25b3c))
8
+
9
+ ## 0.25.0 (2025-07-23)
10
+
11
+ ### Features
12
+
13
+ * 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))
14
+
3
15
  ## 0.24.0 (2025-07-16)
4
16
 
5
17
  ### Features
package/README.md CHANGED
@@ -146,6 +146,7 @@ USAGE:
146
146
  genlayer deploy [options]
147
147
  genlayer call <contractAddress> <method> [options]
148
148
  genlayer write <contractAddress> <method> [options]
149
+ genlayer schema <contractAddress> [options]
149
150
 
150
151
  OPTIONS (deploy):
151
152
  --contract <contractPath> (Optional) Path to the intelligent contract to deploy
@@ -160,12 +161,16 @@ OPTIONS (write):
160
161
  --rpc <rpcUrl> RPC URL for the network
161
162
  --args <args...> Positional arguments for the method (space-separated, use quotes for multi-word arguments)
162
163
 
164
+ OPTIONS (schema):
165
+ --rpc <rpcUrl> RPC URL for the network
166
+
163
167
  EXAMPLES:
164
168
  genlayer deploy
165
169
  genlayer deploy --contract ./my_contract.gpy
166
170
  genlayer deploy --contract ./my_contract.gpy --args "arg1" "arg2" 123
167
171
  genlayer call 0x123456789abcdef greet --args "Hello World!"
168
172
  genlayer write 0x123456789abcdef updateValue --args 42
173
+ genlayer schema 0x123456789abcdef
169
174
  ```
170
175
 
171
176
  ##### Deploy Behavior
@@ -176,6 +181,9 @@ EXAMPLES:
176
181
  - `call` - Calls a contract method without sending a transaction or changing the state (read-only)
177
182
  - `write` - Sends a transaction to a contract method that modifies the state
178
183
 
184
+ ##### Schema
185
+ - `schema` - Retrieves the contract schema
186
+
179
187
  #### Keypair Management
180
188
 
181
189
  Generate and manage keypairs.
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.24.0";
17859
+ var version = "0.26.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 ConfigFileManager = class {
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 BaseAction = class extends ConfigFileManager {
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}/3 - Enter password to decrypt keystore:`;
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 >= 3) {
40970
- this.failSpinner("Maximum password attempts exceeded (3/3).");
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: createAccount(await this.getPrivateKey())
41058
+ account
40995
41059
  });
40996
41060
  }
40997
41061
  return this._genlayerClient;
40998
41062
  }
40999
- async getPrivateKey() {
41000
- const keypairPath = this.getConfigByKey("keyPairPath");
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
- return await this.createKeypair("./keypair.json", false);
41069
+ decryptedPrivateKey = await this.createKeypair(_BaseAction.DEFAULT_KEYSTORE_PATH, false);
41070
+ keypairPath = this.getConfigByKey("keyPairPath");
41004
41071
  }
41005
- const keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
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
- return await this.createKeypair("./keypair.json", true);
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
- return await this.decryptKeystore(keystoreData);
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 < 8) {
41028
- this.failSpinner("Password must be at least 8 characters long");
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 {
@@ -42468,6 +42553,27 @@ var WriteAction = class extends BaseAction {
42468
42553
  }
42469
42554
  };
42470
42555
 
42556
+ // src/commands/contracts/schema.ts
42557
+ var SchemaAction = class extends BaseAction {
42558
+ constructor() {
42559
+ super();
42560
+ }
42561
+ async schema({
42562
+ contractAddress,
42563
+ rpc
42564
+ }) {
42565
+ const client = await this.getClient(rpc, true);
42566
+ await client.initializeConsensusSmartContract();
42567
+ this.startSpinner(`Getting schema for contract at ${contractAddress}...`);
42568
+ try {
42569
+ const result = await client.getContractSchema(contractAddress);
42570
+ this.succeedSpinner("Contract schema retrieved successfully", result);
42571
+ } catch (error) {
42572
+ this.failSpinner("Error retrieving contract schema", error);
42573
+ }
42574
+ }
42575
+ };
42576
+
42471
42577
  // src/commands/contracts/index.ts
42472
42578
  function parseArg(value, previous = []) {
42473
42579
  if (value === "true") return [...previous, true];
@@ -42496,8 +42602,8 @@ function initializeContractsCommands(program2) {
42496
42602
  parseArg,
42497
42603
  []
42498
42604
  ).action(async (contractAddress, method, options) => {
42499
- const caller = new CallAction();
42500
- await caller.call({ contractAddress, method, ...options });
42605
+ const callAction = new CallAction();
42606
+ await callAction.call({ contractAddress, method, ...options });
42501
42607
  });
42502
42608
  program2.command("write <contractAddress> <method>").description("Sends a transaction to a contract method that modifies the state").option("--rpc <rpcUrl>", "RPC URL for the network").option(
42503
42609
  "--args <args...>",
@@ -42505,8 +42611,12 @@ function initializeContractsCommands(program2) {
42505
42611
  parseArg,
42506
42612
  []
42507
42613
  ).action(async (contractAddress, method, options) => {
42508
- const writer = new WriteAction();
42509
- await writer.write({ contractAddress, method, ...options });
42614
+ const writeAction = new WriteAction();
42615
+ await writeAction.write({ contractAddress, method, ...options });
42616
+ });
42617
+ program2.command("schema <contractAddress>").description("Get the schema for a deployed contract").option("--rpc <rpcUrl>", "RPC URL for the network").action(async (contractAddress, options) => {
42618
+ const schemaAction = new SchemaAction();
42619
+ await schemaAction.schema({ contractAddress, ...options });
42510
42620
  });
42511
42621
  return program2;
42512
42622
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genlayer",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "GenLayer Command Line Tool",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -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
 
@@ -2,6 +2,7 @@ import {Command} from "commander";
2
2
  import {DeployAction, DeployOptions, DeployScriptsOptions} from "./deploy";
3
3
  import {CallAction, CallOptions} from "./call";
4
4
  import {WriteAction, WriteOptions} from "./write";
5
+ import {SchemaAction, SchemaOptions} from "./schema";
5
6
 
6
7
  function parseArg(value: string, previous: any[] = []): any[] {
7
8
  if (value === "true") return [...previous, true];
@@ -43,8 +44,8 @@ export function initializeContractsCommands(program: Command) {
43
44
  [],
44
45
  )
45
46
  .action(async (contractAddress: string, method: string, options: CallOptions) => {
46
- const caller = new CallAction();
47
- await caller.call({contractAddress, method, ...options});
47
+ const callAction = new CallAction();
48
+ await callAction.call({contractAddress, method, ...options});
48
49
  });
49
50
 
50
51
  program
@@ -58,8 +59,17 @@ export function initializeContractsCommands(program: Command) {
58
59
  [],
59
60
  )
60
61
  .action(async (contractAddress: string, method: string, options: WriteOptions) => {
61
- const writer = new WriteAction();
62
- await writer.write({contractAddress, method, ...options});
62
+ const writeAction = new WriteAction();
63
+ await writeAction.write({contractAddress, method, ...options});
64
+ });
65
+
66
+ program
67
+ .command("schema <contractAddress>")
68
+ .description("Get the schema for a deployed contract")
69
+ .option("--rpc <rpcUrl>", "RPC URL for the network")
70
+ .action(async (contractAddress: string, options: SchemaOptions) => {
71
+ const schemaAction = new SchemaAction();
72
+ await schemaAction.schema({contractAddress, ...options});
63
73
  });
64
74
 
65
75
  return program;
@@ -0,0 +1,31 @@
1
+ import {BaseAction} from "../../lib/actions/BaseAction";
2
+ import type {Address} from "genlayer-js/types";
3
+
4
+ export interface SchemaOptions {
5
+ rpc?: string;
6
+ }
7
+
8
+ export class SchemaAction extends BaseAction {
9
+ constructor() {
10
+ super();
11
+ }
12
+
13
+ async schema({
14
+ contractAddress,
15
+ rpc,
16
+ }: {
17
+ contractAddress: string;
18
+ rpc?: string;
19
+ }): Promise<void> {
20
+ const client = await this.getClient(rpc, true);
21
+ await client.initializeConsensusSmartContract();
22
+ this.startSpinner(`Getting schema for contract at ${contractAddress}...`);
23
+
24
+ try {
25
+ const result = await client.getContractSchema(contractAddress as Address);
26
+ this.succeedSpinner("Contract schema retrieved successfully", result);
27
+ } catch (error) {
28
+ this.failSpinner("Error retrieving contract schema", error);
29
+ }
30
+ }
31
+ }
@@ -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}/3 - Enter password to decrypt keystore:`;
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 >= 3) {
32
- this.failSpinner("Maximum password attempts exceeded (3/3).");
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: createAccount((await this.getPrivateKey()) as any),
72
+ account: account,
63
73
  });
64
74
  }
65
75
  return this._genlayerClient;
66
76
  }
67
77
 
68
- protected async getPrivateKey(): Promise<string> {
69
- const keypairPath = this.getConfigByKey("keyPairPath");
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
- return await this.createKeypair("./keypair.json", false);
85
+ decryptedPrivateKey = await this.createKeypair(BaseAction.DEFAULT_KEYSTORE_PATH, false);
86
+ keypairPath = this.getConfigByKey("keyPairPath")!;
74
87
  }
75
88
 
76
- const keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8"));
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
- return await this.createKeypair("./keypair.json", true);
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
- return await this.decryptKeystore(keystoreData);
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 < 8) {
107
- this.failSpinner("Password must be at least 8 characters long");
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, "getPrivateKey").mockResolvedValue(mockPrivateKey);
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, "getPrivateKey").mockResolvedValue(mockPrivateKey);
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, "getPrivateKey").mockResolvedValue(mockPrivateKey);
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, "getPrivateKey").mockResolvedValue(mockPrivateKey);
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(() => {});
@@ -0,0 +1,94 @@
1
+ import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
2
+ import {createClient, createAccount} from "genlayer-js";
3
+ import {SchemaAction} from "../../src/commands/contracts/schema";
4
+
5
+ vi.mock("genlayer-js");
6
+
7
+ describe("SchemaAction", () => {
8
+ let schemaAction: SchemaAction;
9
+ const mockClient = {
10
+ getContractSchema: vi.fn(),
11
+ initializeConsensusSmartContract: vi.fn(),
12
+ };
13
+
14
+ const mockPrivateKey = "mocked_private_key";
15
+
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ vi.mocked(createClient).mockReturnValue(mockClient as any);
19
+ vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any);
20
+ schemaAction = new SchemaAction();
21
+ vi.spyOn(schemaAction as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey});
22
+
23
+ vi.spyOn(schemaAction as any, "startSpinner").mockImplementation(() => {});
24
+ vi.spyOn(schemaAction as any, "succeedSpinner").mockImplementation(() => {});
25
+ vi.spyOn(schemaAction as any, "failSpinner").mockImplementation(() => {});
26
+ vi.spyOn(schemaAction as any, "log").mockImplementation(() => {});
27
+ });
28
+
29
+ afterEach(() => {
30
+ vi.restoreAllMocks();
31
+ });
32
+
33
+ test("gets contract schema successfully", async () => {
34
+ const mockResult = {
35
+ methods: {
36
+ getData: {
37
+ params: [["value", "int"]],
38
+ ret: "int",
39
+ readonly: true,
40
+ },
41
+ },
42
+ };
43
+
44
+ vi.mocked(mockClient.getContractSchema).mockResolvedValue(mockResult);
45
+
46
+ await schemaAction.schema({
47
+ contractAddress: "0xMockedContract",
48
+ });
49
+
50
+ expect(mockClient.getContractSchema).toHaveBeenCalledWith("0xMockedContract");
51
+ expect(schemaAction["succeedSpinner"]).toHaveBeenCalledWith(
52
+ "Contract schema retrieved successfully",
53
+ mockResult,
54
+ );
55
+ });
56
+
57
+ test("handles getContractSchema errors", async () => {
58
+ vi.mocked(mockClient.getContractSchema).mockRejectedValue(new Error("Mocked schema error"));
59
+
60
+ await schemaAction.schema({contractAddress: "0xMockedContract"});
61
+
62
+ expect(schemaAction["failSpinner"]).toHaveBeenCalledWith("Error retrieving contract schema", expect.any(Error));
63
+ });
64
+
65
+ test("uses custom RPC URL when provided", async () => {
66
+ const mockResult = {methods: {}};
67
+ vi.mocked(mockClient.getContractSchema).mockResolvedValue(mockResult);
68
+
69
+ await schemaAction.schema({
70
+ contractAddress: "0xMockedContract",
71
+ rpc: "https://custom-rpc-url.com",
72
+ });
73
+
74
+ expect(createClient).toHaveBeenCalledWith(
75
+ expect.objectContaining({
76
+ endpoint: "https://custom-rpc-url.com",
77
+ }),
78
+ );
79
+ expect(mockClient.getContractSchema).toHaveBeenCalledWith("0xMockedContract");
80
+ expect(schemaAction["succeedSpinner"]).toHaveBeenCalledWith(
81
+ "Contract schema retrieved successfully",
82
+ mockResult,
83
+ );
84
+ });
85
+
86
+ test("initializes consensus smart contract", async () => {
87
+ const mockResult = {methods: {}};
88
+ vi.mocked(mockClient.getContractSchema).mockResolvedValue(mockResult);
89
+
90
+ await schemaAction.schema({contractAddress: "0xMockedContract"});
91
+
92
+ expect(mockClient.initializeConsensusSmartContract).toHaveBeenCalled();
93
+ });
94
+ });
@@ -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, "getPrivateKey").mockResolvedValue(mockPrivateKey);
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(() => {});
@@ -0,0 +1,67 @@
1
+ import { Command } from "commander";
2
+ import { SchemaAction } from "../../src/commands/contracts/schema";
3
+ import { vi, describe, beforeEach, afterEach, test, expect } from "vitest";
4
+ import { initializeContractsCommands } from "../../src/commands/contracts";
5
+
6
+ vi.mock("../../src/commands/contracts/schema");
7
+ vi.mock("esbuild", () => ({
8
+ buildSync: vi.fn(),
9
+ }));
10
+
11
+ describe("schema command", () => {
12
+ let program: Command;
13
+
14
+ beforeEach(() => {
15
+ program = new Command();
16
+ initializeContractsCommands(program);
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ test("SchemaAction.schema is called with default options", async () => {
25
+ program.parse(["node", "test", "schema", "0xMockedContract"]);
26
+ expect(SchemaAction).toHaveBeenCalledTimes(1);
27
+ expect(SchemaAction.prototype.schema).toHaveBeenCalledWith({
28
+ contractAddress: "0xMockedContract",
29
+ });
30
+ });
31
+
32
+ test("SchemaAction.schema is called with custom RPC URL", async () => {
33
+ program.parse([
34
+ "node",
35
+ "test",
36
+ "schema",
37
+ "0xMockedContract",
38
+ "--rpc",
39
+ "https://custom-rpc-url.com"
40
+ ]);
41
+ expect(SchemaAction).toHaveBeenCalledTimes(1);
42
+ expect(SchemaAction.prototype.schema).toHaveBeenCalledWith({
43
+ contractAddress: "0xMockedContract",
44
+ rpc: "https://custom-rpc-url.com"
45
+ });
46
+ });
47
+
48
+ test("SchemaAction is instantiated when the schema command is executed", async () => {
49
+ program.parse(["node", "test", "schema", "0xMockedContract"]);
50
+ expect(SchemaAction).toHaveBeenCalledTimes(1);
51
+ });
52
+
53
+ test("throws error for unrecognized options", async () => {
54
+ const schemaCommand = program.commands.find((cmd) => cmd.name() === "schema");
55
+ schemaCommand?.exitOverride();
56
+ expect(() => program.parse(["node", "test", "schema", "0xMockedContract", "--unknown"]))
57
+ .toThrowError("error: unknown option '--unknown'");
58
+ });
59
+
60
+ test("SchemaAction.schema is called without throwing errors for valid options", async () => {
61
+ program.parse(["node", "test", "schema", "0xMockedContract"]);
62
+ vi.mocked(SchemaAction.prototype.schema).mockResolvedValueOnce(undefined);
63
+ expect(() =>
64
+ program.parse(["node", "test", "schema", "0xMockedContract"])
65
+ ).not.toThrow();
66
+ });
67
+ });
@@ -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 result = await baseAction["getPrivateKey"]();
255
+ const account = await baseAction["getAccount"](false);
230
256
 
231
- expect(result).toBe(mockWallet.privateKey);
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 result = await baseAction["getPrivateKey"]();
277
+ const account = await baseAction["getAccount"](false);
244
278
 
245
- expect(result).toBe(mockWallet.privateKey);
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["getPrivateKey"]()).rejects.toThrow("process exited");
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 result = await baseAction["getPrivateKey"]();
310
+ const account = await baseAction["getAccount"](false);
267
311
 
268
- expect(result).toBe(mockWallet.privateKey);
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 mockConfig = { existingKey: "existingValue" };
103
- vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockConfig));
104
+ const existingConfig = { existingKey: "existingValue" };
105
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(existingConfig));
104
106
 
105
- configFileManager.writeConfig("existingKey", "updatedValue");
107
+ configFileManager.writeConfig("existingKey", "newValue");
106
108
 
107
- const expectedConfig = { existingKey: "updatedValue" };
108
- expect(fs.writeFileSync).toHaveBeenCalledWith(
109
- mockConfigFilePath,
110
- JSON.stringify(expectedConfig, null, 2)
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
  });