Add native-token USD prices and execution-client fingerprinting#40
Merged
Conversation
Two new product features sharing the existing RPC monitor pipeline:
- Native-token prices: `/chains` and `/chains/:id` now include a `price: { usd, updatedAt } | null` field sourced from CoinGecko. New `priceService.js` keys its cache by CoinGecko coinId (so sibling chains like all ETH-L2s share a single entry), coalesces concurrent fetches, applies a 3s timeout per request, negatively caches missing IDs with a short TTL, and is prefetched on startup so the first `/chains` call doesn't pay a round-trip on the hot path.
- Client diversity: each working RPC endpoint's `web3_clientVersion` is parsed into structured metadata (name, version, repo, language, layer). New `/clients` and `/clients/:id` endpoints expose per-chain client summaries with node counts and version breakdowns. `/rpc-monitor/:id` also gains a `clients` array.
Both surfaces are exposed as MCP tools (`get_clients`) and the get_chains/get_chain_by_id MCP responses are price-enriched. Adds env vars PRICE_CACHE_TTL_MS, PRICE_NEGATIVE_CACHE_TTL_MS, PRICE_FETCH_TIMEOUT_MS. Includes unit + integration tests covering parser edge cases, cache/coalescing behavior, and the new routes.
…ring sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds native-token USD pricing (via CoinGecko + caching) to chain responses and introduces execution-client fingerprinting based on web3_clientVersion, exposing aggregated client summaries via new REST + MCP endpoints.
Changes:
- Add
priceto/chains,/chains/:id, and MCP chain tools via newpriceService(TTL cache, negative cache, inflight coalescing, timeout, startup prefetch). - Parse
web3_clientVersioninto structured client metadata and aggregate per-chain client/version counts; expose via/clients,/clients/:id,get_clients, and include in/rpc-monitor/:id. - Small UI refactor in
public/app.js(extract helper methods fromshowNodeDetails).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/rpcMonitor.test.js | Adds unit coverage for parsed client field and per-chain client aggregation helpers. |
| tests/unit/priceService.test.js | New unit tests for CoinGecko ID mapping, caching behavior, and batch fetch logic. |
| tests/unit/mcp-tools.test.js | Updates MCP tool mocks and assertions for new price enrichment and get_clients. |
| tests/unit/clientParser.test.js | New unit tests for web3_clientVersion parsing across known/unknown clients and edge cases. |
| tests/integration/api.test.js | Extends API integration tests to assert price fields and new /clients endpoints. |
| rpcMonitor.js | Adds client parsing to endpoint results plus aggregation helpers (getClientsByChain, summarizeChainClients). |
| public/app.js | Extracts badge/website rendering helpers from showNodeDetails. |
| priceService.js | Implements CoinGecko pricing fetch + caching + prefetch. |
| mcp-tools.js | Enriches chain tools with price and adds a new get_clients MCP tool. |
| index.js | Adds price enrichment to chain endpoints, adds /clients REST endpoints, and includes client summaries in /rpc-monitor/:id. |
| config.js | Adds env-configured TTLs and timeout for price caching/fetching. |
| clientRegistry.js | Introduces a registry mapping client names to metadata (repo/language/website/layer). |
| clientParser.js | Adds web3_clientVersion parser that normalizes and enriches client metadata via registry lookups. |
Comment on lines
+143
to
+147
| } else { | ||
| // Negative cache: short TTL so we don't hammer CoinGecko on every request | ||
| // when an ID is missing or temporarily unavailable. | ||
| priceCache.set(id, { usd: null, updatedAt }); | ||
| } |
Comment on lines
+1
to
+4
| // Curated registry of known blockchain client software. | ||
| // Key is the lowercased, hyphen-free client name as it appears in | ||
| // `web3_clientVersion` responses. Add new entries as they're observed in | ||
| // production — unknown names still round-trip through the parser with `repo: null`. |
Comment on lines
+73
to
+75
| * Extract the semver portion of a version segment, preserving an optional `v` | ||
| * prefix and pre-release/build metadata. Some clients emit extra descriptors | ||
| * (e.g. "v1.26.0+commit.abc"); we keep them intact — the raw string is still |
Comment on lines
+243
to
+244
| * array of summaries — one per chain with at least one working endpoint that | ||
| * reported a parseable `client`. |
Resolves conflicts by favoring main's RPC-monitoring structure (dataService.js owns monitoring; rpcMonitor.js stays as legacy). New client-aggregation functions moved out of rpcMonitor.js into a dedicated clientsView.js that consumes getRpcMonitoringResults from dataService.js and parses clientVersion via clientParser.js. - index.js, mcp-tools.js: import RPC monitor fns from dataService.js, import getClientsByChain/summarizeChainClients from clientsView.js, keep priceService imports - public/app.js: take main's showNodeHeader extraction; drop the PR's duplicate showStatusBadge / inlined helpers superseded by main's refactor; keep the CodeQL URL-protocol fix - rpcMonitor.js, tests/unit/rpcMonitor.test.js: restored to main's version (PR's additions superseded by clientsView.js) - clientsView.js, tests/unit/clientsView.test.js: new - tests/integration/api.test.js, tests/unit/mcp-tools.test.js: switch mocks from rpcMonitor.js to clientsView.js All 3 remaining test failures (dataService loadData "all sources failing", mcp-tools get_stats x2) are pre-existing on main.
priceService: distinguish "upstream failed" from "id missing". On HTTP / network error the cache is no longer poisoned with negative entries — retries can immediately re-fetch instead of waiting for PRICE_NEGATIVE_CACHE_TTL_MS. Two new tests cover the retry path after both network and HTTP failures. clientRegistry: drop misleading "hyphen-free" claim from the header comment — registry already contains op-geth and op-reth. clientParser: rewrite normalizeVersion docstring to describe what the function actually does (trim) rather than implying semver extraction. clientsView: getClientsByChain() (no-arg form) now filters out chains where every working endpoint had an unparseable client, so /clients stays a directory of known software. Single-chain form unchanged so callers can still see unknownNodes.
| // Negative-cache with the short TTL so we don't hammer CoinGecko. | ||
| priceCache.set(id, { usd: null, updatedAt }); | ||
| } | ||
| // else: upstream failed; leave any prior entry intact so retries can succeed. |
Comment on lines
+18
to
19
| import { getPricesForChains, getPriceForChain } from './priceService.js'; | ||
|
|
Comment on lines
+85
to
+87
| export const PRICE_CACHE_TTL_MS = parseIntEnv('PRICE_CACHE_TTL_MS', 3600000); | ||
| export const PRICE_NEGATIVE_CACHE_TTL_MS = parseIntEnv('PRICE_NEGATIVE_CACHE_TTL_MS', 300000); | ||
| export const PRICE_FETCH_TIMEOUT_MS = parseIntEnv('PRICE_FETCH_TIMEOUT_MS', 3000); |
Comment on lines
+90
to
+93
| for (const id of coinIds) { | ||
| const pending = inflight.get(id); | ||
| if (pending) { | ||
| waiters.push(pending.then(({ map, ok: pendingOk }) => ({ id, usd: map.get(id), ok: pendingOk }))); |
Comment on lines
+149
to
+152
| } else if (ok) { | ||
| // Upstream succeeded but didn't return this id — it's genuinely missing. | ||
| // Negative-cache with the short TTL so we don't hammer CoinGecko. | ||
| priceCache.set(id, { usd: null, updatedAt }); |
| } | ||
|
|
||
| const { chainId } = args; | ||
| if (!isValidChainId(chainId)) { |
| return { content: [{ type: 'text', text: lines.join('\n') }] }; | ||
| } | ||
|
|
||
| function handleGetClients(args) { |
Comment on lines
+105
to
+107
| const response = await fetchWithTimeout(url); | ||
| if (response.ok) { | ||
| const data = await response.json(); |
Comment on lines
+15
to
+16
| * endpoint failed to parse are excluded from the list so `/clients` | ||
| * stays a directory of *known* client software. |
Comment on lines
+222
to
+223
| const chainIds = chains.map((c) => c.chainId); | ||
| const priceMap = await getPricesForChains(chainIds); |
Johnaverse
pushed a commit
that referenced
this pull request
May 14, 2026
Brings in PR #40 (native-token USD prices + execution-client fingerprinting) and PR #41 (dependabot fastify 5.8.5 bump). Conflict resolution favored PR #39's architectural structure: the PR #40 features are ported into the new src/ layout instead of into the old monolithic index.js / mcp-tools.js. - src/http/routes/chains.js: /chains and /chains/:id now enrich responses with price via priceService. - src/http/routes/clients.js (new): /clients and /clients/:id expose getClientsByChain output. - src/http/routes/rpcMonitor.js: /rpc-monitor/:id gains a clients array via summarizeChainClients. - src/http/app.js: registers clientsRoutes, kicks off prefetchAllPrices on startup (with pino-logged failure). - index.js: kept the PR #39 shim form (imports buildApp from src/http/app.js). - mcp-tools.js: keeps PR #39's L2BEAT/refresher tools (get_scaling_chains, get_l2beat_by_id, get_refresher_status) AND adds PR #40's get_clients; get_chains / get_chain_by_id handlers enriched with price. - package.json: union of deps — pino ^10.3.0 (PR #39) + fastify ^5.8.5 (PR #41). - tests/integration/api.test.js: kept the PR #39 hoisted-mocks pattern, added priceService and clientsView mock factories so the new routes resolve under mocked seams. - tests/unit/mcp-tools.test.js: kept l2beatRefresher mock, added clientsView + priceService mocks; tool count is now 17 (13 base + 3 L2BEAT/refresher + 1 clients). - package-lock.json: regenerated by npm install. Test result: 670 passing / 0 failing / 4 skipped (PR #39 was 618/0/4 and PR #40 added priceService + clientsView + clientParser test modules; merged total adds 52 net new passing tests).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/chains,/chains/:id, and the equivalent MCP tools. Sourced from CoinGecko via a newpriceServicethat keys cache by coinId (sibling chains share entries), coalesces concurrent fetches, applies a 3s timeout, negatively caches missing IDs, and prefetches on startup so cold/chainsrequests don't block.web3_clientVersionis parsed into{ name, version, repo, language, layer }(seeclientParser.js+clientRegistry.js). New/clientsand/clients/:idREST endpoints and aget_clientsMCP tool expose per-chain summaries with node-count and version breakdowns./rpc-monitor/:idalso gains aclientsarray.PRICE_CACHE_TTL_MS,PRICE_NEGATIVE_CACHE_TTL_MS,PRICE_FETCH_TIMEOUT_MS.public/app.js(extract-method onshowNodeDetails).Test plan
npm test— 549 passed, 4 skipped/chainson a cold cache and confirm the response includespricefor known chains (Ethereum, Polygon, BSC, ...) andnullfor unknown ones/chains/:idfor chainId 1 and confirmprice.usdis a number/clientsafter the RPC monitor has run at least once; confirm chains with parsedweb3_clientVersionshow client summaries/clients/1and confirmclients[].versionsis sorted bynodeCountdescendingPRICE_FETCH_TIMEOUT_MS=1) degrades gracefully —price: nullrather than 5xxget_clientstool from a connected client