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
27 changes: 27 additions & 0 deletions dataService.js
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,33 @@ export function getAllChains() {
return cachedData.indexed.all.map(transformChain);
}

/**
* Count chains grouped by tag category
* @param {Array} chains - Array of chain objects
* @returns {Object} Counts for each category
*/
export function countChainsByTag(chains) {
const totalChains = chains.length;
let totalTestnets = 0;
let totalL2s = 0;
let totalBeacons = 0;
let totalMainnets = 0;

for (const chain of chains) {
const tags = chain.tags || [];
const isTestnet = tags.includes('Testnet');
const isL2 = tags.includes('L2');
const isBeacon = tags.includes('Beacon');

if (isTestnet) totalTestnets += 1;
if (isL2) totalL2s += 1;
if (isBeacon) totalBeacons += 1;
if (!isTestnet && !isL2 && !isBeacon) totalMainnets += 1;
}

return { totalChains, totalMainnets, totalTestnets, totalL2s, totalBeacons };
}
Comment on lines +1040 to +1060
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

countChainsByTag() is new logic used by /stats, but there’s no targeted unit test coverage for it. Adding a small unit test suite (e.g., mixed tags, missing tags, and the “mainnet excludes Testnet/L2/Beacon” case) would help prevent regressions in stats reporting.

Copilot uses AI. Check for mistakes.

/**
* Add value to a keyword set if it is a non-empty string
*/
Expand Down
48 changes: 27 additions & 21 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import Fastify from 'fastify';
import cors from '@fastify/cors';
import rateLimit from '@fastify/rate-limit';
import helmet from '@fastify/helmet';
import fastifyStatic from '@fastify/static';
import { readFile } from 'node:fs/promises';
import { basename, resolve } from 'node:path';
import { loadData, initializeDataOnStartup, getCachedData, searchChains, getChainById, getAllChains, getAllRelations, getRelationsById, getEndpointsById, getAllEndpoints, getAllKeywords, validateChainData, traverseRelations } from './dataService.js';
import { basename, resolve, dirname, join } from 'node:path';
import { fileURLToPath as toFilePath } from 'node:url';
import { loadData, initializeDataOnStartup, getCachedData, searchChains, getChainById, getAllChains, getAllRelations, getRelationsById, getEndpointsById, getAllEndpoints, getAllKeywords, validateChainData, traverseRelations, countChainsByTag } from './dataService.js';
import { getMonitoringResults, getMonitoringStatus, startRpcHealthCheck } from './rpcMonitor.js';
import {
PORT, HOST, BODY_LIMIT, MAX_PARAM_LENGTH,
Expand Down Expand Up @@ -51,11 +53,22 @@ export async function buildApp(options = {}) {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"]
styleSrc: ["'self'"],
fontSrc: ["'self'"],
connectSrc: ["'self'"],
imgSrc: ["'self'", "data:"]
}
}
});

// Serve public/ directory for the 3D visualization UI
const __dir = dirname(toFilePath(import.meta.url));
await fastify.register(fastifyStatic, {
root: join(__dir, 'public'),
prefix: '/ui/',
decorateReply: false
});

// Security: Rate limiting
await fastify.register(rateLimit, {
max: RATE_LIMIT_MAX,
Expand All @@ -75,7 +88,7 @@ export async function buildApp(options = {}) {
/**
* Health check endpoint
*/
fastify.get('/health', async (request, reply) => {
fastify.get('/health', async () => {
const cachedData = getCachedData();
return {
status: 'ok',
Expand Down Expand Up @@ -157,7 +170,7 @@ export async function buildApp(options = {}) {
/**
* Get all chain relations
*/
fastify.get('/relations', async (request, reply) => {
fastify.get('/relations', async () => {
const relations = getAllRelations();

return relations;
Expand Down Expand Up @@ -205,7 +218,7 @@ export async function buildApp(options = {}) {
/**
* Get all endpoints
*/
fastify.get('/endpoints', async (request, reply) => {
fastify.get('/endpoints', async () => {
const endpoints = getAllEndpoints();

return {
Expand Down Expand Up @@ -234,7 +247,7 @@ export async function buildApp(options = {}) {
/**
* Get raw data sources
*/
fastify.get('/sources', async (request, reply) => {
fastify.get('/sources', async () => {
const cachedData = getCachedData();
return {
lastUpdated: cachedData.lastUpdated,
Expand Down Expand Up @@ -281,7 +294,7 @@ export async function buildApp(options = {}) {
/**
* Get SLIP-0044 coin types as JSON
*/
fastify.get('/slip44', async (request, reply) => {
fastify.get('/slip44', async (_request, reply) => {
const cachedData = getCachedData();

if (!cachedData.slip44) {
Expand Down Expand Up @@ -366,7 +379,7 @@ export async function buildApp(options = {}) {
/**
* Get RPC monitoring results
*/
fastify.get('/rpc-monitor', async (request, reply) => {
fastify.get('/rpc-monitor', async () => {
const results = getMonitoringResults();
const status = getMonitoringStatus();

Expand Down Expand Up @@ -409,15 +422,11 @@ export async function buildApp(options = {}) {
/**
* Get aggregate stats
*/
fastify.get('/stats', async (request, reply) => {
fastify.get('/stats', async () => {
const chains = getAllChains();
const monitorResults = getMonitoringResults();

const totalChains = chains.length;
const totalMainnets = chains.filter(c => !c.tags?.includes('Testnet') && !c.tags?.includes('L2') && !c.tags?.includes('Beacon')).length;
const totalTestnets = chains.filter(c => c.tags?.includes('Testnet')).length;
const totalL2s = chains.filter(c => c.tags?.includes('L2')).length;
const totalBeacons = chains.filter(c => c.tags?.includes('Beacon')).length;
const { totalChains, totalMainnets, totalTestnets, totalL2s, totalBeacons } = countChainsByTag(chains);

const rpcWorking = monitorResults.workingEndpoints;
const rpcFailed = monitorResults.failedEndpoints || 0;
Expand Down Expand Up @@ -487,10 +496,9 @@ export async function buildApp(options = {}) {
/**
* Parse and validate an integer parameter
* @param {string} param - Parameter value to parse
* @param {string} paramName - Name of the parameter for error message
* @returns {number|null} Parsed integer or null if invalid
*/
function parseIntParam(param, paramName = 'ID') {
function parseIntParam(param) {
const parsed = Number.parseInt(param, 10);
return Number.isNaN(parsed) ? null : parsed;
}
Expand All @@ -507,12 +515,10 @@ function sendError(reply, code, message) {

// Only run the server if this file is executed directly (CLI mode)
// This allows the file to be imported for testing without starting the server
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __filename = toFilePath(import.meta.url);

// Check if this file is being run directly
const isMainModule = process.argv[1] === __filename || process.argv[1] === fileURLToPath(import.meta.url);
const isMainModule = process.argv[1] === __filename;

if (isMainModule) {
const start = async () => {
Expand Down
Loading