diff --git a/.env.example b/.env.example index 476d594..e0f7bc5 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,8 @@ FOC_API_URL=https://your-deployment.example.com # Server port FOC_SERVER_PORT=17824 -# Lotus RPC endpoints +# Lotus RPC endpoints. Compose passes each URL to a separate indexer and +# selects its network with PONDER_NETWORK; no network-specific image is needed. # For local Lotus gateway via the lotus-proxy container: # CALIBNET_RPC_URL=http://host.docker.internal:11235/rpc/v1 # MAINNET_RPC_URL=http://host.docker.internal:11234/rpc/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6b27ba4..fe7c279 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,7 +56,9 @@ jobs: cache: npm cache-dependency-path: indexer/package-lock.json - run: npm ci - # typecheck symlinks ponder.config.mainnet.ts -> ponder.config.ts, - # runs ponder codegen to materialize ponder-env.d.ts, then tsc --noEmit. - # No DB or RPC needed; codegen accepts dummy connection strings. + # Tests load the actual config under both networks and strict-mode failures. + - run: npm test + # Typecheck runs mainnet codegen to materialize ponder-env.d.ts, then + # tsc --noEmit. No DB or RPC is needed; codegen accepts dummy values. + - run: npm run codegen:calibnet - run: npm run typecheck diff --git a/README.md b/README.md index d6d4adc..deefe20 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,13 @@ cp .env.example .env docker compose up -d ``` +When upgrading to this unified configuration, rebuild the indexers before restarting them. The unified +`ponder.config.ts` is baked into the indexer image, so mounting the updated source directories alone does not update it: + +```bash +docker compose up -d --build ponder-calibnet ponder-mainnet +``` + ### Configuration All configuration is via `.env`. Copy `.env.example` for a template. @@ -219,7 +226,19 @@ npm test ### Indexer -The Ponder indexer uses a [Filecoin-compatible fork](https://github.com/rvagg/filecoin-ponder) of Ponder. The Docker build clones this fork automatically. +The Ponder indexer uses a [Filecoin-compatible fork](https://github.com/rvagg/filecoin-ponder) of Ponder. Set `PONDER_NETWORK` to `mainnet` or `calibnet`; the default is `mainnet` for local development. Set `PONDER_STRICT_ENV=true` to require `PONDER_NETWORK`, `DATABASE_URL`, and `RPC_URL`, as the Docker Compose services do. + +Useful commands: + +```bash +cd indexer +npm run dev:mainnet +npm run dev:calibnet +npm run codegen:mainnet +npm run codegen:calibnet +npm test +npm run typecheck +``` To regenerate contract ABIs: diff --git a/docker-compose.yml b/docker-compose.yml index 30d37dc..af23b2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,6 +52,8 @@ services: extra_hosts: - "host.docker.internal:host-gateway" environment: + PONDER_NETWORK: calibnet + PONDER_STRICT_ENV: "true" DATABASE_URL: postgres://ponder:ponder@postgres-calibnet:5432/ponder RPC_URL: ${CALIBNET_RPC_URL:?} PORT: 17827 @@ -63,7 +65,6 @@ services: command: ["npm", "start", "--", "--schema", "${CALIBNET_DATA_SCHEMA:-data_v1}", "--views-schema", "public"] restart: unless-stopped volumes: - - ./indexer/ponder.config.calibnet.ts:/app/ponder.config.ts - ./indexer/ponder.schema.ts:/app/ponder.schema.ts - ./indexer/src:/app/src - ./indexer/abis:/app/abis @@ -76,6 +77,8 @@ services: extra_hosts: - "host.docker.internal:host-gateway" environment: + PONDER_NETWORK: mainnet + PONDER_STRICT_ENV: "true" DATABASE_URL: postgres://ponder:ponder@postgres-mainnet:5432/ponder RPC_URL: ${MAINNET_RPC_URL:?} PORT: 17828 @@ -83,7 +86,6 @@ services: command: ["npm", "start", "--", "--schema", "${MAINNET_DATA_SCHEMA:-data_v1}", "--views-schema", "public"] restart: unless-stopped volumes: - - ./indexer/ponder.config.mainnet.ts:/app/ponder.config.ts - ./indexer/ponder.schema.ts:/app/ponder.schema.ts - ./indexer/src:/app/src - ./indexer/abis:/app/abis diff --git a/indexer/.dockerignore b/indexer/.dockerignore index 54db41b..b1640b5 100644 --- a/indexer/.dockerignore +++ b/indexer/.dockerignore @@ -2,4 +2,3 @@ node_modules dist .env .env.local -ponder.config*.ts diff --git a/indexer/.gitignore b/indexer/.gitignore index e1cdb86..79cae59 100644 --- a/indexer/.gitignore +++ b/indexer/.gitignore @@ -3,7 +3,3 @@ dist/ .ponder/ generated/ ponder-env.d.ts -# Symlinked at runtime (Docker volume mount selects the network) and at -# typecheck time (npm run typecheck symlinks mainnet's config). Either way -# this file is generated, not authored. -ponder.config.ts diff --git a/indexer/Dockerfile b/indexer/Dockerfile index 0b4f3f9..15fd23f 100644 --- a/indexer/Dockerfile +++ b/indexer/Dockerfile @@ -5,14 +5,11 @@ WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --no-audit --no-fund -COPY tsconfig.json ponder.schema.ts ./ +COPY tsconfig.json ponder.config.ts ponder.schema.ts ./ COPY abis ./abis COPY src ./src -COPY docker-entrypoint.sh ./ -RUN chmod +x /app/docker-entrypoint.sh ENV NO_COLOR=1 RUN chown -R node:node /app USER node -ENTRYPOINT ["/app/docker-entrypoint.sh"] CMD ["npm", "start"] diff --git a/indexer/docker-entrypoint.sh b/indexer/docker-entrypoint.sh deleted file mode 100644 index 1a30fc5..0000000 --- a/indexer/docker-entrypoint.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -eu - -if [ ! -r ponder.config.ts ]; then - echo "Missing /app/ponder.config.ts. Mount a network-specific Ponder config before starting the indexer." >&2 - exit 1 -fi - -exec "$@" diff --git a/indexer/package.json b/indexer/package.json index 3df057b..d05266b 100644 --- a/indexer/package.json +++ b/indexer/package.json @@ -4,10 +4,17 @@ "type": "module", "scripts": { "dev": "ponder dev", + "dev:calibnet": "PONDER_NETWORK=calibnet ponder dev", + "dev:mainnet": "PONDER_NETWORK=mainnet ponder dev", "start": "ponder start", + "start:calibnet": "PONDER_NETWORK=calibnet ponder start", + "start:mainnet": "PONDER_NETWORK=mainnet ponder start", "codegen": "ponder codegen", + "codegen:calibnet": "PONDER_NETWORK=calibnet RPC_URL=http://localhost:1235/rpc/v1 DATABASE_URL=postgres://noop:noop@localhost:5432/noop ponder codegen", + "codegen:mainnet": "PONDER_NETWORK=mainnet RPC_URL=http://localhost:1234/rpc/v1 DATABASE_URL=postgres://noop:noop@localhost:5432/noop ponder codegen", "generate-abis": "./scripts/generate-abis.sh", - "typecheck": "ln -sf ponder.config.mainnet.ts ponder.config.ts && RPC_URL=http://localhost:1234 DATABASE_URL=postgres://noop:noop@localhost:5432/noop ponder codegen && tsc --noEmit; rc=$?; rm -f ponder.config.ts; exit $rc" + "test": "node --experimental-strip-types --test test/*.test.ts", + "typecheck": "npm run codegen:mainnet && tsc --noEmit" }, "dependencies": { "hono": "^4.12", diff --git a/indexer/ponder.config.calibnet.ts b/indexer/ponder.config.calibnet.ts deleted file mode 100644 index 4b93dfc..0000000 --- a/indexer/ponder.config.calibnet.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { createConfig } from "ponder" -import { PDPVerifierAbi } from "./abis/PDPVerifier" -import { FilecoinWarmStorageServiceAbi } from "./abis/FilecoinWarmStorageService" -import { FilecoinPayV1Abi } from "./abis/FilecoinPayV1" -import { ServiceProviderRegistryAbi } from "./abis/ServiceProviderRegistry" -import { SessionKeyRegistryAbi } from "./abis/SessionKeyRegistry" -import { FilBeamOperatorAbi } from "./abis/FilBeamOperator" - -// Calibnet proxy addresses (deployed at v1.0.0, same addresses across UUPS upgrades) -// Source: https://github.com/FilOzone/filecoin-services/blob/main/service_contracts/deployments.json -const CALIBNET = { - PDP_VERIFIER: "0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C", - FWSS: "0x02925630df557F957f70E112bA06e50965417CA0", - FILECOIN_PAY: "0x09a0fDc2723fAd1A7b8e3e00eE5DF73841df55a0", - SP_REGISTRY: "0x839e5c9988e4e9977d40708d0094103c0839Ac9D", - SESSION_KEY_REGISTRY: "0x518411c2062E119Aaf7A8B12A2eDf9a939347655", - // Storacha runs a separate FWSS-fork listener on the same PDPVerifier and FilecoinPay - STORACHA_FWSS: "0x0c6875983B20901a7C3c86871f43FdEE77946424", - // FilBeam CDN bandwidth ledger; non-upgradeable, redeployed periodically. - // Addresses derived from FWSS:FilBeamControllerChanged event history. - FILBEAM_OPERATORS: [ - "0x5f7e5e2a756430edee781ff6e6f7954254ef629a", // initial - "0x50abae0b3d37a2fa4d3a7f62fbd49f9a566aeb95", // v1.0.0 - "0xea6631b25ba4c9c9e285da25a03aa96acc921530", // v1.0.1 - "0x5991e4f9fcef4ae23959ee03638b4688a7e1ecff", // v1.0.2 (current) - ], -} as const - -// Snapshot floor is 3,090,000 (null round); start +2 so the parent is valid. -// Captures FilecoinPay rail 1 (3,125,305) and PDPVerifier sets 1-48 (3,144,601 onward). -const START_BLOCK = 3_090_002 - -export default createConfig({ - database: { - kind: "postgres", - connectionString: process.env.DATABASE_URL ?? "postgres://ponder:ponder@localhost:17825/ponder", - }, - chains: { - calibnet: { - id: 314159, - rpc: process.env.RPC_URL ?? "http://localhost:1235/rpc/v1", - pollingInterval: 30_000, - }, - }, - accounts: { - FilecoinPayAccount: { - address: CALIBNET.FILECOIN_PAY, - chain: "calibnet", - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - }, - contracts: { - PDPVerifier: { - abi: PDPVerifierAbi, - chain: "calibnet", - address: CALIBNET.PDP_VERIFIER, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - FWSS: { - abi: FilecoinWarmStorageServiceAbi, - chain: "calibnet", - address: CALIBNET.FWSS, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - FilecoinPay: { - abi: FilecoinPayV1Abi, - chain: "calibnet", - address: CALIBNET.FILECOIN_PAY, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - SPRegistry: { - abi: ServiceProviderRegistryAbi, - chain: "calibnet", - address: CALIBNET.SP_REGISTRY, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - SessionKeyRegistry: { - abi: SessionKeyRegistryAbi, - chain: "calibnet", - address: CALIBNET.SESSION_KEY_REGISTRY, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - StorachaFWSS: { - abi: FilecoinWarmStorageServiceAbi, - chain: "calibnet", - address: CALIBNET.STORACHA_FWSS, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - FilBeamOperator: { - abi: FilBeamOperatorAbi, - chain: "calibnet", - address: CALIBNET.FILBEAM_OPERATORS, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - }, -}) diff --git a/indexer/ponder.config.mainnet.ts b/indexer/ponder.config.mainnet.ts deleted file mode 100644 index e976149..0000000 --- a/indexer/ponder.config.mainnet.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { createConfig } from "ponder" -import { PDPVerifierAbi } from "./abis/PDPVerifier" -import { FilecoinWarmStorageServiceAbi } from "./abis/FilecoinWarmStorageService" -import { FilecoinPayV1Abi } from "./abis/FilecoinPayV1" -import { ServiceProviderRegistryAbi } from "./abis/ServiceProviderRegistry" -import { SessionKeyRegistryAbi } from "./abis/SessionKeyRegistry" -import { FilBeamOperatorAbi } from "./abis/FilBeamOperator" - -// Mainnet proxy addresses (deployed at v1.0.0, same addresses across UUPS upgrades) -// Source: https://github.com/FilOzone/filecoin-services/blob/main/service_contracts/deployments.json -const MAINNET = { - PDP_VERIFIER: "0xBADd0B92C1c71d02E7d520f64c0876538fa2557F", - FWSS: "0x8408502033C418E1bbC97cE9ac48E5528F371A9f", - FILECOIN_PAY: "0x23b1e018F08BB982348b15a86ee926eEBf7F4DAa", - SP_REGISTRY: "0xf55dDbf63F1b55c3F1D4FA7e339a68AB7b64A5eB", - SESSION_KEY_REGISTRY: "0x74FD50525A958aF5d484601E252271f9625231aB", - // Storacha runs a separate FWSS-fork listener on the same PDPVerifier and FilecoinPay - STORACHA_FWSS: "0x56e53c5e7F27504b810494cc3b88b2aa0645a839", - // FilBeam CDN bandwidth ledger; non-upgradeable, redeployed periodically. - // Addresses derived from FWSS:FilBeamControllerChanged event history. - FILBEAM_OPERATORS: [ - "0x5f7e5e2a756430edee781ff6e6f7954254ef629a", // initial - "0xea6631b25ba4c9c9e285da25a03aa96acc921530", // v1.0.1 - "0x9e90749d298c4ca43bb468ca859dfe167f9cdcf2", // v1.0.2 (current) - ], -} as const - -// v1.0.0 deployed ~epoch 5,220,000 on mainnet (Nov 2, 2025). Start slightly before. -const START_BLOCK = 5_215_000 - -export default createConfig({ - database: { - kind: "postgres", - connectionString: process.env.DATABASE_URL ?? "postgres://ponder:ponder@localhost:17826/ponder", - }, - chains: { - mainnet: { - id: 314, - rpc: process.env.RPC_URL ?? "http://localhost:1234/rpc/v1", - pollingInterval: 30_000, - }, - }, - accounts: { - FilecoinPayAccount: { - address: MAINNET.FILECOIN_PAY, - chain: "mainnet", - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - }, - contracts: { - PDPVerifier: { - abi: PDPVerifierAbi, - chain: "mainnet", - address: MAINNET.PDP_VERIFIER, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - FWSS: { - abi: FilecoinWarmStorageServiceAbi, - chain: "mainnet", - address: MAINNET.FWSS, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - FilecoinPay: { - abi: FilecoinPayV1Abi, - chain: "mainnet", - address: MAINNET.FILECOIN_PAY, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - SPRegistry: { - abi: ServiceProviderRegistryAbi, - chain: "mainnet", - address: MAINNET.SP_REGISTRY, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - SessionKeyRegistry: { - abi: SessionKeyRegistryAbi, - chain: "mainnet", - address: MAINNET.SESSION_KEY_REGISTRY, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - StorachaFWSS: { - abi: FilecoinWarmStorageServiceAbi, - chain: "mainnet", - address: MAINNET.STORACHA_FWSS, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - FilBeamOperator: { - abi: FilBeamOperatorAbi, - chain: "mainnet", - address: MAINNET.FILBEAM_OPERATORS, - startBlock: START_BLOCK, - includeTransactionReceipts: true, - }, - }, -}) diff --git a/indexer/ponder.config.ts b/indexer/ponder.config.ts new file mode 100644 index 0000000..9622f72 --- /dev/null +++ b/indexer/ponder.config.ts @@ -0,0 +1,85 @@ +import { createConfig } from "ponder" +import { PDPVerifierAbi } from "./abis/PDPVerifier.ts" +import { FilecoinWarmStorageServiceAbi } from "./abis/FilecoinWarmStorageService.ts" +import { FilecoinPayV1Abi } from "./abis/FilecoinPayV1.ts" +import { ServiceProviderRegistryAbi } from "./abis/ServiceProviderRegistry.ts" +import { SessionKeyRegistryAbi } from "./abis/SessionKeyRegistry.ts" +import { FilBeamOperatorAbi } from "./abis/FilBeamOperator.ts" +import { NETWORKS, parseNetwork, parseStrictEnv, readEnv } from "./src/networks.ts" + +const strictEnv = parseStrictEnv(process.env.PONDER_STRICT_ENV) +const networkName = parseNetwork(process.env.PONDER_NETWORK, strictEnv) +const network = NETWORKS[networkName] + +export default createConfig({ + database: { + kind: "postgres", + connectionString: readEnv("DATABASE_URL", network.DATABASE_URL, strictEnv), + }, + chains: { + [networkName]: { + id: network.CHAIN_ID, + rpc: readEnv("RPC_URL", network.RPC_URL, strictEnv), + pollingInterval: 30_000, + }, + }, + accounts: { + FilecoinPayAccount: { + address: network.FILECOIN_PAY, + chain: networkName, + startBlock: network.START_BLOCK, + includeTransactionReceipts: true, + }, + }, + contracts: { + PDPVerifier: { + abi: PDPVerifierAbi, + chain: networkName, + address: network.PDP_VERIFIER, + startBlock: network.START_BLOCK, + includeTransactionReceipts: true, + }, + FWSS: { + abi: FilecoinWarmStorageServiceAbi, + chain: networkName, + address: network.FWSS, + startBlock: network.START_BLOCK, + includeTransactionReceipts: true, + }, + FilecoinPay: { + abi: FilecoinPayV1Abi, + chain: networkName, + address: network.FILECOIN_PAY, + startBlock: network.START_BLOCK, + includeTransactionReceipts: true, + }, + SPRegistry: { + abi: ServiceProviderRegistryAbi, + chain: networkName, + address: network.SP_REGISTRY, + startBlock: network.START_BLOCK, + includeTransactionReceipts: true, + }, + SessionKeyRegistry: { + abi: SessionKeyRegistryAbi, + chain: networkName, + address: network.SESSION_KEY_REGISTRY, + startBlock: network.START_BLOCK, + includeTransactionReceipts: true, + }, + StorachaFWSS: { + abi: FilecoinWarmStorageServiceAbi, + chain: networkName, + address: network.STORACHA_FWSS, + startBlock: network.START_BLOCK, + includeTransactionReceipts: true, + }, + FilBeamOperator: { + abi: FilBeamOperatorAbi, + chain: networkName, + address: network.FILBEAM_OPERATORS, + startBlock: network.START_BLOCK, + includeTransactionReceipts: true, + }, + }, +}) diff --git a/indexer/src/networks.ts b/indexer/src/networks.ts new file mode 100644 index 0000000..1af0144 --- /dev/null +++ b/indexer/src/networks.ts @@ -0,0 +1,93 @@ +export type NetworkName = "mainnet" | "calibnet" + +type Network = { + CHAIN_ID: number + DATABASE_URL: string + RPC_URL: string + START_BLOCK: number + PDP_VERIFIER: `0x${string}` + FWSS: `0x${string}` + FILECOIN_PAY: `0x${string}` + SP_REGISTRY: `0x${string}` + SESSION_KEY_REGISTRY: `0x${string}` + STORACHA_FWSS: `0x${string}` + FILBEAM_OPERATORS: readonly `0x${string}`[] +} + +// Proxy addresses are stable across UUPS upgrades. +// Source: https://github.com/FilOzone/filecoin-services/blob/main/service_contracts/deployments.json +export const MAINNET = { + CHAIN_ID: 314, + DATABASE_URL: "postgres://ponder:ponder@localhost:17826/ponder", + RPC_URL: "http://localhost:1234/rpc/v1", + // v1.0.0 deployed ~epoch 5,220,000 on mainnet (Nov 2, 2025). Start slightly before. + START_BLOCK: 5_215_000, + PDP_VERIFIER: "0xBADd0B92C1c71d02E7d520f64c0876538fa2557F", + FWSS: "0x8408502033C418E1bbC97cE9ac48E5528F371A9f", + FILECOIN_PAY: "0x23b1e018F08BB982348b15a86ee926eEBf7F4DAa", + SP_REGISTRY: "0xf55dDbf63F1b55c3F1D4FA7e339a68AB7b64A5eB", + SESSION_KEY_REGISTRY: "0x74FD50525A958aF5d484601E252271f9625231aB", + // Storacha runs a separate FWSS-fork listener on the same PDPVerifier and FilecoinPay. + STORACHA_FWSS: "0x56e53c5e7F27504b810494cc3b88b2aa0645a839", + // FilBeam CDN bandwidth ledger; non-upgradeable, redeployed periodically. + // Addresses derived from FWSS:FilBeamControllerChanged event history. + FILBEAM_OPERATORS: [ + "0x5f7e5e2a756430edee781ff6e6f7954254ef629a", // initial + "0xea6631b25ba4c9c9e285da25a03aa96acc921530", // v1.0.1 + "0x9e90749d298c4ca43bb468ca859dfe167f9cdcf2", // v1.0.2 (current) + ], +} as const satisfies Network + +export const CALIBNET = { + CHAIN_ID: 314159, + DATABASE_URL: "postgres://ponder:ponder@localhost:17825/ponder", + RPC_URL: "http://localhost:1235/rpc/v1", + // Snapshot floor is 3,090,000 (null round); start +2 so the parent is valid. + // Captures FilecoinPay rail 1 (3,125,305) and PDPVerifier sets 1-48 (3,144,601 onward). + START_BLOCK: 3_090_002, + PDP_VERIFIER: "0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C", + FWSS: "0x02925630df557F957f70E112bA06e50965417CA0", + FILECOIN_PAY: "0x09a0fDc2723fAd1A7b8e3e00eE5DF73841df55a0", + SP_REGISTRY: "0x839e5c9988e4e9977d40708d0094103c0839Ac9D", + SESSION_KEY_REGISTRY: "0x518411c2062E119Aaf7A8B12A2eDf9a939347655", + // Storacha runs a separate FWSS-fork listener on the same PDPVerifier and FilecoinPay. + STORACHA_FWSS: "0x0c6875983B20901a7C3c86871f43FdEE77946424", + // FilBeam CDN bandwidth ledger; non-upgradeable, redeployed periodically. + // Addresses derived from FWSS:FilBeamControllerChanged event history. + FILBEAM_OPERATORS: [ + "0x5f7e5e2a756430edee781ff6e6f7954254ef629a", // initial + "0x50abae0b3d37a2fa4d3a7f62fbd49f9a566aeb95", // v1.0.0 + "0xea6631b25ba4c9c9e285da25a03aa96acc921530", // v1.0.1 + "0x5991e4f9fcef4ae23959ee03638b4688a7e1ecff", // v1.0.2 (current) + ], +} as const satisfies Network + +export const NETWORKS = { + mainnet: MAINNET, + calibnet: CALIBNET, +} as const satisfies Record + +export function parseStrictEnv(value: string | undefined): boolean { + return value === "1" || value === "true" +} + +export function parseNetwork(value: string | undefined, strict: boolean): NetworkName { + if (value === undefined || value === "") { + if (strict) throw new Error("PONDER_NETWORK is required when PONDER_STRICT_ENV=true") + return "mainnet" + } + if (value === "mainnet" || value === "calibnet") return value + throw new Error(`Unsupported PONDER_NETWORK "${value}". Expected "mainnet" or "calibnet".`) +} + +export function readEnv( + name: string, + fallback: string, + strict: boolean, + environment: NodeJS.ProcessEnv = process.env, +): string { + const value = environment[name] + if (value !== undefined && value !== "") return value + if (strict) throw new Error(`${name} is required when PONDER_STRICT_ENV=true`) + return fallback +} diff --git a/indexer/test/networks.test.ts b/indexer/test/networks.test.ts new file mode 100644 index 0000000..550a2fb --- /dev/null +++ b/indexer/test/networks.test.ts @@ -0,0 +1,156 @@ +import assert from "node:assert/strict" +import { spawnSync } from "node:child_process" +import { test } from "node:test" +import { NETWORKS, parseNetwork, parseStrictEnv, readEnv } from "../src/networks.ts" + +const configUrl = new URL("../ponder.config.ts", import.meta.url).href +const inspectConfigScript = ` +const config = (await import(${JSON.stringify(configUrl)})).default +const networkName = Object.keys(config.chains)[0] +console.log(JSON.stringify({ + networkName, + chain: config.chains[networkName], + database: config.database, + fwss: { + chain: config.contracts.FWSS.chain, + address: config.contracts.FWSS.address, + startBlock: config.contracts.FWSS.startBlock, + includeTransactionReceipts: config.contracts.FWSS.includeTransactionReceipts, + }, +})) +` + +function loadConfig(environment: Record = {}) { + const env: NodeJS.ProcessEnv = { ...process.env, NODE_NO_WARNINGS: "1" } + delete env.PONDER_NETWORK + delete env.PONDER_STRICT_ENV + delete env.DATABASE_URL + delete env.RPC_URL + Object.assign(env, environment) + + return spawnSync(process.execPath, ["--experimental-strip-types", "--input-type=module", "--eval", inspectConfigScript], { + encoding: "utf8", + env, + }) +} + +test("parseNetwork defaults to mainnet only outside strict mode", () => { + assert.equal(parseNetwork(undefined, false), "mainnet") + assert.equal(parseNetwork("", false), "mainnet") + assert.throws(() => parseNetwork(undefined, true), /PONDER_NETWORK is required/) + assert.throws(() => parseNetwork("", true), /PONDER_NETWORK is required/) +}) + +test("parseNetwork accepts supported networks and rejects other values", () => { + assert.equal(parseNetwork("mainnet", false), "mainnet") + assert.equal(parseNetwork("calibnet", true), "calibnet") + assert.throws(() => parseNetwork("testnet", false), /Unsupported PONDER_NETWORK "testnet"/) +}) + +test("parseStrictEnv accepts only the documented enabled values", () => { + assert.equal(parseStrictEnv("true"), true) + assert.equal(parseStrictEnv("1"), true) + assert.equal(parseStrictEnv("false"), false) + assert.equal(parseStrictEnv(undefined), false) +}) + +test("readEnv uses overrides and requires values in strict mode", () => { + assert.equal(readEnv("RPC_URL", "fallback", false, {}), "fallback") + assert.equal(readEnv("RPC_URL", "fallback", false, { RPC_URL: "override" }), "override") + assert.throws(() => readEnv("RPC_URL", "fallback", true, {}), /RPC_URL is required/) + assert.throws(() => readEnv("DATABASE_URL", "fallback", true, { DATABASE_URL: "" }), /DATABASE_URL is required/) +}) + +test("network registry preserves mainnet deployment configuration", () => { + assert.deepEqual(NETWORKS.mainnet, { + CHAIN_ID: 314, + DATABASE_URL: "postgres://ponder:ponder@localhost:17826/ponder", + RPC_URL: "http://localhost:1234/rpc/v1", + START_BLOCK: 5_215_000, + PDP_VERIFIER: "0xBADd0B92C1c71d02E7d520f64c0876538fa2557F", + FWSS: "0x8408502033C418E1bbC97cE9ac48E5528F371A9f", + FILECOIN_PAY: "0x23b1e018F08BB982348b15a86ee926eEBf7F4DAa", + SP_REGISTRY: "0xf55dDbf63F1b55c3F1D4FA7e339a68AB7b64A5eB", + SESSION_KEY_REGISTRY: "0x74FD50525A958aF5d484601E252271f9625231aB", + STORACHA_FWSS: "0x56e53c5e7F27504b810494cc3b88b2aa0645a839", + FILBEAM_OPERATORS: [ + "0x5f7e5e2a756430edee781ff6e6f7954254ef629a", + "0xea6631b25ba4c9c9e285da25a03aa96acc921530", + "0x9e90749d298c4ca43bb468ca859dfe167f9cdcf2", + ], + }) +}) + +test("network registry preserves calibnet deployment configuration", () => { + assert.deepEqual(NETWORKS.calibnet, { + CHAIN_ID: 314159, + DATABASE_URL: "postgres://ponder:ponder@localhost:17825/ponder", + RPC_URL: "http://localhost:1235/rpc/v1", + START_BLOCK: 3_090_002, + PDP_VERIFIER: "0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C", + FWSS: "0x02925630df557F957f70E112bA06e50965417CA0", + FILECOIN_PAY: "0x09a0fDc2723fAd1A7b8e3e00eE5DF73841df55a0", + SP_REGISTRY: "0x839e5c9988e4e9977d40708d0094103c0839Ac9D", + SESSION_KEY_REGISTRY: "0x518411c2062E119Aaf7A8B12A2eDf9a939347655", + STORACHA_FWSS: "0x0c6875983B20901a7C3c86871f43FdEE77946424", + FILBEAM_OPERATORS: [ + "0x5f7e5e2a756430edee781ff6e6f7954254ef629a", + "0x50abae0b3d37a2fa4d3a7f62fbd49f9a566aeb95", + "0xea6631b25ba4c9c9e285da25a03aa96acc921530", + "0x5991e4f9fcef4ae23959ee03638b4688a7e1ecff", + ], + }) +}) + +test("actual Ponder config rejects unsupported networks and missing strict variables", () => { + let result = loadConfig({ PONDER_NETWORK: "testnet" }) + assert.notEqual(result.status, 0) + assert.match(result.stderr, /Unsupported PONDER_NETWORK "testnet"/) + + result = loadConfig({ PONDER_STRICT_ENV: "true" }) + assert.notEqual(result.status, 0) + assert.match(result.stderr, /PONDER_NETWORK is required/) + + result = loadConfig({ PONDER_STRICT_ENV: "true", PONDER_NETWORK: "mainnet" }) + assert.notEqual(result.status, 0) + assert.match(result.stderr, /DATABASE_URL is required/) + + result = loadConfig({ + PONDER_STRICT_ENV: "true", + PONDER_NETWORK: "mainnet", + DATABASE_URL: "postgres://noop:noop@localhost/noop", + }) + assert.notEqual(result.status, 0) + assert.match(result.stderr, /RPC_URL is required/) +}) + +test("actual Ponder config selects mainnet and calibnet from the environment", () => { + const databaseUrl = "postgres://noop:noop@localhost/noop" + const rpcUrl = "http://localhost:1234/rpc/v1" + + for (const expected of [ + { networkName: "mainnet", chainId: 314, fwss: NETWORKS.mainnet.FWSS, startBlock: NETWORKS.mainnet.START_BLOCK }, + { networkName: "calibnet", chainId: 314159, fwss: NETWORKS.calibnet.FWSS, startBlock: NETWORKS.calibnet.START_BLOCK }, + ]) { + const result = loadConfig({ + PONDER_STRICT_ENV: "true", + PONDER_NETWORK: expected.networkName, + DATABASE_URL: databaseUrl, + RPC_URL: rpcUrl, + }) + assert.equal(result.status, 0, result.stderr) + + const config = JSON.parse(result.stdout) + assert.deepEqual(config, { + networkName: expected.networkName, + chain: { id: expected.chainId, rpc: rpcUrl, pollingInterval: 30_000 }, + database: { kind: "postgres", connectionString: databaseUrl }, + fwss: { + chain: expected.networkName, + address: expected.fwss, + startBlock: expected.startBlock, + includeTransactionReceipts: true, + }, + }) + } +}) diff --git a/indexer/tsconfig.json b/indexer/tsconfig.json index 1af766a..8a82fd9 100644 --- a/indexer/tsconfig.json +++ b/indexer/tsconfig.json @@ -9,6 +9,8 @@ // Ponder requires bundler resolution for virtual modules (ponder:registry, ponder:schema) "module": "ESNext", "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, "resolveJsonModule": true, "isolatedModules": true, @@ -19,11 +21,5 @@ "skipLibCheck": true }, - "include": [ - "src", - "ponder.config.calibnet.ts", - "ponder.config.mainnet.ts", - "ponder.schema.ts", - "ponder-env.d.ts" - ] + "include": ["src", "test", "ponder.config.ts", "ponder.schema.ts", "ponder-env.d.ts"] } diff --git a/shared/system-context.ts b/shared/system-context.ts index 8aaa8be..6c7f6af 100644 --- a/shared/system-context.ts +++ b/shared/system-context.ts @@ -429,7 +429,7 @@ The current FOC contracts (same proxy addresses) were deployed across two releas - Deployment of the current proxy addresses on both networks. - Calibnet proxy deployment: FilecoinPay ~epoch 3,120,400 (2025-10-20), PDPVerifier/FWSS/SPRegistry ~epoch 3,141,300 (2025-10-27). First indexed event: fp_deposit at block 3,125,196 (2025-10-21). - Mainnet proxy deployment: FilecoinPay ~epoch 5,425,000, PDPVerifier ~5,441,500, FWSS ~5,459,500. First indexed event: fp_deposit at block 5,465,823 (2025-11-04) - ~40k epoch gap between deployment and first user activity. -- For a precise indexed-from block per network, query \`MIN(block_number)\` on a high-coverage table (e.g. \`fp_deposit\`). The \`--startBlock\` in \`indexer/ponder.config..ts\` sits below first activity. +- For a precise indexed-from block per network, query \`MIN(block_number)\` on a high-coverage table (e.g. \`fp_deposit\`). The \`START_BLOCK\` in \`indexer/src/networks.ts\` sits below first activity. - Introduced: FWSS GA contracts, FilecoinPay v1, PDPVerifier v3.1.0, ServiceProviderRegistry with capability key-value store, SessionKeyRegistry. - Source: [filecoin-services v1.0.0](https://github.com/FilOzone/filecoin-services/releases/tag/v1.0.0)