/** * Read-only E2E round-trip against the **public Chain 138 RPC**. * * Whereas `notaryChainRoundtrip.e2e.test.ts` spins up ganache locally * and exercises both writes and reads, this suite targets the real * public endpoint (`https://rpc.public-0138.defi-oracle.io`) and * closes the proxmox `EXT-CHAIN138-CI-RPC` blocker on the * CurrenciCombo side. * * It does **not** perform any writes: * - we don't own a funded key on Chain 138 in CI; * - writes against mainnet-equivalent infra would be reckless and * non-deterministic. * * What it does do: * 1. Prove the orchestrator's ethers client can reach the public RPC. * 2. Verify `eth_chainId` matches the expected Chain 138. * 3. Verify `eth_blockNumber` returns a plausible current height. * 4. If `NOTARY_REGISTRY_ADDRESS` is set, read a synthetic * `plans(bytes32)` key and assert the contract responded (zeros * are fine — the call succeeding is the point). * 5. Build an orchestrator notaryChain config pointed at the real * chain and confirm the module still gracefully mock-falls-back * when the orchestrator's signing key isn't set. * * Gated on **BOTH** `RUN_E2E=1` and `E2E_USE_PUBLIC_CHAIN138=1` so the * default E2E path stays offline. */ import { JsonRpcProvider, Contract, id as keccakId, ZeroHash } from "ethers"; import { compileNotaryRegistry } from "./helpers/compileNotaryRegistry"; const RUN_E2E = process.env.RUN_E2E === "1"; const USE_PUBLIC = process.env.E2E_USE_PUBLIC_CHAIN138 === "1"; const d = RUN_E2E && USE_PUBLIC ? describe : describe.skip; const DEFAULT_PUBLIC_RPC = "https://rpc.public-0138.defi-oracle.io"; const EXPECTED_CHAIN_ID = 138n; function getPublicRpcUrl(): string { // If the caller set CHAIN_138_RPC_URL, honour it (matches how the // orchestrator's own services pick up config); otherwise use the // documented public endpoint. return process.env.CHAIN_138_RPC_URL || DEFAULT_PUBLIC_RPC; } d("NotaryRegistry read-only round-trip against public Chain 138", () => { let rpcUrl: string; let provider: JsonRpcProvider; beforeAll(() => { rpcUrl = getPublicRpcUrl(); // staticNetwork=true skips the network discovery handshake every // call; cacheTimeout=-1 disables the 250ms response cache so // subsequent JSON-RPC calls see fresh data. provider = new JsonRpcProvider(rpcUrl, undefined, { staticNetwork: true, cacheTimeout: -1, }); }); it("resolves a network descriptor", async () => { const net = await provider.getNetwork(); expect(net).toBeDefined(); expect(typeof net.chainId).toBe("bigint"); }, 30_000); it("eth_chainId matches Chain 138", async () => { const net = await provider.getNetwork(); expect(net.chainId).toBe(EXPECTED_CHAIN_ID); }, 30_000); it("eth_blockNumber returns a positive current height", async () => { const blockNumber = await provider.getBlockNumber(); expect(blockNumber).toBeGreaterThan(0); }, 30_000); it("eth_getBlockByNumber returns a well-formed block", async () => { const latest = await provider.getBlockNumber(); const block = await provider.getBlock(latest); expect(block).not.toBeNull(); if (block) { expect(block.number).toBe(latest); expect(block.hash).toMatch(/^0x[0-9a-fA-F]{64}$/); expect(typeof block.timestamp).toBe("number"); expect(block.timestamp).toBeGreaterThan(1_600_000_000); } }, 30_000); it("reads plans(bytes32) if NOTARY_REGISTRY_ADDRESS is set", async () => { const addr = process.env.NOTARY_REGISTRY_ADDRESS; if (!addr) { // Not a failure — this is the current CI state until the // deployed NotaryRegistry address is published to the // environment. Document it instead of failing. expect(addr).toBeUndefined(); return; } const { abi } = compileNotaryRegistry(); const readOnly = new Contract(addr, abi, provider); // Synthetic id — we expect an empty / zero record but the call // itself must succeed (proves ABI matches deployed contract). const syntheticKey = keccakId("e2e-public-read-only-" + Date.now()); const record = await readOnly.getFunction("plans")(syntheticKey); // plans() returns (planHash, creator, registeredAt, finalizedAt, success, receiptHash) expect(record).toBeDefined(); expect(Array.isArray(record) || typeof record === "object").toBe(true); // Either a fresh key → zeros, or an already-used key — both are OK. // We only assert the types match the tuple shape. const [planHash, , registeredAt, finalizedAt] = record as readonly [ string, string, bigint, bigint, boolean, string, ]; expect(typeof planHash).toBe("string"); expect(typeof registeredAt).toBe("bigint"); expect(typeof finalizedAt).toBe("bigint"); // For a synthetic key, every field should be zero. expect(planHash).toBe(ZeroHash); expect(registeredAt).toBe(0n); expect(finalizedAt).toBe(0n); }, 60_000); it("orchestrator notaryChain module mock-falls-back when signing key is absent", async () => { const saved = { rpc: process.env.CHAIN_138_RPC_URL, addr: process.env.NOTARY_REGISTRY_ADDRESS, pk: process.env.ORCHESTRATOR_PRIVATE_KEY, }; // Point at the public RPC but leave the signing key unset. process.env.CHAIN_138_RPC_URL = rpcUrl; delete process.env.ORCHESTRATOR_PRIVATE_KEY; try { jest.resetModules(); const chain = await import("../../src/services/notaryChain"); const result = await chain.anchorPlan({ plan_id: "public-rpc-readonly-" + Date.now(), steps: [], created_at: new Date().toISOString(), } as never); // With no signer, isConfigured() returns false → mock path. expect(result.mode).toBe("mock"); expect(result.planHash).toMatch(/^0x[0-9a-fA-F]{64}$/); expect(result.txHash).toBeUndefined(); } finally { if (saved.rpc !== undefined) process.env.CHAIN_138_RPC_URL = saved.rpc; else delete process.env.CHAIN_138_RPC_URL; if (saved.addr !== undefined) process.env.NOTARY_REGISTRY_ADDRESS = saved.addr; if (saved.pk !== undefined) process.env.ORCHESTRATOR_PRIVATE_KEY = saved.pk; } }, 30_000); });