From 6e8d20a0e9495f7eb6d835aabd65d83506fe1c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaan=20Uzdo=C4=9Fan?= Date: Thu, 6 Aug 2026 11:44:56 +0300 Subject: [PATCH 1/2] feat: use dRPC's chain label as a last-resort name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chains that only dRPC knows about ended up named "Chain " (e.g. chain 61900) because the name lookup only consulted chain-overrides.json, Etherscan, Blockscout and chainid.network. dRPC's chains.yaml does carry a name — a protocol-level `label` plus a per-network `id` — we just weren't reading it. Extract it into DrpcChainData.name and use it in generate.ts right before the "Chain " placeholder, so better-curated sources still win. Joining label + network id needs two touch-ups: drop the label when the network id already repeats it ("megaETH" + "MegaETH Testnet"), and capitalise an all-lowercase network id ("mainnet"). Covered by a new scripts/providers/drpc.test.ts, wired into `npm test`. Against today's chains.yaml all 262 dRPC chains resolve to a name: 61900 -> Mova Mainnet, 1672 -> Pharos Mainnet, 2910 -> Morph Hoodi, 6281971 -> DogeOS Testnet. Co-Authored-By: Claude Opus 4.8 --- package.json | 2 +- scripts/generate.ts | 4 ++++ scripts/providers/drpc.test.ts | 30 ++++++++++++++++++++++++++++++ scripts/providers/drpc.ts | 25 ++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 scripts/providers/drpc.test.ts diff --git a/package.json b/package.json index 7aecfe3..f46c14d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "generate": "tsx scripts/generate.ts", "sync": "tsx scripts/sync.ts", "typecheck": "tsc --noEmit", - "test": "tsx --test scripts/sync.test.ts", + "test": "tsx --test scripts/sync.test.ts scripts/providers/drpc.test.ts", "test:integration": "tsx --test --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=junit.xml tests/chain-tests.test.ts tests/etherscan-instances.test.ts", "test:integration:chain": "tsx --test --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=junit-chain.xml tests/chain-tests.test.ts", "test:integration:etherscan": "tsx --test --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=junit-etherscan.xml tests/etherscan-instances.test.ts" diff --git a/scripts/generate.ts b/scripts/generate.ts index 0cb6926..d6749ad 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -530,11 +530,15 @@ async function main() { if (!hasActiveSource) continue; const blockscoutName = blockscout?.hostedBy === "blockscout" ? blockscout.name : undefined; + // dRPC's name comes last (before the placeholder): its labels are less + // curated than the explorers' or chainid.network's, but for chains only + // dRPC knows about it beats "Chain 61900". const sourcifyName = override?.sourcifyName ?? etherscan?.chainName ?? blockscoutName ?? meta?.name ?? + drpc?.name ?? `Chain ${chainId}`; const rpcSlots: RpcSlot[] = []; diff --git a/scripts/providers/drpc.test.ts b/scripts/providers/drpc.test.ts new file mode 100644 index 0000000..3ca14a4 --- /dev/null +++ b/scripts/providers/drpc.test.ts @@ -0,0 +1,30 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { buildDrpcChainName } from "./drpc.js"; + +describe("buildDrpcChainName", () => { + it("joins the protocol label and the network id", () => { + // The case that motivated this: chain 61900 is known only to dRPC. + assert.equal(buildDrpcChainName("Mova", "Mainnet"), "Mova Mainnet"); + assert.equal(buildDrpcChainName("Polygon", "Amoy"), "Polygon Amoy"); + }); + + it("does not repeat the label when the network id already starts with it", () => { + assert.equal(buildDrpcChainName("megaETH", "MegaETH Testnet"), "MegaETH Testnet"); + assert.equal(buildDrpcChainName("Moonriver", "Moonriver"), "Moonriver"); + assert.equal(buildDrpcChainName("Ozean", "Ozean Poseidon Testnet"), "Ozean Poseidon Testnet"); + }); + + it("capitalises an all-lowercase network id", () => { + assert.equal(buildDrpcChainName("Ethereum Classic", "mainnet"), "Ethereum Classic Mainnet"); + }); + + it("leaves mixed-case network ids alone", () => { + assert.equal(buildDrpcChainName("Berachain", "bArtio"), "Berachain bArtio"); + }); + + it("returns undefined without a label, and the label alone without a network id", () => { + assert.equal(buildDrpcChainName(undefined, "Mainnet"), undefined); + assert.equal(buildDrpcChainName("Mova", undefined), "Mova"); + }); +}); diff --git a/scripts/providers/drpc.ts b/scripts/providers/drpc.ts index 448d6a6..3da6158 100644 --- a/scripts/providers/drpc.ts +++ b/scripts/providers/drpc.ts @@ -2,16 +2,20 @@ import YAML from "yaml"; export interface DrpcChainData { shortName: string; + /** Human-readable name, e.g. "Mova Mainnet". Absent when the YAML has no label. */ + name?: string; } // chains.yaml shape (only the fields we consume). interface DrpcYamlChain { + id?: string; "chain-id"?: string | number; "short-names"?: string[]; } interface DrpcYamlProtocol { type?: string; + label?: string; chains?: DrpcYamlChain[]; } @@ -40,6 +44,25 @@ function parseChainId(raw: string | number | undefined): number | null { return Number.isNaN(id) || id <= 0 ? null : id; } +/** + * Build a display name from a protocol label and a network id, e.g. + * `label: Mova` + `id: Mainnet` -> "Mova Mainnet". + * + * Two touch-ups keep the result readable: + * - When the network id already starts with the label (`megaETH` + + * `MegaETH Testnet`, `Moonriver` + `Moonriver`), the label is dropped + * instead of repeated. + * - An all-lowercase network id (only `mainnet` today) is capitalised. + */ +export function buildDrpcChainName(label: string | undefined, networkId: string | undefined): string | undefined { + if (!label) return undefined; + if (!networkId) return label; + + const network = /^[a-z]+$/.test(networkId) ? networkId[0].toUpperCase() + networkId.slice(1) : networkId; + if (network.toLowerCase().startsWith(label.toLowerCase())) return network; + return `${label} ${network}`; +} + /** * Fetches the list of EVM chains supported by dRPC from their public * chains.yaml config. @@ -78,7 +101,7 @@ export async function fetchDrpcChains(): Promise> { // First occurrence wins — don't let a later mislabelled entry overwrite. if (!result.has(chainId)) { - result.set(chainId, { shortName }); + result.set(chainId, { shortName, name: buildDrpcChainName(protocol.label, chain.id) }); } } } From 12e0550aa023bf71293dfec2a7180076bf76078f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaan=20Uzdo=C4=9Fan?= Date: Thu, 6 Aug 2026 11:48:06 +0300 Subject: [PATCH 2/2] chore: drop comment in generate.ts name fallback Co-Authored-By: Claude Opus 4.8 --- scripts/generate.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/generate.ts b/scripts/generate.ts index d6749ad..ea5994d 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -530,9 +530,6 @@ async function main() { if (!hasActiveSource) continue; const blockscoutName = blockscout?.hostedBy === "blockscout" ? blockscout.name : undefined; - // dRPC's name comes last (before the placeholder): its labels are less - // curated than the explorers' or chainid.network's, but for chains only - // dRPC knows about it beats "Chain 61900". const sourcifyName = override?.sourcifyName ?? etherscan?.chainName ??