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
5 changes: 2 additions & 3 deletions src/chains/testnetAsimov.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {GenLayerChain} from "@/types";
import {STAKING_ABI} from "@/abi/staking";

// chains/localnet.ts
const TESTNET_JSON_RPC_URL = "https://zksync-os-testnet-genlayer.zksync.dev";
const TESTNET_WS_URL = "wss://zksync-os-testnet-genlayer.zksync.dev/ws";
const TESTNET_JSON_RPC_URL = "http://34.12.136.220:9151";
// WebSocket not available on testnet GenLayer RPC nodes
Comment on lines 5 to +7
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Stale comment: file path reference is incorrect.

Line 5 contains // chains/localnet.ts but this file is testnetAsimov.ts. This appears to be a copy-paste artifact.

✏️ Proposed fix
-// chains/localnet.ts
 const TESTNET_JSON_RPC_URL = "http://34.12.136.220:9151";
 // WebSocket not available on testnet GenLayer RPC nodes
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// chains/localnet.ts
const TESTNET_JSON_RPC_URL = "https://zksync-os-testnet-genlayer.zksync.dev";
const TESTNET_WS_URL = "wss://zksync-os-testnet-genlayer.zksync.dev/ws";
const TESTNET_JSON_RPC_URL = "http://34.12.136.220:9151";
// WebSocket not available on testnet GenLayer RPC nodes
const TESTNET_JSON_RPC_URL = "http://34.12.136.220:9151";
// WebSocket not available on testnet GenLayer RPC nodes
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/chains/testnetAsimov.ts` around lines 5 - 7, The inline comment "//
chains/localnet.ts" at the top of this file is a stale copy-paste and should be
removed or replaced with an accurate note; locate the TESTNET_JSON_RPC_URL
constant and update or delete the incorrect comment so the header reflects that
this is testnetAsimov (or remove the file-path comment entirely) to avoid
confusion.


const STAKING_CONTRACT = {
address: "0x63Fa5E0bb10fb6fA98F44726C5518223F767687A" as Address,
Expand Down Expand Up @@ -3990,7 +3990,6 @@ export const testnetAsimov: GenLayerChain = defineChain({
rpcUrls: {
default: {
http: [TESTNET_JSON_RPC_URL],
webSocket: [TESTNET_WS_URL],
},
},
nativeCurrency: {
Expand Down
5 changes: 2 additions & 3 deletions src/chains/testnetBradbury.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {Address, defineChain} from "viem";
import {GenLayerChain} from "@/types";
import {STAKING_ABI} from "@/abi/staking";

const TESTNET_JSON_RPC_URL = "https://zksync-os-testnet-genlayer.zksync.dev";
const TESTNET_WS_URL = "wss://zksync-os-testnet-genlayer.zksync.dev/ws";
const TESTNET_JSON_RPC_URL = "http://34.91.102.53:9151";
// WebSocket not available on testnet GenLayer RPC nodes

const STAKING_CONTRACT = {
address: "0x4A4449E617F8D10FDeD0b461CadEf83939E821A5" as Address,
Expand Down Expand Up @@ -3335,7 +3335,6 @@ export const testnetBradbury: GenLayerChain = defineChain({
rpcUrls: {
default: {
http: [TESTNET_JSON_RPC_URL],
webSocket: [TESTNET_WS_URL],
},
},
nativeCurrency: {
Expand Down
23 changes: 13 additions & 10 deletions src/transactions/decoders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,24 @@ export const decodeInputData = (
};

export const decodeTransaction = (tx: GenLayerRawTransaction): GenLayerTransaction => {
const txDataDecoded = decodeInputData(tx.txData, tx.recipient);
// Normalize field names across chain ABIs (Bradbury uses different names)
const txData = tx.txData ?? (tx as any).txCalldata;
const numOfInitialValidators = tx.numOfInitialValidators ?? (tx as any).initialRotations;

const txDataDecoded = decodeInputData(txData, tx.recipient);

const decodedTx = {
...tx,
txData: tx.txData,
txData: txData,
txDataDecoded: txDataDecoded,

currentTimestamp: tx.currentTimestamp?.toString() ?? "0",
// Bradbury uses `initialRotations`; older chains use `numOfInitialValidators`
numOfInitialValidators: (tx.numOfInitialValidators ?? (tx as any).initialRotations)?.toString() ?? "0",
txSlot: tx.txSlot?.toString() ?? "0",
createdTimestamp: tx.createdTimestamp?.toString() ?? "0",
lastVoteTimestamp: tx.lastVoteTimestamp?.toString() ?? "0",
queuePosition: tx.queuePosition?.toString() ?? "0",
numOfRounds: tx.numOfRounds?.toString() ?? "0",
currentTimestamp: tx.currentTimestamp.toString(),
numOfInitialValidators: numOfInitialValidators?.toString() ?? "0",
txSlot: tx.txSlot.toString(),
createdTimestamp: tx.createdTimestamp.toString(),
lastVoteTimestamp: tx.lastVoteTimestamp.toString(),
queuePosition: tx.queuePosition.toString(),
numOfRounds: tx.numOfRounds.toString(),

readStateBlockRange: {
...tx.readStateBlockRange,
Expand Down
205 changes: 46 additions & 159 deletions tests/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
// These are excluded from regular `npm test` to avoid CI dependence on testnet availability.

import {describe, it, expect, beforeAll} from "vitest";
import {createPublicClient, http, webSocket, getContract, Address as ViemAddress} from "viem";
import {testnetAsimov} from "@/chains/testnetAsimov";
import {testnetBradbury} from "@/chains/testnetBradbury";
import {createClient} from "@/client/client";
import {STAKING_ABI} from "@/abi/staking";
import {Address} from "@/types/accounts";
import {GenLayerChain} from "@/types";

Expand All @@ -25,173 +23,19 @@ for (const {name, chain} of testnets) {

describe(`Testnet ${name} - HTTP RPC`, () => {
it("should fetch chain ID", async () => {
const client = createPublicClient({
chain,
transport: http(chain.rpcUrls.default.http[0]),
});
// Use genlayer-js createClient (uses id: Date.now() to avoid id:0 rejection)
const client = createClient({chain});
const chainId = await client.getChainId();
expect(chainId).toBe(chain.id);
}, TIMEOUT);

it("should fetch latest block number", async () => {
const client = createPublicClient({
chain,
transport: http(chain.rpcUrls.default.http[0]),
});
const blockNumber = await client.getBlockNumber();
expect(blockNumber).toBeGreaterThan(0n);
}, TIMEOUT);
});

// ─── WebSocket RPC Connectivity ──────────────────────────────────────────────

describe(`Testnet ${name} - WebSocket RPC`, () => {
const wsUrl = chain.rpcUrls.default.webSocket?.[0];

it("should have a WS URL configured", () => {
expect(wsUrl).toBeDefined();
expect(wsUrl).toMatch(/^wss?:\/\//);
});

it("should connect and fetch chain ID over WebSocket", async () => {
if (!wsUrl) return;
const client = createPublicClient({
chain,
transport: webSocket(wsUrl),
});
const chainId = await client.getChainId();
// WS endpoint may point to the underlying chain (different ID from GenLayer overlay)
// The key assertion is that the connection works and returns a valid number
expect(chainId).toBeTypeOf("number");
expect(chainId).toBeGreaterThan(0);
if (chainId !== chain.id) {
console.warn(
`WS chain ID (${chainId}) differs from HTTP chain ID (${chain.id}). ` +
`WS URL may point to the underlying L1/L2 chain.`
);
}
}, TIMEOUT);

it("should fetch latest block number over WebSocket", async () => {
if (!wsUrl) return;
const client = createPublicClient({
chain,
transport: webSocket(wsUrl),
});
const client = createClient({chain});
const blockNumber = await client.getBlockNumber();
expect(blockNumber).toBeGreaterThan(0n);
}, TIMEOUT);
});

// ─── Staking Read-Only via WebSocket ─────────────────────────────────────────

describe(`Testnet ${name} - Staking over WebSocket`, () => {
const wsUrl = chain.rpcUrls.default.webSocket?.[0];
const stakingAddress = chain.stakingContract?.address as ViemAddress;

// First check if WS points to the same chain — if not, skip staking tests
let wsMatchesChain = false;
let wsPub: ReturnType<typeof createPublicClient> | null = null;

beforeAll(async () => {
if (!wsUrl) return;
wsPub = createPublicClient({chain, transport: webSocket(wsUrl)});
try {
const chainId = await wsPub.getChainId();
wsMatchesChain = chainId === chain.id;
if (!wsMatchesChain) {
console.warn(
`WS chain ID (${chainId}) differs from testnet (${chain.id}). ` +
`Staking contract calls will be skipped — WS endpoint serves a different chain.`
);
}
} catch {
console.warn("WS connection failed during setup");
}
}, TIMEOUT);

it("epoch() via WS", async () => {
if (!wsMatchesChain || !wsPub) return;
const contract = getContract({address: stakingAddress, abi: STAKING_ABI, client: wsPub});
const epoch = await contract.read.epoch();
expect(epoch).toBeTypeOf("bigint");
}, TIMEOUT);

it("activeValidatorsCount() via WS", async () => {
if (!wsMatchesChain || !wsPub) return;
const contract = getContract({address: stakingAddress, abi: STAKING_ABI, client: wsPub});
const count = await contract.read.activeValidatorsCount();
expect(count).toBeTypeOf("bigint");
expect(count).toBeGreaterThanOrEqual(0n);
}, TIMEOUT);

it("activeValidators() via WS", async () => {
if (!wsMatchesChain || !wsPub) return;
const contract = getContract({address: stakingAddress, abi: STAKING_ABI, client: wsPub});
const validators = await contract.read.activeValidators();
expect(Array.isArray(validators)).toBe(true);
}, TIMEOUT);

it("isValidator() via WS", async () => {
if (!wsMatchesChain || !wsPub) return;
const contract = getContract({address: stakingAddress, abi: STAKING_ABI, client: wsPub});
const validators = (await contract.read.activeValidators()) as ViemAddress[];
const nonZero = validators.filter(v => v !== "0x0000000000000000000000000000000000000000");
if (nonZero.length === 0) return;

const result = await contract.read.isValidator([nonZero[0]]);
expect(result).toBe(true);
}, TIMEOUT);

it("validatorView() via WS", async () => {
if (!wsMatchesChain || !wsPub) return;
const contract = getContract({address: stakingAddress, abi: STAKING_ABI, client: wsPub});
const validators = (await contract.read.activeValidators()) as ViemAddress[];
const nonZero = validators.filter(v => v !== "0x0000000000000000000000000000000000000000");
if (nonZero.length === 0) return;

const view = await contract.read.validatorView([nonZero[0]]) as unknown;
if (Array.isArray(view)) {
expect(view.length).toBe(12);
return;
}

expect(typeof view).toBe("object");
expect(view).not.toBeNull();
const viewObject = view as Record<string, unknown>;
expect(viewObject).toHaveProperty("left");
expect(viewObject).toHaveProperty("right");
expect(viewObject).toHaveProperty("parent");
expect(viewObject).toHaveProperty("eBanned");
expect(viewObject).toHaveProperty("ePrimed");
expect(viewObject).toHaveProperty("vStake");
expect(viewObject).toHaveProperty("vShares");
expect(viewObject).toHaveProperty("dStake");
expect(viewObject).toHaveProperty("dShares");
expect(viewObject).toHaveProperty("vDeposit");
expect(viewObject).toHaveProperty("vWithdrawal");
expect(viewObject).toHaveProperty("live");
}, TIMEOUT);

it("getValidatorQuarantineList() via WS", async () => {
if (!wsMatchesChain || !wsPub) return;
const contract = getContract({address: stakingAddress, abi: STAKING_ABI, client: wsPub});
const list = await contract.read.getValidatorQuarantineList();
expect(Array.isArray(list)).toBe(true);
}, TIMEOUT);

it("epochOdd() / epochEven() via WS", async () => {
if (!wsMatchesChain || !wsPub) return;
const contract = getContract({address: stakingAddress, abi: STAKING_ABI, client: wsPub});
const odd = await contract.read.epochOdd();
const even = await contract.read.epochEven();
expect(Array.isArray(odd)).toBe(true);
expect(Array.isArray(even)).toBe(true);
expect(odd.length).toBe(11);
expect(even.length).toBe(11);
}, TIMEOUT);
});

// ─── Staking Read-Only Methods ───────────────────────────────────────────────

describe(`Testnet ${name} - Staking (read-only)`, () => {
Expand Down Expand Up @@ -316,4 +160,47 @@ describe(`Testnet ${name} - Staking (read-only)`, () => {
});
});

// ─── Transaction Decoding (getTransaction) ─────────────────────────────────

describe(`Testnet ${name} - Transaction Decoding`, () => {
it("getTransaction should decode without crashing on a recent finalized tx", async () => {
const client = createClient({chain});
const blockNumber = await client.getBlockNumber();
expect(blockNumber).toBeGreaterThan(0n);
}, TIMEOUT);
});
Comment on lines +163 to +171
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test does not match description: "Transaction Decoding" only fetches block number.

The test is named "getTransaction should decode without crashing on a recent finalized tx" but only calls getBlockNumber(). It doesn't actually call getTransaction or test any decoding logic.

Either rename this test to reflect what it actually does, or implement actual transaction decoding verification.

💡 Suggested implementation for actual transaction decoding test
 describe(`Testnet ${name} - Transaction Decoding`, () => {
-  it("getTransaction should decode without crashing on a recent finalized tx", async () => {
+  it("should successfully fetch block number", async () => {
     const client = createClient({chain});
     const blockNumber = await client.getBlockNumber();
     expect(blockNumber).toBeGreaterThan(0n);
   }, TIMEOUT);
+
+  // TODO: Add actual transaction decoding test when a known finalized tx hash is available
+  // it("getTransaction should decode without crashing", async () => {
+  //   const client = createClient({chain});
+  //   const tx = await client.getTransaction({ hash: KNOWN_TX_HASH });
+  //   expect(tx).toBeDefined();
+  //   expect(tx.txDataDecoded).toBeDefined();
+  // }, TIMEOUT);
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/smoke.test.ts` around lines 163 - 171, The test labeled "getTransaction
should decode without crashing on a recent finalized tx" does not call
getTransaction and only calls createClient and getBlockNumber; update the test
to either rename it to reflect it's only checking getBlockNumber or implement a
real transaction decoding assertion: use createClient({chain}) to fetch a recent
block (e.g., client.getBlock or client.getBlockWithTransactions), pick a
transaction hash from the block, call client.getTransaction(txHash) and assert
the returned transaction/decoded fields are defined (or that decoding does not
throw), keeping the same TIMEOUT and test structure; reference createClient,
getBlock/getBlockWithTransactions, getTransaction and the existing test name
when making the change.


// ─── GenLayer RPC Methods ───────────────────────────────────────────────────

describe(`Testnet ${name} - GenLayer RPC (gen_call)`, () => {
it("gen_call should be available on the RPC", async () => {
const client = createClient({chain});
// A basic RPC method check — gen_call with invalid params should return an error, not a connection failure
try {
await client.request({
method: "gen_call" as any,
params: [{ type: "read", to: "0x0000000000000000000000000000000000000000", from: "0x0000000000000000000000000000000000000000", data: "0x" }],
});
} catch (e: any) {
// We expect an RPC error (invalid contract, etc.), NOT a "method not found" error
const msg = (e.message || e.details || "").toLowerCase();
expect(msg).not.toContain("method not found");
expect(msg).not.toContain("method_not_found");
}
}, TIMEOUT);
});

// ─── Account Balance ────────────────────────────────────────────────────────

describe(`Testnet ${name} - Account Balance`, () => {
it("should fetch balance for an address", async () => {
const client = createClient({chain});
const balance = await client.getBalance({
address: "0x0000000000000000000000000000000000000001",
});
expect(balance).toBeTypeOf("bigint");
}, TIMEOUT);
});


} // end for loop over testnets
Loading
Loading