From 6b43b882d7d21a3c3a03be832fa1b2d857236e6b Mon Sep 17 00:00:00 2001 From: Jeffrey Graham Date: Tue, 10 Nov 2026 04:31:11 -0500 Subject: [PATCH] fix(indexer): require SOROBAN_RPC_URL without silent testnet fallback --- indexer/README.md | 5 +++-- indexer/src/index.test.ts | 39 +++++++++++++++++++++++++++++++++++++-- indexer/src/index.ts | 11 ++++++++--- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/indexer/README.md b/indexer/README.md index 832b2d2..4e50979 100644 --- a/indexer/README.md +++ b/indexer/README.md @@ -8,8 +8,9 @@ An optional indexer that polls Soroban RPC and persists contract deployments, in cd indexer npm install -# Set the database connection string +# Set the required database connection string and Soroban RPC endpoint export DATABASE_URL="postgresql://user:password@localhost:5432/prism" +export SOROBAN_RPC_URL="https://soroban-testnet.stellar.org" # Generate the Prisma client and run migrations npm run prisma:generate @@ -38,7 +39,7 @@ docker compose up | Environment Variable | Default | Description | | -------------------- | ------------------------------------ | ------------------------------------ | | `DATABASE_URL` | *(required)* | PostgreSQL connection string | -| `SOROBAN_RPC_URL` | `https://soroban-testnet.stellar.org` | Soroban RPC endpoint to poll | +| `SOROBAN_RPC_URL` | *(required)* | Soroban RPC endpoint to poll | | `POLL_INTERVAL_MS` | `5000` | Milliseconds between poll cycles | | `HEALTH_PORT` | `8080` | Port for the `GET /healthz` endpoint | | `HEALTH_MAX_LEDGER_LAG` | `60` | Ledgers behind the network before `/healthz` returns 503 | diff --git a/indexer/src/index.test.ts b/indexer/src/index.test.ts index e8293eb..4d917ab 100644 --- a/indexer/src/index.test.ts +++ b/indexer/src/index.test.ts @@ -1,5 +1,10 @@ -import { describe, expect, it } from "vitest"; -import { backoffDelay, commitPage, healthReport, seedLastLedger } from "./index.js"; +import { describe, expect, it, vi } from "vitest"; + +vi.hoisted(() => { + process.env.SOROBAN_RPC_URL ??= "https://soroban-testnet.stellar.org"; +}); + +import { backoffDelay, commitPage, healthReport, requireEnv, seedLastLedger } from "./index.js"; describe("backoffDelay", () => { it("doubles the delay for each consecutive failure", () => { @@ -127,3 +132,33 @@ describe("commitPage", () => { expect(committed.cursor).toEqual({ lastLedger: 100, paginationCursor: null }); }); }); + + +describe("requireEnv", () => { + it("returns the configured environment variable when set", () => { + expect( + requireEnv("SOROBAN_RPC_URL", undefined, { + SOROBAN_RPC_URL: "https://soroban-testnet.stellar.org", + }) + ).toBe("https://soroban-testnet.stellar.org"); + }); + + it("logs the fatal error and exits with code 1 when SOROBAN_RPC_URL is unset or blank", () => { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { + throw new Error(`process.exit(${code})`); + }) as never); + + try { + for (const env of [{}, { SOROBAN_RPC_URL: " " }]) { + expect(() => requireEnv("SOROBAN_RPC_URL", undefined, env)).toThrow("process.exit(1)"); + expect(errorSpy).toHaveBeenCalledWith( + "[indexer] fatal: environment variable SOROBAN_RPC_URL is required but not set" + ); + } + } finally { + errorSpy.mockRestore(); + exitSpy.mockRestore(); + } + }); +}); diff --git a/indexer/src/index.ts b/indexer/src/index.ts index fb07c50..752469d 100644 --- a/indexer/src/index.ts +++ b/indexer/src/index.ts @@ -14,8 +14,13 @@ import { // --------------------------------------------------------------------------- // Environment variable validation (#20) // --------------------------------------------------------------------------- -function requireEnv(name: string, fallback?: string): string { - const val = process.env[name] ?? fallback; +export function requireEnv( + name: string, + fallback?: string, + env: NodeJS.ProcessEnv = process.env +): string { + const raw = env[name]?.trim(); + const val = raw ? raw : fallback; if (val === undefined) { console.error(`[indexer] fatal: environment variable ${name} is required but not set`); process.exit(1); @@ -36,7 +41,7 @@ function requirePositiveInt(name: string, fallback: number): number { return n; } -const RPC_URL = requireEnv("SOROBAN_RPC_URL", "https://soroban-testnet.stellar.org"); +const RPC_URL = requireEnv("SOROBAN_RPC_URL"); const POLL_INTERVAL_MS = requirePositiveInt("POLL_INTERVAL_MS", 5000); const HEALTH_PORT = requirePositiveInt("HEALTH_PORT", 8080); const MAX_LEDGER_LAG = requirePositiveInt("HEALTH_MAX_LEDGER_LAG", 60);