PR X: E2E round-trip against public Chain 138 RPC #29
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -146,8 +146,16 @@ jobs:
|
||||
- name: Install dependencies
|
||||
working-directory: orchestrator
|
||||
run: npm ci
|
||||
- name: E2E tests (Testcontainers Postgres)
|
||||
- name: E2E tests (Testcontainers Postgres + public Chain 138 RPC)
|
||||
working-directory: orchestrator
|
||||
# EXT-CHAIN138-CI-RPC resolved via the public endpoint at
|
||||
# https://rpc.public-0138.defi-oracle.io — the read-only
|
||||
# public-RPC suite exercises the orchestrator's ethers client
|
||||
# against a real Chain 138 node alongside the ganache-based
|
||||
# round-trip tests. The env var opts the public-RPC suite in;
|
||||
# without it, those tests self-skip.
|
||||
env:
|
||||
E2E_USE_PUBLIC_CHAIN138: "1"
|
||||
run: npm run test:e2e
|
||||
|
||||
# Smart Contracts CI
|
||||
|
||||
151
orchestrator/tests/e2e/notaryChainPublicRpc.e2e.test.ts
Normal file
151
orchestrator/tests/e2e/notaryChainPublicRpc.e2e.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user