diff --git a/benchmarks/wallet-labels-coverage.yml b/benchmarks/wallet-labels-coverage.yml index d6d95d3d..58d0a48f 100644 --- a/benchmarks/wallet-labels-coverage.yml +++ b/benchmarks/wallet-labels-coverage.yml @@ -81,10 +81,21 @@ prometheus: window: 24h expected_freshness_seconds: 5400 -# Chain selector. tabs at the top of the page. Server injects -# `chain="X"` into every PromQL query for the active tab. The special -# value `all` skips the filter, aggregate over every chain at once. +# Chain + Kind selectors. tabs at the top of the page. Server injects +# `chain="X"` and `kind="X"` into every PromQL query for the active tab. +# `kind` separates the two distinct signals this bench actually measures: +# - contract: does the provider resolve a verified smart contract's name +# (Blockscout gets this for free on any verified contract) +# - eoa: does the provider resolve an externally-owned account to a +# curated entity (CEX hot wallet, foundation, public figure) - the +# real labeling-graph signal +# Default is `eoa` because the contract tab is trivially easy for explorers +# and saturates near 100%; the EOA tab is where curated entity coverage +# actually differentiates providers. dimensions: + kind: + - { value: eoa, label: EOA } + - { value: contract, label: Contract } chain: - { value: ethereum, label: Ethereum } - { value: solana, label: Solana } diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx index 22553f10..ceec4e6e 100644 --- a/src/app/benchmarks/[slug]/page.tsx +++ b/src/app/benchmarks/[slug]/page.tsx @@ -154,27 +154,33 @@ export default async function BenchmarkPage({ if (!aggregate) notFound(); const chainOptions = aggregate.dimensions?.chain ?? []; const regionOptions = aggregate.dimensions?.region ?? []; + const kindOptions = aggregate.dimensions?.kind ?? []; const chain = chainOptions[0]?.value ?? null; const region = regionOptions[0]?.value ?? null; + const kind = kindOptions[0]?.value ?? null; - // Pre-fetch every (chain × region) variant in parallel so client flips + // Pre-fetch every (chain × region × kind) variant in parallel so client flips // are zero round-trip. unstable_cache dedupes each (slug, filters) combo // across users - first miss warms it, every later viewer gets it instant. // `all` is the "no filter" sentinel - same as the unscoped fetch. const chainsForFetch = chainOptions.length > 0 ? chainOptions.map((c) => c.value) : [null]; const regionsForFetch = regionOptions.length > 0 ? regionOptions.map((r) => r.value) : [null]; + const kindsForFetch = kindOptions.length > 0 ? kindOptions.map((k) => k.value) : [null]; const variantPairs = chainsForFetch.flatMap((c) => - regionsForFetch.map((r) => [c, r] as const) + regionsForFetch.flatMap((r) => + kindsForFetch.map((k) => [c, r, k] as const) + ) ); const [variantList, all] = await Promise.all([ Promise.all( - variantPairs.map(async ([c, r]) => { - const filters: { chain?: string; region?: string } = {}; + variantPairs.map(async ([c, r, k]) => { + const filters: { chain?: string; region?: string; kind?: string } = {}; if (c && c !== "all") filters.chain = c; if (r && r !== "all") filters.region = r; + if (k && k !== "all") filters.kind = k; const b = await getBenchmark(slug, filters); - return [variantKey(c, r), b ?? aggregate] as const; + return [variantKey(c, r, k), b ?? aggregate] as const; }) ), getBenchmarks(), @@ -204,7 +210,7 @@ export default async function BenchmarkPage({ }, ]), ); - const benchmark = variants[variantKey(chain, region)] ?? aggregate; + const benchmark = variants[variantKey(chain, region, kind)] ?? aggregate; const isDraft = benchmark.status === "draft"; const isAwaiting = isDraft && benchmark.editorialStatus === "live"; @@ -457,8 +463,10 @@ export default async function BenchmarkPage({ variants={variants} chainOptions={chainOptions} regionOptions={regionOptions} + kindOptions={kindOptions} initialChain={chain ?? null} initialRegion={region ?? null} + initialKind={kind ?? null} /> )} @@ -563,8 +571,12 @@ export default async function BenchmarkPage({ /** Stable variant-map key. Mirrors what BenchmarkBody computes on every * filter change. Use `null` for "no dimension" and "all" / undefined as * the unscoped sentinel. */ -function variantKey(chain: string | null, region: string | null): string { - return `${chain ?? "__none"}|${region ?? "__none"}`; +function variantKey( + chain: string | null, + region: string | null, + kind: string | null, +): string { + return `${chain ?? "__none"}|${region ?? "__none"}|${kind ?? "__none"}`; } function DraftNotice({ source }: { source: string }) { diff --git a/src/components/benchmark-body.tsx b/src/components/benchmark-body.tsx index b04a6d2c..40c7874a 100644 --- a/src/components/benchmark-body.tsx +++ b/src/components/benchmark-body.tsx @@ -99,8 +99,12 @@ function summarize(b: Benchmark | undefined): ChainMeta | null { * the whole point). */ /** Stable variant-map key mirroring `page.tsx:variantKey`. */ -function variantKey(chain: string | null, region: string | null): string { - return `${chain ?? "__none"}|${region ?? "__none"}`; +function variantKey( + chain: string | null, + region: string | null, + kind: string | null, +): string { + return `${chain ?? "__none"}|${region ?? "__none"}|${kind ?? "__none"}`; } /** Region values that appear in extras.seriesByRegion24h. Used when the @@ -127,16 +131,20 @@ export function BenchmarkBody({ variants, chainOptions, regionOptions, + kindOptions = [], initialChain, initialRegion, + initialKind = null, }: { variants: Record; chainOptions: ChainOption[]; regionOptions: ChainOption[]; + kindOptions?: ChainOption[]; initialChain: string | null; initialRegion: string | null; + initialKind?: string | null; }) { - // Read ?chain= / ?region= client-side. The server can't read these any + // Read ?chain= / ?region= / ?kind= client-side. The server can't read these any // more (doing so would force /benchmarks/ to render dynamic on // every visit) so URL-driven filter state is hydrated here. Falls back // to the server-rendered initial when the URL has no filter or a @@ -144,22 +152,27 @@ export function BenchmarkBody({ const searchParams = useSearchParams(); const urlChain = searchParams.get("chain"); const urlRegion = searchParams.get("region"); + const urlKind = searchParams.get("kind"); const urlLayer = searchParams.get("layer"); const resolvedInitialChain = (urlChain && chainOptions.find((c) => c.value === urlChain)?.value) ?? initialChain; const resolvedInitialRegion = (urlRegion && regionOptions.find((r) => r.value === urlRegion)?.value) ?? initialRegion; + const resolvedInitialKind = + (urlKind && kindOptions.find((k) => k.value === urlKind)?.value) ?? initialKind; const resolvedInitialLayer: ProviderLayer = urlLayer === "l2" ? "l2" : "l1"; const [chain, setChain] = useState(resolvedInitialChain); const [region, setRegion] = useState(resolvedInitialRegion); + const [kind, setKind] = useState(resolvedInitialKind); const [layer, setLayer] = useState(resolvedInitialLayer); useEffect(() => { const url = new URL(window.location.href); syncParam(url, "chain", chain, chainOptions); syncParam(url, "region", region, regionOptions); + syncParam(url, "kind", kind, kindOptions); // Layer param: drop when default ("l1"), keep when user picked l2. if (layer === "l1") url.searchParams.delete("layer"); else url.searchParams.set("layer", layer); @@ -167,15 +180,17 @@ export function BenchmarkBody({ if (next !== window.location.pathname + window.location.search) { window.history.replaceState(null, "", next); } - }, [chain, region, layer, chainOptions, regionOptions]); + }, [chain, region, kind, layer, chainOptions, regionOptions, kindOptions]); const fallbackChain = chainOptions[0]?.value ?? null; const fallbackRegion = regionOptions[0]?.value ?? null; + const fallbackKind = kindOptions[0]?.value ?? null; const effectiveChain = chainOptions.length > 0 ? (chain ?? fallbackChain) : null; const effectiveRegion = regionOptions.length > 0 ? (region ?? fallbackRegion) : null; + const effectiveKind = kindOptions.length > 0 ? (kind ?? fallbackKind) : null; const benchmark = - variants[variantKey(effectiveChain, effectiveRegion)] ?? - variants[variantKey(null, null)] ?? + variants[variantKey(effectiveChain, effectiveRegion, effectiveKind)] ?? + variants[variantKey(null, null, null)] ?? Object.values(variants)[0]; if (!benchmark) return null; @@ -267,7 +282,10 @@ export function BenchmarkBody({ return ( <> - {(hasLayerSplit || chainOptions.length > 0 || regionOptions.length > 0) && ( + {(hasLayerSplit || + chainOptions.length > 0 || + regionOptions.length > 0 || + kindOptions.length > 0) && (
{hasLayerSplit && ( setLayer(v as ProviderLayer)} /> )} + {kindOptions.length > 0 && ( + [ + o.value, + summarize( + variants[variantKey(effectiveChain, effectiveRegion, o.value)], + ), + ]) + .filter(([, v]) => v !== null) as [string, ChainMeta][] + )} + /> + )} {chainOptions.length > 0 && ( [ o.value, - summarize(variants[variantKey(o.value, effectiveRegion)]), + summarize(variants[variantKey(o.value, effectiveRegion, effectiveKind)]), ]) .filter(([, v]) => v !== null) as [string, ChainMeta][] )} @@ -306,7 +342,7 @@ export function BenchmarkBody({ regionOptions .map((o) => [ o.value, - summarize(variants[variantKey(effectiveChain, o.value)]), + summarize(variants[variantKey(effectiveChain, o.value, effectiveKind)]), ]) .filter(([, v]) => v !== null) as [string, ChainMeta][] )} diff --git a/src/lib/spec-schema.ts b/src/lib/spec-schema.ts index 63c8cb72..1bbf7635 100644 --- a/src/lib/spec-schema.ts +++ b/src/lib/spec-schema.ts @@ -280,6 +280,14 @@ export const SpecSchema = z }) ) .optional(), + kind: z + .array( + z.object({ + value: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/), + label: z.string().min(1).max(64), + }) + ) + .optional(), }) .optional(), diff --git a/src/lib/spec.ts b/src/lib/spec.ts index 7af4a2ed..17dc357f 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -166,6 +166,7 @@ export const loadAllBenchmarks = cache(loadAllBenchmarksCached); export type BenchmarkFilters = { chain?: string; region?: string; + kind?: string; }; /** @@ -218,8 +219,8 @@ function parseFilterSig(sig: string): BenchmarkFilters { if (!sig) return out; for (const kv of sig.split("&")) { const [k, v] = kv.split("="); - if (k && v && (k === "chain" || k === "region")) { - out[k as "chain" | "region"] = v; + if (k && v && (k === "chain" || k === "region" || k === "kind")) { + out[k as "chain" | "region" | "kind"] = v; } } return out; diff --git a/src/types/benchmark.ts b/src/types/benchmark.ts index cf88bb6a..5b2f128d 100644 --- a/src/types/benchmark.ts +++ b/src/types/benchmark.ts @@ -148,6 +148,7 @@ export type Benchmark = { dimensions?: { chain?: { value: string; label: string }[]; region?: { value: string; label: string }[]; + kind?: { value: string; label: string }[]; }; category: "Aggregators" | "Bridges" | "Blockchains" | "Trading" | "Wallets" | "RPCs"; results: ProviderResult[];