Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/commands/transactions/appeal.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
}
}
}
16 changes: 16 additions & 0 deletions src/commands/transactions/index.ts
Original file line number Diff line number Diff line change
@@ -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 <txId>")
.description("Appeal a transaction by its hash")
.option("--rpc <rpcUrl>", "RPC URL for the network")
.action(async (txId: TransactionHash, options: AppealOptions) => {
const appealAction = new AppealAction();
await appealAction.appeal({txId, ...options});
});

return program;
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -21,6 +22,7 @@ export function initializeCLI() {
initializeValidatorCommands(program);
initializeScaffoldCommands(program);
initializeNetworkCommands(program);
initializeTransactionsCommands(program);
program.parse(process.argv);
}

Expand Down
99 changes: 99 additions & 0 deletions tests/actions/appeal.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
58 changes: 58 additions & 0 deletions tests/commands/appeal.test.ts
Original file line number Diff line number Diff line change
@@ -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'");
});
});
4 changes: 4 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down