Skip to content
Merged
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ to run the **internal** build locally with those sections visible. See

- **typecheck** — `tsc --noEmit`
- **lint** — eslint
- **test** — vitest
- **test** — vitest. Includes `app/snapshots/networks.contract.test.ts`, which
asserts every expected network stays served by `/api/snapshots`. Nodes sync
from those buckets, so a network must never be dropped just to take it off the
page — hide it with `hiddenFromUi` instead.
- **docs (generated agent index)** — fails if the committed `public/llms.txt`,
`llms-full.txt`, or `AGENTS.md` are stale relative to the route tree. Fix with
`npm run llms && npm run agents`.
Expand Down
15 changes: 14 additions & 1 deletion app/snapshots/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type SnapshotComponent = {
export type Snapshot = {
chainId: string;
chainName: string;
network: string; // "mainnet" | "sepolia"
network: string; // "mainnet" | "sepolia" | "zeronet"
block: number;
timestamp: string;
date: string;
Expand Down Expand Up @@ -74,6 +74,7 @@ export const PRESETS: Preset[] = [
export const CHAIN_NAME_BY_NETWORK: Record<string, string> = {
mainnet: 'base',
sepolia: 'base-sepolia',
zeronet: 'base-zeronet',
};

export function formatBytes(bytes: number): string {
Expand Down Expand Up @@ -175,4 +176,16 @@ export const SAMPLE_SNAPSHOTS: Snapshot[] = [
storage_changesets: 90,
rocksdb_indices: 35,
}),
// Hidden from the page (see isNetworkVisibleInUi) but still served by the API,
// so the dev fallback mirrors what /api/snapshots returns.
sampleSnapshot('zeronet', 'Base Zeronet', '84530', 512000, {
state: 12,
headers: 1,
transactions: 3,
transaction_senders: 1,
receipts: 4,
account_changesets: 2,
storage_changesets: 5,
rocksdb_indices: 2,
}),
];
82 changes: 82 additions & 0 deletions app/snapshots/networks.contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import fs from 'node:fs';
import path from 'node:path';

import { isNetworkVisibleInUi, NETWORK_CONFIGS, NETWORK_IDS } from './r2';

/**
* Contract for the snapshots API surface.
*
* The regression this exists to prevent: #14 "Cobalt and fixes" wanted zeronet
* off the snapshots *page* and deleted its chain descriptor to get there. That
* descriptor is also what /api/snapshots enumerates, so ?network=zeronet started
* answering 400 and zeronet nodes could no longer sync from a snapshot. The page
* looked right, nothing failed, and it went unnoticed.
*
* Every network listed here must keep being served. Hiding one from the UI is a
* `hiddenFromUi` flag, never a deletion.
*
* Adding a network? Add it to EXPECTED_NETWORKS in the same change. Removing one
* is intentional and rare: delete it here too, and make sure no node still syncs
* from that bucket first.
*/
const EXPECTED_NETWORKS = ['mainnet', 'sepolia', 'zeronet'] as const;

/** Networks deliberately absent from the snapshots page but still served. */
const EXPECTED_HIDDEN_FROM_UI = ['zeronet'] as const;

describe('snapshots API network contract', () => {
it.each(EXPECTED_NETWORKS)('serves %s from the API', (id) => {
expect(NETWORK_IDS).toContain(id);
});

it('serves exactly the expected networks — no silent additions or removals', () => {
expect([...NETWORK_IDS].sort()).toEqual([...EXPECTED_NETWORKS].sort());
});

it.each(EXPECTED_HIDDEN_FROM_UI)('hides %s from the page but keeps serving it', (id) => {
expect(NETWORK_IDS).toContain(id);
expect(isNetworkVisibleInUi(id)).toBe(false);
});

it('leaves every other network visible', () => {
const hidden = new Set<string>(EXPECTED_HIDDEN_FROM_UI);
for (const id of NETWORK_IDS) {
if (!hidden.has(id)) expect(isNetworkVisibleInUi(id)).toBe(true);
}
});

it('gives every network the config the loader needs', () => {
for (const network of NETWORK_CONFIGS) {
expect(network.chainName, `${network.id} chainName`).toBeTruthy();
expect(network.bucket, `${network.id} bucket`).toBeTruthy();
expect(network.envPrefix, `${network.id} envPrefix`).toBeTruthy();
expect(network.publicBaseUrl, `${network.id} publicBaseUrl`).toMatch(/^https:\/\//);
}
});

it('keeps ids, buckets, and env prefixes unique', () => {
for (const key of ['id', 'bucket', 'envPrefix'] as const) {
const values = NETWORK_CONFIGS.map((n) => n[key]);
expect(new Set(values).size, `duplicate ${key}`).toBe(values.length);
}
});

// loadSnapshots throws if ANY network fails, so a network whose R2 credentials
// were never provisioned 502s the whole endpoint — every network with it. This
// catches the config half of that at PR time; the credentials themselves live
// in Vercel and cannot be checked from here.
it('documents every network\'s R2 credentials in .env.example', () => {
const envExample = fs.readFileSync(
path.join(process.cwd(), '.env.example'),
'utf8',
);
for (const network of NETWORK_CONFIGS) {
expect(
envExample.includes(`${network.envPrefix}_`),
`${network.id}: .env.example never mentions ${network.envPrefix}_*, so whoever ` +
`deploys this has no signal that its R2 credentials must be set. Without them ` +
`/api/snapshots returns 502 for every network, not just this one.`,
).toBe(true);
}
});
});
8 changes: 6 additions & 2 deletions app/snapshots/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EmptyState } from '../components/ui/EmptyState';

import { SAMPLE_SNAPSHOTS, Snapshot } from './data';
import { getSnapshots } from './r2';
import { getSnapshots, isNetworkVisibleInUi } from './r2';
import { SnapshotsClient } from './SnapshotsClient';

// Statically rendered and revalidated, matching app/upgrades/page.tsx. This keeps the
Expand All @@ -13,7 +13,11 @@ import { SnapshotsClient } from './SnapshotsClient';
export const revalidate = 300;

export default async function SnapshotsPage() {
const snapshots = await loadSnapshots();
// Filtered at the render boundary, not in the data layer: /api/snapshots keeps
// serving every network so nodes can sync from buckets we don't advertise here.
const snapshots = (await loadSnapshots()).filter((snapshot) =>
isNetworkVisibleInUi(snapshot.network),
);

if (snapshots.length === 0) {
return (
Expand Down
24 changes: 23 additions & 1 deletion app/snapshots/r2.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
import { decodeXml } from './r2';
import { decodeXml, isNetworkVisibleInUi, NETWORK_IDS } from './r2';

describe('network visibility', () => {
// Zeronet was removed outright in "Cobalt and fixes" (#14) to hide it from the
// page, which also stopped the API serving it — so zeronet nodes could no
// longer sync from a snapshot. It must stay served and merely unlisted.
it('serves zeronet from the API', () => {
expect(NETWORK_IDS).toContain('zeronet');
});

it('hides zeronet from the snapshots page', () => {
expect(isNetworkVisibleInUi('zeronet')).toBe(false);
});

it('keeps the public networks visible', () => {
expect(isNetworkVisibleInUi('mainnet')).toBe(true);
expect(isNetworkVisibleInUi('sepolia')).toBe(true);
});

it('treats an unknown network as visible, so a new network is not hidden by accident', () => {
expect(isNetworkVisibleInUi('some-future-net')).toBe(true);
});
});

describe('decodeXml', () => {
it('decodes the XML entities R2 listings use', () => {
Expand Down
33 changes: 33 additions & 0 deletions app/snapshots/r2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ type NetworkConfig = {
bucket: string;
publicBaseUrl: string;
envPrefix: string;
/**
* Served by the API but omitted from the snapshots page. For a network that
* nodes still sync from while we don't want to advertise it publicly —
* visibility only, never a reason to stop serving the data.
*/
hiddenFromUi?: boolean;
};

type R2Config = {
Expand Down Expand Up @@ -69,6 +75,14 @@ const NETWORKS: NetworkConfig[] = [
publicBaseUrl: 'https://sepolia-v2-snapshots.base.org',
envPrefix: 'BASE_SEPOLIA',
},
{
id: 'zeronet',
chainName: 'Base Zeronet',
bucket: 'base-zeronet-reth-v2-snapshots',
publicBaseUrl: 'https://zeronet-v2-snapshots.base.org',
envPrefix: 'BASE_ZERONET',
hiddenFromUi: true,
},
];

export type SnapshotLoadFailure = { network: string; error: string };
Expand All @@ -78,6 +92,25 @@ export const SNAPSHOT_CACHE_SECONDS = 300;

export const NETWORK_IDS = NETWORKS.map((network) => network.id);

/**
* Read-only view of the configured networks. Exported so the API contract test
* can assert every network stays served — see networks.contract.test.ts.
*/
export const NETWORK_CONFIGS: readonly Readonly<NetworkConfig>[] = NETWORKS;

const UI_HIDDEN_NETWORK_IDS = new Set(
NETWORKS.filter((network) => network.hiddenFromUi).map((network) => network.id),
);

/**
* Whether a network should be listed on the snapshots page. The API serves every
* network in NETWORKS regardless — nodes sync from buckets we don't advertise —
* so filter with this at the render boundary, never in the data layer.
*/
export function isNetworkVisibleInUi(networkId: string): boolean {
return !UI_HIDDEN_NETWORK_IDS.has(networkId);
}

/** Thrown when any configured network failed, carrying the per-network detail. */
export class SnapshotLoadError extends Error {
constructor(readonly failures: SnapshotLoadFailure[]) {
Expand Down
Loading