From e6e31646661bbcd5219df83f47f34dfdc5642590 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 00:25:32 -0300 Subject: [PATCH 01/13] feat: readonly on call command --- src/commands/contracts/call.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/contracts/call.ts b/src/commands/contracts/call.ts index 742b4741..39febb59 100644 --- a/src/commands/contracts/call.ts +++ b/src/commands/contracts/call.ts @@ -21,7 +21,7 @@ export class CallAction extends BaseAction { args: any[]; rpc?: string; }): Promise { - const client = await this.getClient(rpc); + const client = await this.getClient(rpc, true); await client.initializeConsensusSmartContract(); this.startSpinner(`Calling method ${method} on contract at ${contractAddress}...`); From 10d93e0c98d560e9a82a45521389fa0a88e065d1 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 00:26:27 -0300 Subject: [PATCH 02/13] feat: base action now use the address when the client is read only --- src/lib/actions/BaseAction.ts | 50 +++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/lib/actions/BaseAction.ts b/src/lib/actions/BaseAction.ts index 759fab10..3b7e7e79 100644 --- a/src/lib/actions/BaseAction.ts +++ b/src/lib/actions/BaseAction.ts @@ -5,12 +5,16 @@ import inquirer from "inquirer"; import { inspect } from "util"; import {createClient, createAccount} from "genlayer-js"; import {localnet} from "genlayer-js/chains"; -import type {GenLayerClient, GenLayerChain} from "genlayer-js/types"; +import type {GenLayerClient, GenLayerChain, Hash, Address, Account} from "genlayer-js/types"; import { ethers } from "ethers"; import { writeFileSync, existsSync, readFileSync } from "fs"; import { KeystoreData } from "../interfaces/KeystoreData"; export class BaseAction extends ConfigFileManager { + private static readonly DEFAULT_KEYSTORE_PATH = "./keypair.json"; + private static readonly MAX_PASSWORD_ATTEMPTS = 3; + private static readonly MIN_PASSWORD_LENGTH = 8; + private spinner: Ora; private _genlayerClient: GenLayerClient | null = null; @@ -23,13 +27,13 @@ export class BaseAction extends ConfigFileManager { try { const message = attempt === 1 ? "Enter password to decrypt keystore:" - : `Invalid password. Attempt ${attempt}/3 - Enter password to decrypt keystore:`; + : `Invalid password. Attempt ${attempt}/${BaseAction.MAX_PASSWORD_ATTEMPTS} - Enter password to decrypt keystore:`; const password = await this.promptPassword(message); const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password); return wallet.privateKey; } catch (error) { - if (attempt >= 3) { - this.failSpinner("Maximum password attempts exceeded (3/3)."); + if (attempt >= BaseAction.MAX_PASSWORD_ATTEMPTS) { + this.failSpinner(`Maximum password attempts exceeded (${BaseAction.MAX_PASSWORD_ATTEMPTS}/${BaseAction.MAX_PASSWORD_ATTEMPTS}).`); process.exit(1); } return await this.decryptKeystore(keystoreData, attempt + 1); @@ -52,36 +56,54 @@ export class BaseAction extends ConfigFileManager { return inspect(data, { depth: null, colors: false }); } - protected async getClient(rpcUrl?: string): Promise> { + protected async getClient(rpcUrl?: string, readOnly: boolean = false): Promise> { if (!this._genlayerClient) { const networkConfig = this.getConfig().network; const network = networkConfig ? JSON.parse(networkConfig) : localnet; + const account = await this.getAccount(readOnly); this._genlayerClient = createClient({ chain: network, endpoint: rpcUrl, - account: createAccount((await this.getPrivateKey()) as any), + account: account, }); } return this._genlayerClient; } - protected async getPrivateKey(): Promise { - const keypairPath = this.getConfigByKey("keyPairPath"); + private async getAccount(readOnly: boolean = false): Promise { + let keypairPath = this.getConfigByKey("keyPairPath"); + let decryptedPrivateKey; + let keystoreData; if (!keypairPath || !existsSync(keypairPath)) { await this.confirmPrompt("Keypair file not found. Would you like to create a new keypair?"); - return await this.createKeypair("./keypair.json", false); + decryptedPrivateKey = await this.createKeypair(BaseAction.DEFAULT_KEYSTORE_PATH, false); + keypairPath = this.getConfigByKey("keyPairPath")!; } - const keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8")); + keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8")); if (!this.isValidKeystoreFormat(keystoreData)) { this.failSpinner("Invalid keystore format. Expected encrypted keystore file."); await this.confirmPrompt("Would you like to create a new keypair?"); - return await this.createKeypair("./keypair.json", true); + decryptedPrivateKey = await this.createKeypair(BaseAction.DEFAULT_KEYSTORE_PATH, true); + keypairPath = this.getConfigByKey("keyPairPath")!; + keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8")); + + } + + if (readOnly) { + return this.getAddress(keystoreData); } + + if (!decryptedPrivateKey) { + decryptedPrivateKey = await this.decryptKeystore(keystoreData); + } + return createAccount(decryptedPrivateKey as Hash); + } - return await this.decryptKeystore(keystoreData); + private getAddress(keystoreData: KeystoreData): Address { + return keystoreData.address as Address; } protected async createKeypair(outputPath: string, overwrite: boolean): Promise { @@ -103,8 +125,8 @@ export class BaseAction extends ConfigFileManager { process.exit(1); } - if (password.length < 8) { - this.failSpinner("Password must be at least 8 characters long"); + if (password.length < BaseAction.MIN_PASSWORD_LENGTH) { + this.failSpinner(`Password must be at least ${BaseAction.MIN_PASSWORD_LENGTH} characters long`); process.exit(1); } From d6cd5ad7096abaf476a6c77be6f7f53c9fd78620 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 01:25:55 -0300 Subject: [PATCH 03/13] feat: new temp file at base action --- src/lib/actions/BaseAction.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/lib/actions/BaseAction.ts b/src/lib/actions/BaseAction.ts index 3b7e7e79..39b5ad74 100644 --- a/src/lib/actions/BaseAction.ts +++ b/src/lib/actions/BaseAction.ts @@ -14,6 +14,7 @@ export class BaseAction extends ConfigFileManager { private static readonly DEFAULT_KEYSTORE_PATH = "./keypair.json"; private static readonly MAX_PASSWORD_ATTEMPTS = 3; private static readonly MIN_PASSWORD_LENGTH = 8; + private static readonly TEMP_KEY_FILENAME = "decrypted_private_key"; private spinner: Ora; private _genlayerClient: GenLayerClient | null = null; @@ -21,6 +22,7 @@ export class BaseAction extends ConfigFileManager { constructor() { super(); this.spinner = ora({text: "", spinner: "dots"}); + this.cleanupExpiredTempFiles(); } private async decryptKeystore(keystoreData: KeystoreData, attempt: number = 1): Promise { @@ -30,6 +32,9 @@ export class BaseAction extends ConfigFileManager { : `Invalid password. Attempt ${attempt}/${BaseAction.MAX_PASSWORD_ATTEMPTS} - Enter password to decrypt keystore:`; const password = await this.promptPassword(message); const wallet = await ethers.Wallet.fromEncryptedJson(keystoreData.encrypted, password); + + this.storeTempFile(BaseAction.TEMP_KEY_FILENAME, wallet.privateKey); + return wallet.privateKey; } catch (error) { if (attempt >= BaseAction.MAX_PASSWORD_ATTEMPTS) { @@ -89,7 +94,6 @@ export class BaseAction extends ConfigFileManager { decryptedPrivateKey = await this.createKeypair(BaseAction.DEFAULT_KEYSTORE_PATH, true); keypairPath = this.getConfigByKey("keyPairPath")!; keystoreData = JSON.parse(readFileSync(keypairPath, "utf-8")); - } if (readOnly) { @@ -97,7 +101,8 @@ export class BaseAction extends ConfigFileManager { } if (!decryptedPrivateKey) { - decryptedPrivateKey = await this.decryptKeystore(keystoreData); + const cachedKey = this.getTempFile(BaseAction.TEMP_KEY_FILENAME); + decryptedPrivateKey = cachedKey ? cachedKey : await this.decryptKeystore(keystoreData); } return createAccount(decryptedPrivateKey as Hash); } @@ -141,6 +146,8 @@ export class BaseAction extends ConfigFileManager { writeFileSync(finalOutputPath, JSON.stringify(keystoreData, null, 2)); this.writeConfig('keyPairPath', finalOutputPath); + this.clearTempFile(BaseAction.TEMP_KEY_FILENAME); + return wallet.privateKey; } From a10d416fef9a1779625a331d0abef32bfdb1a12b Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 01:26:43 -0300 Subject: [PATCH 04/13] feat: new config file manager with temp files --- src/lib/config/ConfigFileManager.ts | 73 +++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/lib/config/ConfigFileManager.ts b/src/lib/config/ConfigFileManager.ts index 7227275d..801c8d5b 100644 --- a/src/lib/config/ConfigFileManager.ts +++ b/src/lib/config/ConfigFileManager.ts @@ -2,15 +2,24 @@ import path from "path"; import os from "os"; import fs from "fs"; +interface TempFileData { + content: string; + timestamp: number; +} + export class ConfigFileManager { private folderPath: string; private configFilePath: string; + private tempFolderPath: string; + private static readonly TEMP_FILE_EXPIRATION_MS = 5 * 60 * 1000; // 5 minutes constructor(baseFolder: string = ".genlayer/", configFileName: string = "genlayer-config.json") { this.folderPath = path.resolve(os.homedir(), baseFolder); this.configFilePath = path.resolve(this.folderPath, configFileName); + this.tempFolderPath = path.resolve(os.tmpdir(), "genlayer-temp"); this.ensureFolderExists(); this.ensureConfigFileExists(); + this.ensureTempFolderExists(); } private ensureFolderExists(): void { @@ -25,6 +34,12 @@ export class ConfigFileManager { } } + private ensureTempFolderExists(): void { + if (!fs.existsSync(this.tempFolderPath)) { + fs.mkdirSync(this.tempFolderPath, { recursive: true, mode: 0o700 }); // Owner-only access + } + } + getFolderPath(): string { return this.folderPath; } @@ -48,4 +63,62 @@ export class ConfigFileManager { config[key] = value; fs.writeFileSync(this.configFilePath, JSON.stringify(config, null, 2)); } + + storeTempFile(fileName: string, content: string): void { + this.ensureTempFolderExists(); + const filePath = path.resolve(this.tempFolderPath, fileName); + const tempData: TempFileData = { + content, + timestamp: Date.now() + }; + fs.writeFileSync(filePath, JSON.stringify(tempData), { mode: 0o600 }); // Owner-only access + } + + getTempFile(fileName: string): string | null { + const filePath = path.resolve(this.tempFolderPath, fileName); + + if (!fs.existsSync(filePath)) { + return null; + } + + const fileContent = fs.readFileSync(filePath, "utf-8"); + const tempData: TempFileData = JSON.parse(fileContent); + + if (Date.now() - tempData.timestamp > ConfigFileManager.TEMP_FILE_EXPIRATION_MS) { + this.clearTempFile(fileName); + return null; + } + + return tempData.content; + } + + hasTempFile(fileName: string): boolean { + return this.getTempFile(fileName) !== null; + } + + clearTempFile(fileName: string): void { + const filePath = path.resolve(this.tempFolderPath, fileName); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + } + + cleanupExpiredTempFiles(): void { + if (!fs.existsSync(this.tempFolderPath)) { + return; + } + + const files = fs.readdirSync(this.tempFolderPath); + const now = Date.now(); + + for (const file of files) { + const filePath = path.resolve(this.tempFolderPath, file); + const fileContent = fs.readFileSync(filePath, "utf-8"); + const tempData: TempFileData = JSON.parse(fileContent); + + if (now - tempData.timestamp > ConfigFileManager.TEMP_FILE_EXPIRATION_MS) { + fs.unlinkSync(filePath); + } + } + } } From 2b45ac89e9a4d43e75f9204ee59a34c628c4c45f Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 02:18:44 -0300 Subject: [PATCH 05/13] test: base action --- tests/libs/baseAction.test.ts | 90 ++++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/tests/libs/baseAction.test.ts b/tests/libs/baseAction.test.ts index 24ba0b01..06676761 100644 --- a/tests/libs/baseAction.test.ts +++ b/tests/libs/baseAction.test.ts @@ -6,11 +6,52 @@ import chalk from "chalk"; import {inspect} from "util"; import { ethers } from "ethers"; import { writeFileSync, existsSync, readFileSync } from "fs"; +import fs from "fs"; +import os from "os"; +import { createAccount } from "genlayer-js"; vi.mock("inquirer"); vi.mock("ora"); vi.mock("fs"); +vi.mock("os"); vi.mock("ethers"); +vi.mock("genlayer-js", () => ({ + createAccount: vi.fn(), + createClient: vi.fn(), + localnet: {} +}));vi.mock("genlayer-js", () => ({ + createAccount: vi.fn(), + createClient: vi.fn(), + localnet: {} +}));vi.mock("genlayer-js", () => ({ + createAccount: vi.fn(), + createClient: vi.fn(), + localnet: {} +}));vi.mock("genlayer-js", () => ({ + createAccount: vi.fn(), + createClient: vi.fn(), + localnet: {} +}));vi.mock("genlayer-js", () => ({ + createAccount: vi.fn(), + createClient: vi.fn(), + localnet: {} +}));vi.mock("genlayer-js", () => ({ + createAccount: vi.fn(), + createClient: vi.fn(), + localnet: {} +}));vi.mock("genlayer-js", () => ({ + createAccount: vi.fn(), + createClient: vi.fn(), + localnet: {} +}));vi.mock("genlayer-js", () => ({ + createAccount: vi.fn(), + createClient: vi.fn(), + localnet: {} +}));vi.mock("genlayer-js", () => ({ + createAccount: vi.fn(), + createClient: vi.fn(), + localnet: {} +})); describe("BaseAction", () => { let baseAction: BaseAction; @@ -47,14 +88,25 @@ describe("BaseAction", () => { } as unknown as Ora; (ora as unknown as Mock).mockReturnValue(mockSpinner); + + vi.mocked(os.homedir).mockReturnValue("/mocked/home"); + vi.mocked(os.tmpdir).mockReturnValue("/mocked/tmp"); + vi.mocked(existsSync).mockReturnValue(true); vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockKeystoreData)); vi.mocked(writeFileSync).mockImplementation(() => {}); + vi.mocked(fs.readdirSync).mockReturnValue([] as any); + vi.mocked(fs.mkdirSync).mockImplementation(() => "/mocked/path"); // Mock ethers vi.mocked(ethers.Wallet.createRandom).mockReturnValue(mockWallet as any); vi.mocked(ethers.Wallet.fromEncryptedJson).mockResolvedValue(mockWallet as any); + vi.mocked(createAccount).mockReturnValue({ + privateKey: mockWallet.privateKey, + address: mockWallet.address + } as any); + baseAction = new BaseAction(); // Mock config methods @@ -62,6 +114,12 @@ describe("BaseAction", () => { vi.spyOn(baseAction as any, "getFilePath").mockImplementation(() => "./test-keypair.json"); vi.spyOn(baseAction as any, "writeConfig").mockImplementation(() => {}); vi.spyOn(baseAction as any, "getConfig").mockReturnValue({}); + + // Mock temp file methods + vi.spyOn(baseAction as any, "storeTempFile").mockImplementation(() => {}); + vi.spyOn(baseAction as any, "getTempFile").mockReturnValue(null); + vi.spyOn(baseAction as any, "clearTempFile").mockImplementation(() => {}); + vi.spyOn(baseAction as any, "cleanupExpiredTempFiles").mockImplementation(() => {}); }); afterEach(() => { @@ -226,9 +284,17 @@ describe("BaseAction", () => { test("should return private key when keystore exists and is valid", async () => { vi.mocked(inquirer.prompt).mockResolvedValue({password: "correct-password"}); - const result = await baseAction["getPrivateKey"](); + const account = await baseAction["getAccount"](false); - expect(result).toBe(mockWallet.privateKey); + expect((account as any).privateKey).toBe(mockWallet.privateKey); + expect(existsSync).toHaveBeenCalledWith("./test-keypair.json"); + expect(readFileSync).toHaveBeenCalledWith("./test-keypair.json", "utf-8"); + }); + + test("should return address when called with readOnly=true", async () => { + const address = await baseAction["getAccount"](true); + + expect(address).toBe(mockKeystoreData.address); expect(existsSync).toHaveBeenCalledWith("./test-keypair.json"); expect(readFileSync).toHaveBeenCalledWith("./test-keypair.json", "utf-8"); }); @@ -240,9 +306,9 @@ describe("BaseAction", () => { .mockResolvedValueOnce({password: "new-password"}) // encrypt password .mockResolvedValueOnce({password: "new-password"}); // confirm password - const result = await baseAction["getPrivateKey"](); + const account = await baseAction["getAccount"](false); - expect(result).toBe(mockWallet.privateKey); + expect((account as any).privateKey).toBe(mockWallet.privateKey); expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([ expect.objectContaining({message: chalk.yellow("Keypair file not found. Would you like to create a new keypair?")}) ])); @@ -252,10 +318,20 @@ describe("BaseAction", () => { vi.mocked(readFileSync).mockReturnValue('{"invalid": "format"}'); vi.mocked(inquirer.prompt).mockResolvedValue({confirmAction: false}); - await expect(baseAction["getPrivateKey"]()).rejects.toThrow("process exited"); + await expect(baseAction["getAccount"](false)).rejects.toThrow("process exited"); expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Invalid keystore format. Expected encrypted keystore file.")); }); + test("should use cached key when available", async () => { + vi.spyOn(baseAction as any, "getTempFile").mockReturnValue(mockWallet.privateKey); + + const account = await baseAction["getAccount"](false); + + expect((account as any).privateKey).toBe(mockWallet.privateKey); + expect(baseAction["getTempFile"]).toHaveBeenCalledWith("decrypted_private_key"); + expect(inquirer.prompt).not.toHaveBeenCalled(); + }); + test("should create new keypair when keystore format is invalid and user confirms", async () => { vi.mocked(readFileSync).mockReturnValue('{"invalid": "format"}'); vi.mocked(inquirer.prompt) @@ -263,9 +339,9 @@ describe("BaseAction", () => { .mockResolvedValueOnce({password: "new-password"}) .mockResolvedValueOnce({password: "new-password"}); - const result = await baseAction["getPrivateKey"](); + const account = await baseAction["getAccount"](false); - expect(result).toBe(mockWallet.privateKey); + expect((account as any).privateKey).toBe(mockWallet.privateKey); expect(mockSpinner.fail).toHaveBeenCalledWith(chalk.red("Invalid keystore format. Expected encrypted keystore file.")); expect(inquirer.prompt).toHaveBeenCalledWith(expect.arrayContaining([ expect.objectContaining({message: chalk.yellow("Would you like to create a new keypair?")}) From af7dea6b1bf16f695c1bc1b51e4774e5c3335cde Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 02:19:04 -0300 Subject: [PATCH 06/13] test: config file manager --- tests/libs/configFileManager.test.ts | 194 +++++++++++++++++++++++++-- 1 file changed, 186 insertions(+), 8 deletions(-) diff --git a/tests/libs/configFileManager.test.ts b/tests/libs/configFileManager.test.ts index d49f39b2..b3a3f1db 100644 --- a/tests/libs/configFileManager.test.ts +++ b/tests/libs/configFileManager.test.ts @@ -11,12 +11,14 @@ vi.mock("os") describe("ConfigFileManager", () => { const mockFolderPath = "/mocked/home/.genlayer"; const mockConfigFilePath = `${mockFolderPath}/genlayer-config.json`; + const mockTempFolderPath = "/mocked/tmp/genlayer-temp"; let configFileManager: ConfigFileManager; beforeEach(() => { vi.clearAllMocks(); vi.mocked(os.homedir).mockReturnValue("/mocked/home"); + vi.mocked(os.tmpdir).mockReturnValue("/mocked/tmp"); configFileManager = new ConfigFileManager(); }); @@ -99,15 +101,191 @@ describe("ConfigFileManager", () => { }); test("writeConfig overwrites an existing key in the config file", () => { - const mockConfig = { existingKey: "existingValue" }; - vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockConfig)); + const existingConfig = { existingKey: "existingValue" }; + vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(existingConfig)); - configFileManager.writeConfig("existingKey", "updatedValue"); + configFileManager.writeConfig("existingKey", "newValue"); - const expectedConfig = { existingKey: "updatedValue" }; - expect(fs.writeFileSync).toHaveBeenCalledWith( - mockConfigFilePath, - JSON.stringify(expectedConfig, null, 2) - ); + const expectedConfig = { existingKey: "newValue" }; + expect(fs.writeFileSync).toHaveBeenCalledWith(mockConfigFilePath, JSON.stringify(expectedConfig, null, 2)); + }); + + describe("Temp File Operations", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(os.homedir).mockReturnValue("/mocked/home"); + vi.mocked(os.tmpdir).mockReturnValue("/mocked/tmp"); + configFileManager = new ConfigFileManager(); + }); + + test("storeTempFile creates temp folder and stores file with timestamp", () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + const mockTimestamp = 1234567890; + vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp); + + configFileManager.storeTempFile("test.json", "test content"); + + expect(fs.mkdirSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp", { recursive: true, mode: 0o700 }); + + const expectedData = { + content: "test content", + timestamp: mockTimestamp + }; + expect(fs.writeFileSync).toHaveBeenCalledWith( + "/mocked/tmp/genlayer-temp/test.json", + JSON.stringify(expectedData), + { mode: 0o600 } + ); + }); + + test("storeTempFile does not create temp folder when it already exists", () => { + vi.clearAllMocks(); + vi.mocked(fs.existsSync).mockImplementation((path) => { + if (path === "/mocked/home/.genlayer") return true; + if (path === "/mocked/tmp/genlayer-temp") return true; + return false; + }); + + const testConfigManager = new ConfigFileManager(); + + const mockTimestamp = 1234567890; + vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp); + + testConfigManager.storeTempFile("test.json", "test content"); + + expect(fs.mkdirSync).not.toHaveBeenCalled(); + + const expectedData = { + content: "test content", + timestamp: mockTimestamp + }; + expect(fs.writeFileSync).toHaveBeenCalledWith( + "/mocked/tmp/genlayer-temp/test.json", + JSON.stringify(expectedData), + { mode: 0o600 } + ); + }); + + test("getTempFile returns content when file exists and is not expired", () => { + const mockTimestamp = Date.now() - 60000; // 1 minute ago + const mockFileData = { + content: "cached content", + timestamp: mockTimestamp + }; + + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockFileData)); + + const result = configFileManager.getTempFile("test.json"); + + expect(result).toBe("cached content"); + expect(fs.existsSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/test.json"); + expect(fs.readFileSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/test.json", "utf-8"); + }); + + test("getTempFile returns null when file does not exist", () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + + const result = configFileManager.getTempFile("nonexistent.json"); + + expect(result).toBeNull(); + expect(fs.existsSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/nonexistent.json"); + }); + + test("getTempFile returns null and clears file when expired", () => { + const expiredTimestamp = Date.now() - (6 * 60 * 1000); // 6 minutes ago (expired) + const mockFileData = { + content: "expired content", + timestamp: expiredTimestamp + }; + + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockFileData)); + + const result = configFileManager.getTempFile("expired.json"); + + expect(result).toBeNull(); + expect(fs.unlinkSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/expired.json"); + }); + + test("hasTempFile returns true when valid temp file exists", () => { + const mockTimestamp = Date.now() - 60000; // 1 minute ago + const mockFileData = { + content: "test content", + timestamp: mockTimestamp + }; + + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockFileData)); + + const result = configFileManager.hasTempFile("test.json"); + + expect(result).toBe(true); + }); + + test("hasTempFile returns false when temp file is expired", () => { + const expiredTimestamp = Date.now() - (6 * 60 * 1000); // 6 minutes ago + const mockFileData = { + content: "expired content", + timestamp: expiredTimestamp + }; + + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockFileData)); + + const result = configFileManager.hasTempFile("expired.json"); + + expect(result).toBe(false); + }); + + test("clearTempFile removes specific temp file", () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + + configFileManager.clearTempFile("test.json"); + + expect(fs.existsSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/test.json"); + expect(fs.unlinkSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/test.json"); + }); + + test("clearTempFile does nothing when file does not exist", () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + + configFileManager.clearTempFile("nonexistent.json"); + + expect(fs.existsSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/nonexistent.json"); + expect(fs.unlinkSync).not.toHaveBeenCalled(); + }); + + test("cleanupExpiredTempFiles removes only expired files", () => { + const now = Date.now(); + const validTimestamp = now - 60000; // 1 minute ago (valid) + const expiredTimestamp = now - (6 * 60 * 1000); // 6 minutes ago (expired) + + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readdirSync).mockReturnValue(['valid.json', 'expired.json'] as any); + + vi.mocked(fs.readFileSync) + .mockReturnValueOnce(JSON.stringify({content: "valid", timestamp: validTimestamp})) + .mockReturnValueOnce(JSON.stringify({content: "expired", timestamp: expiredTimestamp})); + + vi.spyOn(Date, 'now').mockReturnValue(now); + + configFileManager.cleanupExpiredTempFiles(); + + expect(fs.readdirSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp"); + expect(fs.readFileSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/valid.json", "utf-8"); + expect(fs.readFileSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/expired.json", "utf-8"); + expect(fs.unlinkSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/expired.json"); + expect(fs.unlinkSync).toHaveBeenCalledTimes(1); // Only expired file should be deleted + }); + + test("cleanupExpiredTempFiles does nothing when temp folder does not exist", () => { + vi.mocked(fs.existsSync).mockReturnValue(false); + + configFileManager.cleanupExpiredTempFiles(); + + expect(fs.existsSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp"); + expect(fs.readdirSync).not.toHaveBeenCalled(); + }); }); }); From 6fc5790cf47a70679fd2b1f994ed59f97a76e5a6 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 02:19:34 -0300 Subject: [PATCH 07/13] test: write action --- tests/actions/write.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/write.test.ts b/tests/actions/write.test.ts index a9064e86..dc886580 100644 --- a/tests/actions/write.test.ts +++ b/tests/actions/write.test.ts @@ -19,7 +19,7 @@ describe("WriteAction", () => { vi.mocked(createClient).mockReturnValue(mockClient as any); vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any); writeAction = new WriteAction(); - vi.spyOn(writeAction as any, "getPrivateKey").mockResolvedValue(mockPrivateKey); + vi.spyOn(writeAction as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey}); vi.spyOn(writeAction as any, "startSpinner").mockImplementation(() => {}); vi.spyOn(writeAction as any, "succeedSpinner").mockImplementation(() => {}); From b4c2c8322dceaafc110e06b61c8d1c3f8a502acf Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 02:19:48 -0300 Subject: [PATCH 08/13] test: receipt action --- tests/actions/receipt.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/receipt.test.ts b/tests/actions/receipt.test.ts index 12099b7e..382562c7 100644 --- a/tests/actions/receipt.test.ts +++ b/tests/actions/receipt.test.ts @@ -23,7 +23,7 @@ describe("ReceiptAction", () => { vi.mocked(createClient).mockReturnValue(mockClient as any); vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any); receiptAction = new ReceiptAction(); - vi.spyOn(receiptAction as any, "getPrivateKey").mockResolvedValue(mockPrivateKey); + vi.spyOn(receiptAction as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey}); vi.spyOn(receiptAction as any, "startSpinner").mockImplementation(() => {}); vi.spyOn(receiptAction as any, "succeedSpinner").mockImplementation(() => {}); From 8225cb8934245593fc2bb8fe5ed638a3ba3dae9d Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 02:20:04 -0300 Subject: [PATCH 09/13] test: deploy action --- tests/actions/deploy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/deploy.test.ts b/tests/actions/deploy.test.ts index 8fdd31b9..3fc67937 100644 --- a/tests/actions/deploy.test.ts +++ b/tests/actions/deploy.test.ts @@ -26,7 +26,7 @@ describe("DeployAction", () => { vi.mocked(createClient).mockReturnValue(mockClient as any); vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any); deployer = new DeployAction(); - vi.spyOn(deployer as any, "getPrivateKey").mockResolvedValue(mockPrivateKey); + vi.spyOn(deployer as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey}); vi.spyOn(deployer as any, "getConfig").mockReturnValue({}); vi.spyOn(deployer as any, "startSpinner").mockImplementation(() => {}); From 14a2c39962c0ac025aba8311399acc7d5fe8e93b Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 02:20:53 -0300 Subject: [PATCH 10/13] test: call action --- tests/actions/call.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/call.test.ts b/tests/actions/call.test.ts index aaf33696..32556ecf 100644 --- a/tests/actions/call.test.ts +++ b/tests/actions/call.test.ts @@ -21,7 +21,7 @@ describe("CallAction", () => { vi.mocked(createClient).mockReturnValue(mockClient as any); vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any); callActions = new CallAction(); - vi.spyOn(callActions as any, "getPrivateKey").mockResolvedValue(mockPrivateKey); + vi.spyOn(callActions as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey}); vi.spyOn(callActions as any, "startSpinner").mockImplementation(() => {}); vi.spyOn(callActions as any, "succeedSpinner").mockImplementation(() => {}); From c4652e907f78cb096f01095aa555180245423a00 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 02:21:06 -0300 Subject: [PATCH 11/13] test: appeal action --- tests/actions/appeal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/appeal.test.ts b/tests/actions/appeal.test.ts index 3caf7e7d..f0d184ef 100644 --- a/tests/actions/appeal.test.ts +++ b/tests/actions/appeal.test.ts @@ -21,7 +21,7 @@ describe("AppealAction", () => { vi.mocked(createClient).mockReturnValue(mockClient as any); vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any); appealAction = new AppealAction(); - vi.spyOn(appealAction as any, "getPrivateKey").mockResolvedValue(mockPrivateKey); + vi.spyOn(appealAction as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey}); vi.spyOn(appealAction as any, "startSpinner").mockImplementation(() => {}); vi.spyOn(appealAction as any, "succeedSpinner").mockImplementation(() => {}); From 458a378299105fd192fb33ddc8e144c07738acd6 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 02:29:58 -0300 Subject: [PATCH 12/13] fix: duplicated code --- tests/libs/baseAction.test.ts | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/tests/libs/baseAction.test.ts b/tests/libs/baseAction.test.ts index 06676761..5b26563c 100644 --- a/tests/libs/baseAction.test.ts +++ b/tests/libs/baseAction.test.ts @@ -19,38 +19,6 @@ vi.mock("genlayer-js", () => ({ createAccount: vi.fn(), createClient: vi.fn(), localnet: {} -}));vi.mock("genlayer-js", () => ({ - createAccount: vi.fn(), - createClient: vi.fn(), - localnet: {} -}));vi.mock("genlayer-js", () => ({ - createAccount: vi.fn(), - createClient: vi.fn(), - localnet: {} -}));vi.mock("genlayer-js", () => ({ - createAccount: vi.fn(), - createClient: vi.fn(), - localnet: {} -}));vi.mock("genlayer-js", () => ({ - createAccount: vi.fn(), - createClient: vi.fn(), - localnet: {} -}));vi.mock("genlayer-js", () => ({ - createAccount: vi.fn(), - createClient: vi.fn(), - localnet: {} -}));vi.mock("genlayer-js", () => ({ - createAccount: vi.fn(), - createClient: vi.fn(), - localnet: {} -}));vi.mock("genlayer-js", () => ({ - createAccount: vi.fn(), - createClient: vi.fn(), - localnet: {} -}));vi.mock("genlayer-js", () => ({ - createAccount: vi.fn(), - createClient: vi.fn(), - localnet: {} })); describe("BaseAction", () => { From 1bb470c28271dde6e95366776989ededcc953515 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Tue, 22 Jul 2025 03:01:24 -0300 Subject: [PATCH 13/13] feat: corrupted files cleanup --- src/lib/config/ConfigFileManager.ts | 13 +++++++++---- tests/libs/configFileManager.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/lib/config/ConfigFileManager.ts b/src/lib/config/ConfigFileManager.ts index 801c8d5b..8ab0ca4b 100644 --- a/src/lib/config/ConfigFileManager.ts +++ b/src/lib/config/ConfigFileManager.ts @@ -113,10 +113,15 @@ export class ConfigFileManager { for (const file of files) { const filePath = path.resolve(this.tempFolderPath, file); - const fileContent = fs.readFileSync(filePath, "utf-8"); - const tempData: TempFileData = JSON.parse(fileContent); - - if (now - tempData.timestamp > ConfigFileManager.TEMP_FILE_EXPIRATION_MS) { + + try { + const fileContent = fs.readFileSync(filePath, "utf-8"); + const tempData: TempFileData = JSON.parse(fileContent); + + if (now - tempData.timestamp > ConfigFileManager.TEMP_FILE_EXPIRATION_MS) { + fs.unlinkSync(filePath); + } + } catch (error) { fs.unlinkSync(filePath); } } diff --git a/tests/libs/configFileManager.test.ts b/tests/libs/configFileManager.test.ts index b3a3f1db..fd52dbac 100644 --- a/tests/libs/configFileManager.test.ts +++ b/tests/libs/configFileManager.test.ts @@ -279,6 +279,30 @@ describe("ConfigFileManager", () => { expect(fs.unlinkSync).toHaveBeenCalledTimes(1); // Only expired file should be deleted }); + test("cleanupExpiredTempFiles removes corrupted files that cannot be parsed", () => { + const now = Date.now(); + const validTimestamp = now - 60000; // 1 minute ago (valid) + + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readdirSync).mockReturnValue(['valid.json', 'corrupted.json'] as any); + + vi.mocked(fs.readFileSync) + .mockReturnValueOnce(JSON.stringify({content: "valid", timestamp: validTimestamp})) + .mockReturnValueOnce("invalid json content"); + + vi.spyOn(Date, 'now').mockReturnValue(now); + + configFileManager.cleanupExpiredTempFiles(); + + expect(fs.readdirSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp"); + expect(fs.readFileSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/valid.json", "utf-8"); + expect(fs.readFileSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/corrupted.json", "utf-8"); + + // The corrupted file should be deleted due to JSON.parse error + expect(fs.unlinkSync).toHaveBeenCalledWith("/mocked/tmp/genlayer-temp/corrupted.json"); + expect(fs.unlinkSync).toHaveBeenCalledTimes(1); // Only corrupted file should be deleted + }); + test("cleanupExpiredTempFiles does nothing when temp folder does not exist", () => { vi.mocked(fs.existsSync).mockReturnValue(false);