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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,12 @@ jobs:
steps:
- uses: actions/checkout@v4

# Pin the Rust toolchain: newer stable (1.97+) fails to compile
# `cargo install --locked stellar-cli --version 22.8.1` (ethnum 1.5.0
# transmute error). 1.96.1 is the last known-good version for the
# pinned soroban-sdk 22.x stack and keeps builds reproducible.
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@1.96.1
with:
targets: wasm32-unknown-unknown,wasm32v1-none

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ state in which user funds are stranded under operator control.

> **SCF reviewers and investors.** A consolidated diligence package — contracts,
> explorer links, CI status, security checklist, roadmap, and budget — is at
> [`docs/DILIGENCE_DATA_ROOM.md`](docs/DILIGENCE_DATA_ROOM.md). Readable in ≤ 10 minutes.
> [`docs/MAINNET_READINESS_SCORECARD.md`](docs/MAINNET_READINESS_SCORECARD.md). Readable in ≤ 10 minutes.

---

Expand Down Expand Up @@ -264,7 +264,7 @@ threat model.

| Document | What it covers |
|---|---|
| [`docs/DILIGENCE_DATA_ROOM.md`](docs/DILIGENCE_DATA_ROOM.md) | **SCF reviewer data room** — contracts, explorer links, CI, security checklist, roadmap, budget, risks; readable in ≤ 10 min |
| [`docs/MAINNET_READINESS_SCORECARD.md`](docs/MAINNET_READINESS_SCORECARD.md) | **Mainnet readiness scorecard** — contracts, explorer links, CI, security checklist, roadmap, budget, risks; readable in ≤ 10 min |
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Full technical architecture: invariants, sequence diagrams, refund stack, failure catalogue, cryptographic primitives, operational characteristics, auditor checklist |
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Developer setup, CI test matrix, pull request expectations, and the [contributor proof checklist](.github/pull_request_template.md) |
| [`ROADMAP.md`](ROADMAP.md) | Milestone-by-milestone delivery plan with verifiable artefacts; testnet v2 live, mainnet UI gated until audit |
Expand All @@ -274,6 +274,7 @@ threat model.
| [`docs/METRICS_SCHEMA.md`](docs/METRICS_SCHEMA.md) | Public metrics schema: definitions, units, sources, cadence, and privacy boundaries |
| [`docs/UNIT_ECONOMICS.md`](docs/UNIT_ECONOMICS.md) | Resolver cost model, bootstrap incentives, break-even ranges, and metrics needed before stronger claims |
| [`docs/RESOLVERS.md`](docs/RESOLVERS.md) | How to run your own resolver |
| [`docs/RISK_REGISTER.md`](docs/RISK_REGISTER.md) | **Investor-grade risk register** — 16 concrete risks with likelihood, impact, mitigations, evidence links, and next actions |
| [`docs/SECURITY.md`](docs/SECURITY.md) | STRIDE threat model, audit prep checklist, bug bounty |
| [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) | Testnet + mainnet deployment, env var reference, network configuration |
| [`docs/REVIEW_RESPONSE.md`](docs/REVIEW_RESPONSE.md) | Direct response to v1 reviewer feedback, item by item |
Expand Down
292 changes: 146 additions & 146 deletions coordinator/test/readiness.test.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,70 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import request from "supertest";
import express from "express";
import { healthRoutes } from "../src/server/routes/health.js";
function makeApp() {
const app = express();
app.use(healthRoutes());
return app;
}
describe("GET /readiness", () => {
const ORIGINAL_ENV = process.env;
beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
process.env.DATABASE_URL = "file:./oversync.db";
process.env.SOROBAN_RPC_URL = "https://soroban-testnet.stellar.org";
process.env.STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
delete process.env.NETWORK_MODE;
delete process.env.ETHEREUM_CHAIN_ID;
delete process.env.WS_ENABLED;
delete process.env.WEBSOCKET_ENABLED;
});
afterEach(() => {
process.env = ORIGINAL_ENV;
vi.restoreAllMocks();
});
it("returns 200 with service name and version", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.service).toBe("oversync-coordinator");
expect(typeof res.body.version).toBe("string");
});
it("defaults networkMode to testnet", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.networkMode).toBe("testnet");
});
it("uses NETWORK_MODE env override for mainnet", async () => {
process.env.NETWORK_MODE = "mainnet";
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.networkMode).toBe("mainnet");
});
it("defaults Ethereum chain to Sepolia in testnet mode", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.ethereum.chainId).toBe(11_155_111);
expect(res.body.ethereum.chainName).toBe("sepolia");
});
it("defaults Ethereum chain to mainnet when NETWORK_MODE=mainnet", async () => {
process.env.NETWORK_MODE = "mainnet";
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.ethereum.chainId).toBe(1);
expect(res.body.ethereum.chainName).toBe("mainnet");
});
it("honours explicit ETHEREUM_CHAIN_ID override", async () => {
process.env.ETHEREUM_CHAIN_ID = "17000";
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.ethereum.chainId).toBe(17_000);
expect(res.body.ethereum.chainName).toBe("holesky");
});
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import request from "supertest";
import express from "express";
import { healthRoutes } from "../src/server/routes/health.js";

