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
17 changes: 14 additions & 3 deletions benchmarks/wallet-labels-coverage.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 }
Expand Down
28 changes: 20 additions & 8 deletions src/app/benchmarks/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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(),
Expand DownExpand Up@@ -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";
Expand DownExpand Up@@ -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}
/>
</Suspense>
)}
Expand DownExpand Up@@ -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 }) {
Expand Down
54 changes: 45 additions & 9 deletions src/components/benchmark-body.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,8 +99,12 @@
* 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
Expand All@@ -127,62 +131,73 @@
variants,
chainOptions,
regionOptions,
kindOptions = [],
initialChain,
initialRegion,
initialKind = null,
}: {
variants: Record<string, Benchmark>;
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/<slug> 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
// value that doesn't match the spec's dimensions.
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<string | null>(resolvedInitialChain);
const [region, setRegion] = useState<string | null>(resolvedInitialRegion);
const [kind, setKind] = useState<string | null>(resolvedInitialKind);
const [layer, setLayer] = useState<ProviderLayer>(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);
const next = url.pathname + (url.search ? url.search : "");
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;

// L1/L2 layer counts. When both > 0 the bench mixes L1 and L2 chains
// and we render a top-level Layer toggle that filters the entire page
// (chart + summary + ledger) to one layer at a time. Default is L1.
const layerCounts = useMemo(() => {

Check failure on line 200 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useMemo" is called conditionally. React Hooks must be called in the exact same order in every component render
let l1 = 0;
let l2 = 0;
for (const r of benchmark.results) {
Expand All@@ -197,7 +212,7 @@
// hasLayerSplit is false the original benchmark is returned untouched
// so non-layer benches keep their existing behavior. The chart, the
// summary stats and the ledger all read from `viewBenchmark`.
const viewBenchmark = useMemo(() => {

Check failure on line 215 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useMemo" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
if (!hasLayerSplit) return benchmark;
return {
...benchmark,
Expand All@@ -215,7 +230,7 @@
// they always saw.
const allowedViews = viewsForBenchmark(viewBenchmark);
const defaultView = defaultViewFor(viewBenchmark);
const [view, setView, viewMounted] = useViewPreference(

Check failure on line 233 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useViewPreference" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
viewBenchmark.slug,
defaultView,
allowedViews,
Expand All@@ -226,7 +241,7 @@
// hidden when they switch to distribution or donut - the model is
// "this is the field of providers the reader chose to focus on",
// not "what each view chose to drop". Resets on bench navigation.
const [excluded, setExcluded] = useState<Set<string>>(() => new Set());

Check failure on line 244 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
const toggleExclude = (slug: string) =>
setExcluded((prev) => {
const next = new Set(prev);
Expand All@@ -242,22 +257,22 @@
// being split between the dimension row and the chart toolbar.
const chartRegions = chartOnlyRegions(benchmark);
const showChartRegionRow = regionOptions.length === 0 && chartRegions.length > 1;
const [chartRegion, setChartRegion] = useState<string>("all");

Check failure on line 260 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?

// Active companion-metric panel. null = main spec metric (default chart
// data, default unit, default header). When a panel id is set, the chart
// pulls its per-provider series from panel.seriesByProvider, swaps the
// header label to panel.label, and the Y-axis unit to panel.unit.
const [activePanelId, setActivePanelId] = useState<string | null>(null);

Check failure on line 266 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
// Single Top-N value shared across every chart view AND the ledger
// so a reader who picks "Top 5" sees the same 5 providers in every
// surface. Each chart still computes its own option set off its own
// post-filter cohort, but the active value is parent-controlled.
const [topN, setTopN] = useState<number | null>(null);

Check failure on line 271 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
const topNControl = useMemo(() => ({ topN, setTopN }), [topN]);

Check failure on line 272 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useMemo" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
const activePanel =
benchmark.metricPanels?.find((p) => p.id === activePanelId) ?? null;
const chartRegionOptions: ChainOption[] = useMemo(

Check failure on line 275 in src/components/benchmark-body.tsx

View workflow job for this annotation

GitHub Actions/ check

React Hook "useMemo" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
() => [
{ value: "all", label: "All" },
...chartRegions.map((r) => ({ value: r, label: REGION_DISPLAY[r] ?? r })),
Expand All@@ -267,7 +282,10 @@

return (
<>
{(hasLayerSplit || chainOptions.length > 0 || regionOptions.length > 0) && (
{(hasLayerSplit ||
chainOptions.length > 0 ||
regionOptions.length > 0 ||
kindOptions.length > 0) && (
<div className="mt-8 space-y-3">
{hasLayerSplit && (
<DimensionRow
Expand All@@ -280,6 +298,24 @@
onSelect={(v) => setLayer(v as ProviderLayer)}
/>
)}
{kindOptions.length > 0 && (
<DimensionRow
label="Kind"
options={kindOptions}
selected={kind ?? fallbackKind}
onSelect={setKind}
metaByValue={Object.fromEntries(
kindOptions
.map((o) => [
o.value,
summarize(
variants[variantKey(effectiveChain, effectiveRegion, o.value)],
),
])
.filter(([, v]) => v !== null) as [string, ChainMeta][]
)}
/>
)}
{chainOptions.length > 0 && (
<DimensionRow
label="Chain"
Expand All@@ -290,7 +326,7 @@
chainOptions
.map((o) => [
o.value,
summarize(variants[variantKey(o.value, effectiveRegion)]),
summarize(variants[variantKey(o.value, effectiveRegion, effectiveKind)]),
])
.filter(([, v]) => v !== null) as [string, ChainMeta][]
)}
Expand All@@ -306,7 +342,7 @@
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][]
)}
Expand Down
8 changes: 8 additions & 0 deletions src/lib/spec-schema.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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(),

Expand Down
5 changes: 3 additions & 2 deletions src/lib/spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,6 +166,7 @@ export const loadAllBenchmarks = cache(loadAllBenchmarksCached);
export type BenchmarkFilters = {
chain?: string;
region?: string;
kind?: string;
};

/**
Expand DownExpand Up@@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/types/benchmark.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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[];
Expand Down
Loading