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
3 changes: 3 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Run tests with coverage
run: npm run test:coverage

Expand Down
78 changes: 78 additions & 0 deletions .github/workflows/refresh-l2beat-fallback.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Refresh L2BEAT fallback

# Runs the live L2BEAT scaling-summary endpoint weekly and opens a PR if the
# normalized output differs from the checked-in data/l2beat-fallback.json.
# Keeps the static safety net from drifting when the live API is unreachable.
on:
schedule:
# 06:00 UTC every Monday
- cron: '0 6 * * 1'
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
refresh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: npm ci

- name: Fetch and normalize live L2BEAT data
id: fetch
continue-on-error: true
run: |
node --input-type=module -e '
import { normalizeL2BeatResponse } from "./src/sources/l2beat.js";
const res = await fetch(process.env.L2BEAT_API_URL || "https://l2beat.com/api/scaling-summary");
if (!res.ok) { process.stderr.write(`HTTP ${res.status}\n`); process.exit(1); }
const json = await res.json();
const projects = normalizeL2BeatResponse(json)
.filter(p => Number.isSafeInteger(p.chainId))
.map(p => ({
slug: p.slug,
chainId: p.chainId,
displayName: p.displayName,
stage: p.stage,
category: p.category,
stack: p.stack,
daLayer: p.daLayer,
hostChainId: p.hostChainId
}));
const payload = {
schemaVersion: 1,
fetchedAt: new Date().toISOString(),
note: "Auto-refreshed weekly by .github/workflows/refresh-l2beat-fallback.yml. Excludes chains whose chainId exceeds Number.MAX_SAFE_INTEGER (e.g. Starknet).",
projects
};
await import("node:fs/promises").then(fs =>
fs.writeFile("data/l2beat-fallback.json", JSON.stringify(payload, null, 2) + "\n", "utf8")
);
'

- name: Skip when fetch failed
if: steps.fetch.outcome != 'success'
run: echo "L2BEAT live API unreachable; skipping refresh until next run."

- name: Create pull request if file changed
if: steps.fetch.outcome == 'success'
uses: peter-evans/create-pull-request@v6
with:
commit-message: 'chore: refresh L2BEAT fallback data'
title: 'chore: refresh L2BEAT fallback data'
body: |
Automated weekly refresh of `data/l2beat-fallback.json` from the
live L2BEAT scaling-summary endpoint. Review the diff for any
unexpected stage transitions or removed/added projects before
merging.
branch: chore/refresh-l2beat-fallback
delete-branch: true
add-paths: data/l2beat-fallback.json
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,7 @@ vite.config.js.timestamp-*
vite.config.ts.timestamp-*

# MCP configuration file
.mcp.json
.mcp.json

# Graphify generated knowledge graph
graphify-out/
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ RUN npm ci --only=production

# Copy application files
COPY *.js ./
COPY src/ ./src/
COPY data/ ./data/
COPY public/ ./public/

# Ensure app owns the working directory
RUN chown -R app:app /app
Expand Down
18 changes: 18 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export const SEARCH_RATE_LIMIT_MAX = parseIntEnv('SEARCH_RATE_LIMIT_MAX', 30);

// RPC health check
export const RPC_CHECK_TIMEOUT_MS = parseIntEnv('RPC_CHECK_TIMEOUT_MS', 8000);
/**
* @deprecated Unused since the unified rolling refresher (services/chainRefresher.js).
* The new loop processes one chain per tick; each chain's RPC endpoints are
* checked in parallel inside that chain's job. There is no global concurrency
* cap. Kept for backwards-compatible env parsing; safe to remove in v2.
*/
export const RPC_CHECK_CONCURRENCY = parseIntEnv('RPC_CHECK_CONCURRENCY', 8);
export const MAX_ENDPOINTS_PER_CHAIN = parseIntEnv('MAX_ENDPOINTS_PER_CHAIN', 5);

Expand All @@ -69,6 +75,18 @@ export const DATA_SOURCE_SLIP44 = parseStringEnv(
'DATA_SOURCE_SLIP44',
'https://raw.githubusercontent.com/satoshilabs/slips/master/slip-0044.md'
);
export const DATA_SOURCE_L2BEAT_API = parseStringEnv(
'DATA_SOURCE_L2BEAT_API',
'https://l2beat.com/api/scaling-summary'
);
export const L2BEAT_FETCH_TIMEOUT_MS = parseIntEnv('L2BEAT_FETCH_TIMEOUT_MS', 10000);
/**
* @deprecated Cadence is now driven by the unified rolling refresher
* (CHAIN_REFRESHER_TICK_MS × queue length). Kept so /scaling/status can keep
* exposing the value as a hint to consumers, but no longer used for
* scheduling. Safe to remove in v2 once consumers migrate to /refresher.
*/
export const L2BEAT_REFRESH_INTERVAL_MS = parseIntEnv('L2BEAT_REFRESH_INTERVAL_MS', 300000);
Comment on lines +78 to +89

