PR #29 (squash-merged via Gitea API)
Some checks failed
CI / Frontend Lint (push) Failing after 6s
CI / Frontend Type Check (push) Failing after 5s
CI / Frontend Build (push) Failing after 5s
CI / Frontend E2E Tests (push) Failing after 7s
CI / Orchestrator Build (push) Failing after 5s
CI / Orchestrator Unit Tests (push) Failing after 6s
CI / Orchestrator E2E (Testcontainers) (push) Failing after 6s
CI / Contracts Compile (push) Failing after 5s
CI / Contracts Test (push) Failing after 7s
Security Scan / Dependency Vulnerability Scan (push) Failing after 5s
Security Scan / OWASP ZAP Scan (push) Failing after 5s

This commit was merged in pull request #29.
This commit is contained in:
2026-04-22 21:59:13 +00:00
parent 3787362406
commit b48eb2ab76
2 changed files with 160 additions and 1 deletions

View File

@@ -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

View 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);
});