function makeApp() {
const app = express();
app.use(healthRoutes());
return app;
}

describe("GET /readiness", () => {
const ORIGINAL_ENV = process.env;

beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
process.env.DATABASE_URL = "file:./oversync.db";
process.env.SOROBAN_RPC_URL = "https://soroban-testnet.stellar.org";
process.env.STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
delete process.env.NETWORK_MODE;
delete process.env.ETHEREUM_CHAIN_ID;
delete process.env.WS_ENABLED;
delete process.env.WEBSOCKET_ENABLED;
});

afterEach(() => {
process.env = ORIGINAL_ENV;
vi.restoreAllMocks();
});

it("returns 200 with service name and version", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.service).toBe("oversync-coordinator");
expect(typeof res.body.version).toBe("string");
});

it("defaults networkMode to testnet", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.networkMode).toBe("testnet");
});

it("uses NETWORK_MODE env override for mainnet", async () => {
process.env.NETWORK_MODE = "mainnet";
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.networkMode).toBe("mainnet");
});

it("defaults Ethereum chain to Sepolia in testnet mode", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.ethereum.chainId).toBe(11_155_111);
expect(res.body.ethereum.chainName).toBe("sepolia");
});

it("defaults Ethereum chain to mainnet when NETWORK_MODE=mainnet", async () => {
process.env.NETWORK_MODE = "mainnet";
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.ethereum.chainId).toBe(1);
expect(res.body.ethereum.chainName).toBe("mainnet");
});

it("honours explicit ETHEREUM_CHAIN_ID override", async () => {
process.env.ETHEREUM_CHAIN_ID = "17000";
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.ethereum.chainId).toBe(17_000);
expect(res.body.ethereum.chainName).toBe("holesky");
});

it("reports Stellar network label as testnet from passphrase", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.stellar.network).toBe("testnet");
Expand All @@ -75,83 +75,83 @@ describe("GET /readiness", () => {
expect(res.body.stellar.networkPassphraseHash).toMatch(/^[a-f0-9]{64}$/);
expect(JSON.stringify(res.body)).not.toContain("Test SDF Network ; September 2015");
});
it("reports Stellar network label as mainnet from passphrase", async () => {
process.env.STELLAR_NETWORK_PASSPHRASE =
"Public Global Stellar Network ; September 2015";
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.stellar.network).toBe("mainnet");
});
it("reports stellar.rpcConfigured true when SOROBAN_RPC_URL is set", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.stellar.rpcConfigured).toBe(true);
});
it("reports stellar.rpcConfigured false when SOROBAN_RPC_URL is absent", async () => {
delete process.env.SOROBAN_RPC_URL;
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.stellar.rpcConfigured).toBe(false);
});
it("reports database.reachable true for a recognised sqlite URL", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.database.reachable).toBe(true);
});
it("reports database.reachable false when DATABASE_URL is unset", async () => {
delete process.env.DATABASE_URL;
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.database.reachable).toBe(false);
});
it("reports websocket.enabled false by default", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.websocket.enabled).toBe(false);
});
it("reports websocket.enabled true when WS_ENABLED=true", async () => {
process.env.WS_ENABLED = "true";
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.websocket.enabled).toBe(true);
});
it("includes an ISO timestamp", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
});
it("never includes the Soroban RPC URL itself", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
const json = JSON.stringify(res.body);
expect(json).not.toContain("soroban-testnet.stellar.org");
});
it("never includes the full Stellar network passphrase", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
const json = JSON.stringify(res.body);
expect(json).not.toContain("Test SDF Network ; September 2015");
});
it("never includes DATABASE_URL", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
const json = JSON.stringify(res.body);
expect(json).not.toContain("oversync.db");
expect(json).not.toContain("DATABASE_URL");
});
it("never includes an Ethereum RPC URL credentials", async () => {
process.env.ETHEREUM_RPC_URL = "https://USER:SECRET@rpc.example.com/private-rpc";
const res = await request(makeApp()).get("/readiness").expect(200);
const json = JSON.stringify(res.body);
expect(json).not.toContain("SECRET");
expect(json).not.toContain("USER");
});
it("does not return order-shaped fields", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.orders).toBeUndefined();
expect(res.body.secret).toBeUndefined();
expect(res.body.hashlock).toBeUndefined();
});