// Disk cache
export const DATA_CACHE_ENABLED = parseBooleanEnv('DATA_CACHE_ENABLED', true);
Expand Down
34 changes: 34 additions & 0 deletions data/l2beat-fallback.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"schemaVersion": 1,
"fetchedAt": "2026-05-05T00:00:00.000Z",
"note": "Hand-curated last-known-good fallback for src/sources/l2beat.js. Used only when the live l2beat.com API is unreachable. Refresh manually when stage classifications change. Source of truth: https://l2beat.com. Excludes chains whose chainId exceeds Number.MAX_SAFE_INTEGER (e.g. Starknet's CAIP-2 numeric ID 0x534e5f4d41494e); the live API can still surface them once the indexer learns to handle BigInt chain IDs.",
"projects": [
{ "slug": "arbitrum", "chainId": 42161, "displayName": "Arbitrum One", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "Arbitrum Orbit", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "optimism", "chainId": 10, "displayName": "OP Mainnet", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "base", "chainId": 8453, "displayName": "Base", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "zksync-era", "chainId": 324, "displayName": "ZKsync Era", "stage": "Stage 0", "category": "ZK Rollup", "stack": "ZK Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "linea", "chainId": 59144, "displayName": "Linea", "stage": "Stage 0", "category": "ZK Rollup", "stack": "Linea zkEVM", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "polygonzkevm", "chainId": 1101, "displayName": "Polygon zkEVM", "stage": "Stage 1", "category": "ZK Rollup", "stack": "Polygon CDK", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "scroll", "chainId": 534352, "displayName": "Scroll", "stage": "Stage 1", "category": "ZK Rollup", "stack": "Scroll", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "blast", "chainId": 81457, "displayName": "Blast", "stage": "Stage 0", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "mantle", "chainId": 5000, "displayName": "Mantle", "stage": "Stage 0", "category": "Optimium", "stack": "OP Stack", "daLayer": "Mantle DA", "hostChainId": 1 },
{ "slug": "zora", "chainId": 7777777, "displayName": "Zora", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "mode", "chainId": 34443, "displayName": "Mode", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "manta-pacific", "chainId": 169, "displayName": "Manta Pacific", "stage": "Stage 0", "category": "Optimium", "stack": "OP Stack", "daLayer": "Celestia", "hostChainId": 1 },
{ "slug": "lisk", "chainId": 1135, "displayName": "Lisk", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "fraxtal", "chainId": 252, "displayName": "Fraxtal", "stage": "Stage 0", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "bob", "chainId": 60808, "displayName": "BOB", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "world-chain", "chainId": 480, "displayName": "World Chain", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "xlayer", "chainId": 196, "displayName": "X Layer", "stage": "Stage 0", "category": "Validium", "stack": "Polygon CDK", "daLayer": "DAC", "hostChainId": 1 },
{ "slug": "taiko", "chainId": 167000, "displayName": "Taiko Alethia", "stage": "Stage 1", "category": "ZK Rollup", "stack": "Taiko", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "redstone", "chainId": 690, "displayName": "Redstone", "stage": "Stage 0", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "ink", "chainId": 57073, "displayName": "Ink", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "soneium", "chainId": 1868, "displayName": "Soneium", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "unichain", "chainId": 130, "displayName": "Unichain", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "zircuit", "chainId": 48900, "displayName": "Zircuit", "stage": "Stage 0", "category": "ZK Rollup", "stack": "ZK Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "metis", "chainId": 1088, "displayName": "Metis Andromeda", "stage": "Stage 0", "category": "Optimium", "stack": "OP Stack", "daLayer": "Metis DA", "hostChainId": 1 },
{ "slug": "boba", "chainId": 288, "displayName": "Boba Network", "stage": "Stage 0", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "kroma", "chainId": 255, "displayName": "Kroma", "stage": "Stage 1", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 },
{ "slug": "morph", "chainId": 2818, "displayName": "Morph", "stage": "Stage 0", "category": "Optimistic Rollup", "stack": "OP Stack", "daLayer": "Ethereum", "hostChainId": 1 }
]
}
Loading
Loading