Skip to content
Open
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
7 changes: 7 additions & 0 deletions src/abi/staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,13 @@ export const STAKING_ABI = [
],
outputs: [],
},
{
name: "validatorMinStake",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{name: "", type: "uint256"}],
},
{
name: "setValidatorMinimumStake",
type: "function",
Expand Down
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 +6 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 | 🟠 Major

Use a TLS RPC endpoint for default chain config.

Line 6 hardcodes a plaintext http:// IP endpoint. This downgrades transport guarantees and makes RPC traffic easier to intercept/tamper with on untrusted networks. Prefer a stable https:// hostname endpoint as the default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/chains/testnetAsimov.ts` around lines 6 - 7, The constant
TESTNET_JSON_RPC_URL is set to a plaintext http:// IP which weakens transport
security; update the default to a TLS endpoint (use an https:// hostname rather
than http://IP) and allow overriding via environment (e.g., read from
process.env.TESTNET_JSON_RPC_URL fallback to the new https URL); locate and
change the TESTNET_JSON_RPC_URL definition in testnetAsimov.ts and ensure any
callers expect an HTTPS RPC URL.


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
Comment on lines +5 to +6
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Default Bradbury RPC should use HTTPS, not plaintext HTTP.

Line 5 sets a non-TLS IP endpoint as default. For SDK defaults, prefer a secure https:// host endpoint to avoid transport integrity/privacy degradation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/chains/testnetBradbury.ts` around lines 5 - 6, The default
TESTNET_JSON_RPC_URL constant is using an insecure "http://" scheme; update the
TESTNET_JSON_RPC_URL value to use "https://" (e.g., "https://34.91.102.53:9151")
so the SDK defaults use TLS. Locate the TESTNET_JSON_RPC_URL constant in
src/chains/testnetBradbury.ts and replace the plaintext HTTP URL with the HTTPS
URL, ensuring any downstream code that relies on this constant continues to
function with the secure endpoint.


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
6 changes: 5 additions & 1 deletion src/staking/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {getContract, decodeEventLog, PublicClient, Client, Transport, Chain, Account, Address as ViemAddress, GetContractReturnType, toHex, encodeFunctionData, BaseError, ContractFunctionRevertedError, decodeErrorResult, RawContractError} from "viem";
import {getContract, decodeEventLog, PublicClient, Client, Transport, Chain, Account, Address as ViemAddress, GetContractReturnType, toHex, encodeFunctionData, BaseError, ContractFunctionRevertedError, decodeErrorResult, RawContractError, formatEther} from "viem";
import {GenLayerClient, GenLayerChain, Address} from "@/types";
import {STAKING_ABI, VALIDATOR_WALLET_ABI} from "@/abi/staking";
import {parseStakingAmount, formatStakingAmount} from "./utils";
Expand Down Expand Up @@ -566,6 +566,7 @@ export const stakingActions = (
epochZeroMinDuration,
epochOdd,
epochEven,
validatorMinStakeRaw,
] = await Promise.all([
contract.read.epoch() as Promise<bigint>,
contract.read.finalized() as Promise<bigint>,
Expand All @@ -574,6 +575,7 @@ export const stakingActions = (
contract.read.epochZeroMinDuration() as Promise<bigint>,
contract.read.epochOdd() as Promise<any>,
contract.read.epochEven() as Promise<any>,
contract.read.validatorMinStake() as Promise<bigint>,
]);

// epochOdd/epochEven return arrays: [start, end, inflation, weight, weightDeposit, weightWithdrawal, vcount, claimed, stakeDeposit, stakeWithdrawal, slashed]
Expand Down Expand Up @@ -607,6 +609,8 @@ export const stakingActions = (
activeValidatorsCount: activeCount,
epochMinDuration,
nextEpochEstimate,
validatorMinStakeRaw,
validatorMinStake: formatEther(validatorMinStakeRaw) + " GEN",
};
},

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
2 changes: 2 additions & 0 deletions src/types/staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ export interface EpochInfo {
activeValidatorsCount: bigint;
epochMinDuration: bigint;
nextEpochEstimate: Date | null;
validatorMinStakeRaw: bigint;
validatorMinStake: string;
}

export interface StakingTransactionResult {
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 +166 to +170
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Transaction decoding test does not exercise decoding path.

Line 166 states this validates getTransaction decode behavior, but the test only checks getBlockNumber(). This can miss the exact regression this block is intended to catch.

Suggested minimal correction
-describe(`Testnet ${name} - Transaction Decoding`, () => {
-  it("getTransaction should decode without crashing on a recent finalized tx", async () => {
+describe(`Testnet ${name} - Transaction Decoding`, () => {
+  it("should fetch and decode at least one recent transaction", async () => {
     const client = createClient({chain});
-    const blockNumber = await client.getBlockNumber();
-    expect(blockNumber).toBeGreaterThan(0n);
+    // Exercise transaction retrieval/decoding path here (not just block height).
+    // Example: fetch a recent block with txs, then call `getTransaction` for one hash.
   }, 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 166 - 170, The test labeled for transaction
decoding currently only calls createClient({chain}) and client.getBlockNumber(),
so it never exercises the getTransaction decoding path; modify the test body to
fetch a recent finalized transaction using the client.getBlock or
client.getTransaction APIs: obtain a recent block via client.getBlockNumber()
then client.getBlock(blockNumber) (or client.getBlockWithTransactions) to
retrieve at least one transaction hash and call client.getTransaction(txHash) to
ensure decoding runs, and add assertions that the returned transaction object is
defined and contains expected fields (e.g., hash and to/from) while keeping the
same TIMEOUT and test name.

});

// ─── 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");
}
Comment on lines +179 to +189
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

gen_call availability check is too permissive on transport failures.

If the RPC is unreachable, this can still pass as long as the error message doesn’t contain "method not found". Smoke connectivity should fail on network/transport errors.

Suggested tightening
     try {
       await client.request({
         method: "gen_call" as any,
         params: [{ type: "read", to: "0x0000000000000000000000000000000000000000", from: "0x0000000000000000000000000000000000000000", data: "0x" }],
       });
     } catch (e: any) {
       const msg = (e.message || e.details || "").toLowerCase();
       expect(msg).not.toContain("method not found");
       expect(msg).not.toContain("method_not_found");
+      expect(msg).not.toContain("network");
+      expect(msg).not.toContain("fetch");
+      expect(msg).not.toContain("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 179 - 189, The current catch for
client.request({ method: "gen_call" }) only asserts absence of "method not
found" but still allows network/transport failures to pass; update the catch in
tests/smoke.test.ts to detect transport-level errors and fail the test for those
(e.g., inspect e.code for common codes like "ECONNREFUSED"/"ENETUNREACH" or
e.message containing "connect"/"network"/"timed out") and rethrow or call
expect.fail for transport errors, otherwise continue to assert the error message
does not contain "method not found"/"method_not_found" so genuine RPC errors
still succeed the connectivity smoke check.

}, 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