it("reports Stellar network label as mainnet from passphrase", async () => {
process.env.STELLAR_NETWORK_PASSPHRASE =
"Public Global Stellar Network ; September 2015";
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.stellar.network).toBe("mainnet");
});

it("reports stellar.rpcConfigured true when SOROBAN_RPC_URL is set", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.stellar.rpcConfigured).toBe(true);
});

it("reports stellar.rpcConfigured false when SOROBAN_RPC_URL is absent", async () => {
delete process.env.SOROBAN_RPC_URL;
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.stellar.rpcConfigured).toBe(false);
});

it("reports database.reachable true for a recognised sqlite URL", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.database.reachable).toBe(true);
});

it("reports database.reachable false when DATABASE_URL is unset", async () => {
delete process.env.DATABASE_URL;
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.database.reachable).toBe(false);
});

it("reports websocket.enabled false by default", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.websocket.enabled).toBe(false);
});

it("reports websocket.enabled true when WS_ENABLED=true", async () => {
process.env.WS_ENABLED = "true";
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.websocket.enabled).toBe(true);
});

it("includes an ISO timestamp", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
});

it("never includes the Soroban RPC URL itself", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
const json = JSON.stringify(res.body);
expect(json).not.toContain("soroban-testnet.stellar.org");
});

it("never includes the full Stellar network passphrase", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
const json = JSON.stringify(res.body);
expect(json).not.toContain("Test SDF Network ; September 2015");
});

it("never includes DATABASE_URL", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
const json = JSON.stringify(res.body);
expect(json).not.toContain("oversync.db");
expect(json).not.toContain("DATABASE_URL");
});

it("never includes an Ethereum RPC URL credentials", async () => {
process.env.ETHEREUM_RPC_URL = "https://USER:SECRET@rpc.example.com/private-rpc";
const res = await request(makeApp()).get("/readiness").expect(200);
const json = JSON.stringify(res.body);
expect(json).not.toContain("SECRET");
expect(json).not.toContain("USER");
});

it("does not return order-shaped fields", async () => {
const res = await request(makeApp()).get("/readiness").expect(200);
expect(res.body.orders).toBeUndefined();
expect(res.body.secret).toBeUndefined();
expect(res.body.hashlock).toBeUndefined();
});
});
16 changes: 11 additions & 5 deletions coordinator/test/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ describe("OrderService.getSnapshots", () => {
orderId: "c1",
txHash: "0xc1src",
blockNumber: 1,
timelock: 600
timelock: 10_000
});
await orders.recordDstLock({
Expand Down Expand Up @@ -230,11 +231,16 @@ describe("OrderService.getSnapshots", () => {
const refundedSnapshot = snapshots.find((s) => s.orderId === refunded1.publicId)!;
expect(completedSnapshot.currentState).toBe("completed");
expect(refundedSnapshot.currentState).toBe("refunded");
// Order is by updated_at DESC; completed1 gets more transitions so its
// updatedAt should be >= refunded1's.
expect(completedSnapshot.timestamps.updatedAt).toBeGreaterThanOrEqual(
refundedSnapshot.timestamps.updatedAt
);
// updated_at is second-granularity (strftime('%s','now')), and the two
// orders can straddle a second boundary, so assert the sort invariant
// directly instead of comparing the two orders' timestamps against each
// other (which flakes whenever the boundary falls mid-test).
const updatedAts = snapshots.map((s) => s.timestamps.updatedAt);
for (let i = 1; i < updatedAts.length; i++) {
const prev = updatedAts[i - 1]!;
const curr = updatedAts[i]!;
expect(prev).toBeGreaterThanOrEqual(curr);
}
// The array is sorted DESC, so whichever has the higher updatedAt is first.
const [first, second] = snapshots as [OrderSnapshot, OrderSnapshot];
const isCompletedFirst = first.orderId === completed1.publicId;
Expand Down
Loading