From ed21f78851d3738bc111ee84fe32c5921a00554d Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Mon, 7 Jul 2025 11:13:25 -0300 Subject: [PATCH 1/7] feat: appeal action --- src/commands/transactions/appeal.ts | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/commands/transactions/appeal.ts diff --git a/src/commands/transactions/appeal.ts b/src/commands/transactions/appeal.ts new file mode 100644 index 0000000..e2532c6 --- /dev/null +++ b/src/commands/transactions/appeal.ts @@ -0,0 +1,39 @@ +import {TransactionHash} from "genlayer-js/types"; +import {BaseAction} from "../../lib/actions/BaseAction"; + +export interface AppealOptions { + rpc?: string; +} + +export class AppealAction extends BaseAction { + constructor() { + super(); + } + + async appeal({ + txId, + rpc, + }: { + txId: TransactionHash; + rpc?: string; + }): Promise { + const client = await this.getClient(rpc); + await client.initializeConsensusSmartContract(); + this.startSpinner(`Appealing transaction ${txId}...`); + + try { + const hash = await client.appealTransaction({ + txId, + }); + + const result = await client.waitForTransactionReceipt({ + hash, + retries: 100, + interval: 5000, + }); + this.succeedSpinner("Appeal operation successfully executed", result); + } catch (error) { + this.failSpinner("Error during appeal operation", error); + } + } +} \ No newline at end of file From c91fcda52409f6c804385e1bf8ecf2dae66ccfda Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Mon, 7 Jul 2025 11:13:48 -0300 Subject: [PATCH 2/7] feat: appeal command --- src/commands/transactions/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/commands/transactions/index.ts diff --git a/src/commands/transactions/index.ts b/src/commands/transactions/index.ts new file mode 100644 index 0000000..d8d5fdd --- /dev/null +++ b/src/commands/transactions/index.ts @@ -0,0 +1,16 @@ +import {Command} from "commander"; +import {TransactionHash} from "genlayer-js/types"; +import {AppealAction, AppealOptions} from "./appeal"; + +export function initializeTransactionsCommands(program: Command) { + program + .command("appeal ") + .description("Appeal a transaction by its hash") + .option("--rpc ", "RPC URL for the network") + .action(async (txId: TransactionHash, options: AppealOptions) => { + const appealer = new AppealAction(); + await appealer.appeal({txId, ...options}); + }); + + return program; +} \ No newline at end of file From 0f81fb0be1d78988317e0b2f1c8558f5cb94cfad Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Mon, 7 Jul 2025 11:14:40 -0300 Subject: [PATCH 3/7] fix: importing appeal command --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index 1e101f8..8cd83bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import {initializeValidatorCommands} from "../src/commands/validators"; import {initializeUpdateCommands} from "../src/commands/update"; import {initializeScaffoldCommands} from "../src/commands/scaffold"; import {initializeNetworkCommands} from "../src/commands/network"; +import {initializeTransactionsCommands} from "../src/commands/transactions"; export function initializeCLI() { program.version(version).description(CLI_DESCRIPTION); @@ -21,6 +22,7 @@ export function initializeCLI() { initializeValidatorCommands(program); initializeScaffoldCommands(program); initializeNetworkCommands(program); + initializeTransactionsCommands(program); program.parse(process.argv); } From 34e9b3c0eb5e600b2bf4353deae5c7155698cae4 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Mon, 7 Jul 2025 11:15:23 -0300 Subject: [PATCH 4/7] tests: available commands test --- tests/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/index.test.ts b/tests/index.test.ts index bd81447..3e25aa8 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -41,6 +41,10 @@ vi.mock("../src/commands/network", () => ({ initializeNetworkCommands: vi.fn(), })); +vi.mock("../src/commands/transactions", () => ({ + initializeTransactionsCommands: vi.fn(), +})); + describe("CLI", () => { it("should initialize CLI", () => { expect(initializeCLI).not.toThrow(); From a0cc939a7a5c72190b500c49479f06d19afd9e50 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Mon, 7 Jul 2025 11:25:30 -0300 Subject: [PATCH 5/7] tests: appeal command --- tests/commands/appeal.test.ts | 58 +++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/commands/appeal.test.ts diff --git a/tests/commands/appeal.test.ts b/tests/commands/appeal.test.ts new file mode 100644 index 0000000..db7c51f --- /dev/null +++ b/tests/commands/appeal.test.ts @@ -0,0 +1,58 @@ +import {Command} from "commander"; +import {AppealAction} from "../../src/commands/transactions/appeal"; +import {vi, describe, beforeEach, afterEach, test, expect} from "vitest"; +import {initializeTransactionsCommands} from "../../src/commands/transactions"; + +vi.mock("../../src/commands/transactions/appeal"); + +describe("appeal command", () => { + let program: Command; + const mockTxId = "0x1234567890123456789012345678901234567890123456789012345678901234"; + + beforeEach(() => { + program = new Command(); + initializeTransactionsCommands(program); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("AppealAction.appeal is called with default options", async () => { + program.parse(["node", "test", "appeal", mockTxId]); + expect(AppealAction).toHaveBeenCalledTimes(1); + expect(AppealAction.prototype.appeal).toHaveBeenCalledWith({ + txId: mockTxId, + }); + }); + + test("AppealAction.appeal is called with custom RPC URL", async () => { + program.parse([ + "node", + "test", + "appeal", + mockTxId, + "--rpc", + "https://custom-rpc-url-for-appeal.com", + ]); + expect(AppealAction).toHaveBeenCalledTimes(1); + expect(AppealAction.prototype.appeal).toHaveBeenCalledWith({ + txId: mockTxId, + rpc: "https://custom-rpc-url-for-appeal.com", + }); + }); + + test("AppealAction is instantiated when the appeal command is executed", async () => { + program.parse(["node", "test", "appeal", mockTxId]); + expect(AppealAction).toHaveBeenCalledTimes(1); + }); + + test("throws error for unrecognized options", async () => { + const appealCommand = program.commands.find(cmd => cmd.name() === "appeal"); + appealCommand?.exitOverride(); + expect(() => + program.parse(["node", "test", "appeal", mockTxId, "--invalid-option"]), + ).toThrowError("error: unknown option '--invalid-option'"); + }); +}); \ No newline at end of file From cacc09b4bbd06e2071e8f8d3c554642ee195c074 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Mon, 7 Jul 2025 11:30:38 -0300 Subject: [PATCH 6/7] tests: appeal action --- tests/actions/appeal.test.ts | 99 ++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 tests/actions/appeal.test.ts diff --git a/tests/actions/appeal.test.ts b/tests/actions/appeal.test.ts new file mode 100644 index 0000000..3caf7e7 --- /dev/null +++ b/tests/actions/appeal.test.ts @@ -0,0 +1,99 @@ +import {describe, test, vi, beforeEach, afterEach, expect} from "vitest"; +import {createClient, createAccount} from "genlayer-js"; +import type {TransactionHash} from "genlayer-js/types"; +import {AppealAction} from "../../src/commands/transactions/appeal"; + +vi.mock("genlayer-js"); + +describe("AppealAction", () => { + let appealAction: AppealAction; + const mockClient = { + appealTransaction: vi.fn(), + waitForTransactionReceipt: vi.fn(), + initializeConsensusSmartContract: vi.fn(), + }; + + const mockPrivateKey = "mocked_private_key"; + const mockTxId = "0x1234567890123456789012345678901234567890123456789012345678901234" as TransactionHash; + + beforeEach(() => { + vi.clearAllMocks(); + 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, "startSpinner").mockImplementation(() => {}); + vi.spyOn(appealAction as any, "succeedSpinner").mockImplementation(() => {}); + vi.spyOn(appealAction as any, "failSpinner").mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("calls appealTransaction successfully", async () => { + const mockReceipt = {status: "success"}; + + vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt); + + await appealAction.appeal({ + txId: mockTxId, + }); + + expect(mockClient.appealTransaction).toHaveBeenCalledWith({ + txId: mockTxId, + }); + expect(appealAction["succeedSpinner"]).toHaveBeenCalledWith( + "Appeal operation successfully executed", + mockReceipt, + ); + }); + + test("handles appealTransaction errors", async () => { + vi.mocked(mockClient.appealTransaction).mockRejectedValue(new Error("Mocked appeal error")); + + await appealAction.appeal({txId: mockTxId}); + + expect(appealAction["failSpinner"]).toHaveBeenCalledWith( + "Error during appeal operation", + expect.any(Error), + ); + }); + + test("uses custom RPC URL for appeal operations", async () => { + const rpcUrl = "https://custom-rpc-url.com"; + const mockReceipt = {status: "success"}; + + vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt); + + await appealAction.appeal({ + txId: mockTxId, + rpc: rpcUrl, + }); + + expect(createClient).toHaveBeenCalledWith( + expect.objectContaining({ + endpoint: rpcUrl, + }), + ); + expect(mockClient.appealTransaction).toHaveBeenCalledWith({ + txId: mockTxId, + }); + expect(appealAction["succeedSpinner"]).toHaveBeenCalledWith( + "Appeal operation successfully executed", + mockReceipt, + ); + }); + + test("initializes consensus smart contract before appeal", async () => { + const mockReceipt = {status: "success"}; + + vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt); + + await appealAction.appeal({txId: mockTxId}); + + expect(mockClient.initializeConsensusSmartContract).toHaveBeenCalledTimes(1); + expect(mockClient.appealTransaction).toHaveBeenCalled(); + }); +}); \ No newline at end of file From 1510747da7924a9211ffc4764238c2abefa2c2f5 Mon Sep 17 00:00:00 2001 From: Edinaldo Junior Date: Mon, 7 Jul 2025 11:32:41 -0300 Subject: [PATCH 7/7] chore: const name --- src/commands/transactions/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/transactions/index.ts b/src/commands/transactions/index.ts index d8d5fdd..8a2d50f 100644 --- a/src/commands/transactions/index.ts +++ b/src/commands/transactions/index.ts @@ -8,8 +8,8 @@ export function initializeTransactionsCommands(program: Command) { .description("Appeal a transaction by its hash") .option("--rpc ", "RPC URL for the network") .action(async (txId: TransactionHash, options: AppealOptions) => { - const appealer = new AppealAction(); - await appealer.appeal({txId, ...options}); + const appealAction = new AppealAction(); + await appealAction.appeal({txId, ...options}); }); return program;