import { describe, it, expect, vi, beforeEach } from "vitest";
import { MegaETHClient, MegaETHError, RealtimeTransactionExpiredError, MiniBlockNotFoundError } from "../../src/client/megaeth.js";
import { createTestConfig } from "../setup.js";
vi.mock("axios", () => {
const instance = { post: vi.fn() } as any;
const create = vi.fn(() => instance);
return {
__esModule: true,
default: { create },
create,
__instance: instance,
} as any;
});
// Import mocked axios instance for resetting between tests
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - __instance is provided by our mock above
import { __instance as axiosInstance } from "axios";
describe("MegaETHClient (HTTP)", () => {
beforeEach(() => {
axiosInstance.post.mockReset();
});
it("callRPC success returns result", async () => {
axiosInstance.post.mockResolvedValueOnce({ data: { result: "ok" } });
const client = new MegaETHClient(createTestConfig({ megaethWsUrl: undefined }));
const res = await client.callRPC("eth_chainId", []);
expect(res).toBe("ok");
expect(axiosInstance.post).toHaveBeenCalledWith("/", expect.objectContaining({ method: "eth_chainId" }));
});
it("maps realtime expired error to RealtimeTransactionExpiredError", async () => {
axiosInstance.post.mockResolvedValueOnce({ data: { error: { message: "realtime transaction expired", code: -32000 } } });
const client = new MegaETHClient(createTestConfig());
await expect(client.callRPC("realtime_sendRawTransaction", ["0xdead"])).rejects.toBeInstanceOf(RealtimeTransactionExpiredError);
});
it("maps mini block not found error to MiniBlockNotFoundError", async () => {
axiosInstance.post.mockResolvedValueOnce({ data: { error: { message: "mini block not found", code: -32000 } } });
const client = new MegaETHClient(createTestConfig());
await expect(client.callRPC("eth_getBlockByNumber", [123])).rejects.toBeInstanceOf(MiniBlockNotFoundError);
});
it("other RPC error maps to MegaETHError with code preserved", async () => {
axiosInstance.post.mockResolvedValueOnce({ data: { error: { message: "boom", code: 42, data: { a: 1 } } } });
const client = new MegaETHClient(createTestConfig());
try {
await client.callRPC("x", []);
throw new Error("should not reach");
} catch (err: any) {
expect(err).toBeInstanceOf(MegaETHError);
expect(err.code).toBe(42);
expect(err.data).toEqual({ a: 1 });
}
});
it("network/axios error is annotated and rethrown", async () => {
const baseErr = Object.assign(new Error("down"), { response: { data: { error: { message: "down" } } } });
axiosInstance.post.mockRejectedValueOnce(baseErr);
const client = new MegaETHClient(createTestConfig());
try {
await client.callRPC("eth_getLogsWithCursor", [{ fromBlock: 1 }]);
throw new Error("should not reach");
} catch (err: any) {
expect(err.rpcMethod).toBe("eth_getLogsWithCursor");
expect(err.rpcParams).toEqual([{ fromBlock: 1 }]);
}
});
it("validateConnection ok when chainId matches config", async () => {
const chainIdHex = "0x" + (1337).toString(16);
axiosInstance.post.mockResolvedValueOnce({ data: { result: chainIdHex } });
const client = new MegaETHClient(createTestConfig({ megaethChainId: 1337 }));
await expect(client.validateConnection()).resolves.toBeUndefined();
});
it("validateConnection throws on mismatch", async () => {
const chainIdHex = "0x1";
axiosInstance.post.mockResolvedValueOnce({ data: { result: chainIdHex } });
const client = new MegaETHClient(createTestConfig({ megaethChainId: 1337 }));
await expect(client.validateConnection()).rejects.toThrow(/Failed to validate MegaETH connection/);
});
it("isMegaETHChain returns false for 'method not found'", async () => {
const error = Object.assign(new Error("method not found"), { response: { data: { error: { message: "method not found" } } } });
axiosInstance.post.mockRejectedValueOnce(error);
const client = new MegaETHClient(createTestConfig());
await expect(client.isMegaETHChain()).resolves.toBe(false);
});
it("isMegaETHChain returns true for other errors (e.g., invalid params)", async () => {
const error = Object.assign(new Error("invalid params"), { response: { data: { error: { message: "invalid params" } } } });
axiosInstance.post.mockRejectedValueOnce(error);
const client = new MegaETHClient(createTestConfig());
await expect(client.isMegaETHChain()).resolves.toBe(true);
});
it("isMegaETHChain currently returns false when method succeeds (potential bug)", async () => {
// NOTE: According to plan, success should imply MegaETH and return true. Current implementation returns false.
axiosInstance.post.mockResolvedValueOnce({ data: { result: "ok" } });
const client = new MegaETHClient(createTestConfig());
await expect(client.isMegaETHChain()).resolves.toBe(false);
// TODO: When implementation is fixed, change expectation to true and adjust test name.
});
it("sendRawTransaction calls realtime_sendRawTransaction with/without timeout", async () => {
const receipt = { transactionHash: "0xabc" } as any;
axiosInstance.post.mockResolvedValue({ data: { result: receipt } });
const client = new MegaETHClient(createTestConfig());
await client.sendRawTransaction("0xdeadbeef");
await client.sendRawTransaction("0xbeef", 5000);
const calls = axiosInstance.post.mock.calls;
const payloads = calls.map(([, p]) => p);
expect(payloads[0]).toMatchObject({ method: "realtime_sendRawTransaction", params: ["0xdeadbeef"] });
expect(payloads[1]).toMatchObject({ method: "realtime_sendRawTransaction", params: ["0xbeef", 5000] });
});
it("getLogsWithCursor merges filter and sets hasMore based on cursor", async () => {
axiosInstance.post.mockResolvedValueOnce({ data: { result: { logs: [1], cursor: "c1" } } });
const client = new MegaETHClient(createTestConfig());
const res = await client.getLogsWithCursor({ fromBlock: 1, topics: [] }, undefined, 10);
expect(res.logs).toEqual([1]);
expect(res.cursor).toBe("c1");
expect(res.hasMore).toBe(true);
});
it("getChainInfo combines and parses results", async () => {
axiosInstance.post
.mockResolvedValueOnce({ data: { result: "0x539" } }) // chainId 1337
.mockResolvedValueOnce({ data: { result: "0x10" } }); // block 16
const client = new MegaETHClient(createTestConfig());
const info = await client.getChainInfo();
expect(info).toEqual({
chainId: 1337,
networkName: "MegaETH",
isRealtimeEnabled: true,
latestBlock: 16,
miniBlockInterval: 100,
});
});
it("getBlockByNumber hex-encodes number inputs", async () => {
axiosInstance.post.mockResolvedValueOnce({ data: { result: { number: "0xa" } } });
const client = new MegaETHClient(createTestConfig());
await client.getBlockByNumber(10);
const [, payload] = axiosInstance.post.mock.calls[0];
expect(payload).toMatchObject({ method: "eth_getBlockByNumber", params: ["0xa", false] });
});
});