From 17ca7b2bd09cc5355093d79a31f307590e47ef01 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Tue, 2 Jun 2026 18:05:59 +0200 Subject: [PATCH] feat(network-fees): L1/L2 toggle + 9 L2 providers --- benchmarks/network-fees.yml | 128 +++++++++++++++++++++++++++++- src/components/benchmark-body.tsx | 37 ++++++++- src/components/layer-filter.tsx | 63 +++++++++++++++ src/lib/spec-schema.ts | 10 +++ src/lib/spec.ts | 3 + src/types/benchmark.ts | 9 +++ 6 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 src/components/layer-filter.tsx diff --git a/benchmarks/network-fees.yml b/benchmarks/network-fees.yml index 2a75701a..6a679cd3 100644 --- a/benchmarks/network-fees.yml +++ b/benchmarks/network-fees.yml @@ -2,10 +2,10 @@ slug: network-fees number: "031" -title: Current native transfer fee across L1 blockchains -seo_title: "Cheapest blockchain transaction fee 2026: Ethereum, Solana, BNB, Avalanche, TRON, Cardano, SUI, TON, Stellar, Litecoin, Monero USD live" -seo_description: "Live USD cost of a native-token transfer on 11 L1 chains: Ethereum, Solana, BNB, Avalanche, TRON, Cardano, SUI, TON, Stellar, Litecoin, Monero. Slow/standard/fast tiers refreshed every 30s, USD price from Mobula." -subtitle: "Current USD cost of moving the chain's native asset, refreshed every 30 seconds for the same 11 L1s tracked on the finality bench." +title: Current native transfer fee across L1 and L2 blockchains +seo_title: "Cheapest blockchain transaction fee 2026: L1 and L2 USD live (Ethereum, Arbitrum, Optimism, Base, zkSync, Solana, BNB, Avalanche)" +seo_description: "Live USD cost of a native-token transfer across 20 L1 and L2 chains. L1: Ethereum, Solana, BNB, Avalanche, TRON, Cardano, SUI, TON, Stellar, Litecoin, Monero. L2: Arbitrum, Optimism, Base, zkSync, Linea, Scroll, Blast, Mantle, Taiko. Slow/standard/fast tiers refreshed every 30s." +subtitle: "Current USD cost of moving the chain's native asset across 20 L1 and L2 chains, refreshed every 30 seconds." seo_intro: | This page shows the live USD cost of sending the native token on every major Layer-1 blockchain. We sample the chain-native fee market (eth_feeHistory for EVM chains, getRecentPrioritizationFees for Solana, koios protocol params for Cardano, fee_stats for Stellar, fee_estimate for Monero, mempool oracles for Litecoin, getChainParameters for TRON, sui_getReferenceGasPrice for SUI, hardcoded typical observed for TON), convert to the chain's smallest unit, then multiply by the native-token USD price pulled from Mobula every 30 seconds. The result is a head-to-head comparison of what it actually costs an end user to move one unit of value on each chain, rather than a gas-price comparison in gwei or lamports which is meaningless across protocols. @@ -83,6 +83,7 @@ prometheus: providers: - slug: ethereum name: Ethereum + layer: l1 tag: EIP-1559 fee market, 21000 gas × (base + p50 priority) formula: "Median USD cost of a native ETH transfer over 24h: (base_fee + p50 priority_fee) × 21000 gas × ETH USD price." queries: @@ -95,6 +96,7 @@ providers: - slug: bnb name: BNB Chain + layer: l1 tag: EIP-1559 fee market, 21000 gas formula: "Median USD cost of a native BNB transfer over 24h." queries: @@ -107,6 +109,7 @@ providers: - slug: avalanche name: Avalanche + layer: l1 tag: C-Chain EIP-1559 fee market, 21000 gas formula: "Median USD cost of a native AVAX transfer over 24h." queries: @@ -119,6 +122,7 @@ providers: - slug: solana name: Solana + layer: l1 tag: 5000 lamport base + getRecentPrioritizationFees × 200 CU formula: "Median USD cost of a native SOL transfer over 24h: 5000 lamports + p50 priority × 200 CU × SOL USD price." queries: @@ -131,6 +135,7 @@ providers: - slug: tron name: TRON + layer: l1 tag: Bandwidth model, 268 bytes × getTransactionFee SUN/byte formula: "Deterministic USD cost of a native TRX transfer using the bandwidth-rate parameter × 268 bytes typical transfer × TRX USD price." queries: @@ -143,6 +148,7 @@ providers: - slug: cardano name: Cardano + layer: l1 tag: min_fee_b + size × min_fee_a, deterministic formula: "Protocol-deterministic ADA transfer cost: koios live min_fee_a/b × 250 bytes typical × ADA USD price." queries: @@ -155,6 +161,7 @@ providers: - slug: sui name: Sui + layer: l1 tag: suix_getReferenceGasPrice × 2_000_000 gas budget formula: "USD cost of a native SUI transfer: live reference gas price × 2M gas budget × SUI USD price." queries: @@ -167,6 +174,7 @@ providers: - slug: ton name: TON + layer: l1 tag: Hardcoded 0.005 TON typical wallet-v4 transfer formula: "Conservative typical-observed cost of a TON wallet transfer × TON USD price. TON has no fee-estimate RPC; this is the observed median." queries: @@ -179,6 +187,7 @@ providers: - slug: stellar name: Stellar + layer: l1 tag: 100 stroops base per operation, surge during congestion formula: "Live horizon fee_stats.last_ledger_base_fee × 1 op × XLM USD price." queries: @@ -191,6 +200,7 @@ providers: - slug: litecoin name: Litecoin + layer: l1 tag: litecoinspace fee oracle × 225 vBytes formula: "Mempool-based litoshi/vByte × 225 vBytes typical P2WPKH transfer × LTC USD price." queries: @@ -203,6 +213,7 @@ providers: - slug: monero name: Monero + layer: l1 tag: get_fee_estimate × 1500 bytes RingCT formula: "Monero-rpc fee per byte × 1500 bytes typical 1-in-2-out RingCT × XMR USD price." queries: @@ -212,3 +223,112 @@ providers: mean: avg_over_time(tx_fee_native_transfer_usd{chain="monero",tier="std"}[24h]) success: avg_over_time(tx_fee_health{chain="monero"}[24h]) series: tx_fee_native_transfer_usd{chain="monero",tier="std"} + + - slug: arbitrum + name: Arbitrum One + layer: l2 + tag: "Optimistic rollup (Nitro), 21000 gas" + formula: "Median USD cost of a native ETH transfer on Arbitrum One over 24h. L2 execution cost only; L1 data posting fee excluded (Phase 2)." + queries: + p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="arbitrum",tier="std"}[24h]) + p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="arbitrum",tier="fast"}[24h]) + p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="arbitrum",tier="fast"}[24h]) + mean: avg_over_time(tx_fee_native_transfer_usd{chain="arbitrum",tier="std"}[24h]) + success: avg_over_time(tx_fee_health{chain="arbitrum"}[24h]) + series: tx_fee_native_transfer_usd{chain="arbitrum",tier="std"} + - slug: optimism + name: Optimism + layer: l2 + tag: "OP Stack optimistic rollup, 21000 gas" + formula: "Median USD cost of a native ETH transfer on Optimism over 24h. L2 execution cost only; L1 data posting fee excluded (Phase 2)." + queries: + p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="optimism",tier="std"}[24h]) + p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="optimism",tier="fast"}[24h]) + p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="optimism",tier="fast"}[24h]) + mean: avg_over_time(tx_fee_native_transfer_usd{chain="optimism",tier="std"}[24h]) + success: avg_over_time(tx_fee_health{chain="optimism"}[24h]) + series: tx_fee_native_transfer_usd{chain="optimism",tier="std"} + - slug: base + name: Base + layer: l2 + tag: "OP Stack optimistic rollup (Coinbase), 21000 gas" + formula: "Median USD cost of a native ETH transfer on Base over 24h. L2 execution cost only; L1 data posting fee excluded (Phase 2)." + queries: + p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="base",tier="std"}[24h]) + p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="base",tier="fast"}[24h]) + p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="base",tier="fast"}[24h]) + mean: avg_over_time(tx_fee_native_transfer_usd{chain="base",tier="std"}[24h]) + success: avg_over_time(tx_fee_health{chain="base"}[24h]) + series: tx_fee_native_transfer_usd{chain="base",tier="std"} + - slug: zksync + name: zkSync Era + layer: l2 + tag: "ZK rollup, 21000 gas" + formula: "Median USD cost of a native ETH transfer on zkSync Era over 24h. L2 execution cost only; L1 data posting fee excluded (Phase 2)." + queries: + p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="zksync",tier="std"}[24h]) + p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="zksync",tier="fast"}[24h]) + p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="zksync",tier="fast"}[24h]) + mean: avg_over_time(tx_fee_native_transfer_usd{chain="zksync",tier="std"}[24h]) + success: avg_over_time(tx_fee_health{chain="zksync"}[24h]) + series: tx_fee_native_transfer_usd{chain="zksync",tier="std"} + - slug: linea + name: Linea + layer: l2 + tag: "ZK rollup (Stark), 21000 gas" + formula: "Median USD cost of a native ETH transfer on Linea over 24h. L2 execution cost only; L1 data posting fee excluded (Phase 2)." + queries: + p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="linea",tier="std"}[24h]) + p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="linea",tier="fast"}[24h]) + p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="linea",tier="fast"}[24h]) + mean: avg_over_time(tx_fee_native_transfer_usd{chain="linea",tier="std"}[24h]) + success: avg_over_time(tx_fee_health{chain="linea"}[24h]) + series: tx_fee_native_transfer_usd{chain="linea",tier="std"} + - slug: scroll + name: Scroll + layer: l2 + tag: "zkEVM rollup, 21000 gas" + formula: "Median USD cost of a native ETH transfer on Scroll over 24h. L2 execution cost only; L1 data posting fee excluded (Phase 2)." + queries: + p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="scroll",tier="std"}[24h]) + p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="scroll",tier="fast"}[24h]) + p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="scroll",tier="fast"}[24h]) + mean: avg_over_time(tx_fee_native_transfer_usd{chain="scroll",tier="std"}[24h]) + success: avg_over_time(tx_fee_health{chain="scroll"}[24h]) + series: tx_fee_native_transfer_usd{chain="scroll",tier="std"} + - slug: blast + name: Blast + layer: l2 + tag: "OP Stack optimistic rollup (Blast), 21000 gas" + formula: "Median USD cost of a native ETH transfer on Blast over 24h. L2 execution cost only; L1 data posting fee excluded (Phase 2)." + queries: + p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="blast",tier="std"}[24h]) + p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="blast",tier="fast"}[24h]) + p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="blast",tier="fast"}[24h]) + mean: avg_over_time(tx_fee_native_transfer_usd{chain="blast",tier="std"}[24h]) + success: avg_over_time(tx_fee_health{chain="blast"}[24h]) + series: tx_fee_native_transfer_usd{chain="blast",tier="std"} + - slug: mantle + name: Mantle + layer: l2 + tag: "OP Stack optimistic rollup (Mantle), 21000 gas" + formula: "Median USD cost of a native ETH transfer on Mantle over 24h. L2 execution cost only; L1 data posting fee excluded (Phase 2)." + queries: + p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="mantle",tier="std"}[24h]) + p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="mantle",tier="fast"}[24h]) + p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="mantle",tier="fast"}[24h]) + mean: avg_over_time(tx_fee_native_transfer_usd{chain="mantle",tier="std"}[24h]) + success: avg_over_time(tx_fee_health{chain="mantle"}[24h]) + series: tx_fee_native_transfer_usd{chain="mantle",tier="std"} + - slug: taiko + name: Taiko + layer: l2 + tag: "Based rollup, 21000 gas" + formula: "Median USD cost of a native ETH transfer on Taiko over 24h. L2 execution cost only; L1 data posting fee excluded (Phase 2)." + queries: + p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="taiko",tier="std"}[24h]) + p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="taiko",tier="fast"}[24h]) + p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="taiko",tier="fast"}[24h]) + mean: avg_over_time(tx_fee_native_transfer_usd{chain="taiko",tier="std"}[24h]) + success: avg_over_time(tx_fee_health{chain="taiko"}[24h]) + series: tx_fee_native_transfer_usd{chain="taiko",tier="std"} diff --git a/src/components/benchmark-body.tsx b/src/components/benchmark-body.tsx index 36c8cf5c..1e4e5a3a 100644 --- a/src/components/benchmark-body.tsx +++ b/src/components/benchmark-body.tsx @@ -12,6 +12,7 @@ import { DistributionChart } from "@/components/distribution-chart"; import { DonutChart } from "@/components/donut-chart"; import { RegionGrid } from "@/components/region-grid"; import { MetricViewTabs } from "@/components/metric-view-tabs"; +import { LayerFilter, type LayerSelection } from "@/components/layer-filter"; import { CountLeaderboard } from "@/components/count-leaderboard"; import { SummaryStat } from "@/components/summary-stat"; import { ViewSwitcher } from "@/components/view-switcher"; @@ -143,23 +144,29 @@ export function BenchmarkBody({ const searchParams = useSearchParams(); const urlChain = searchParams.get("chain"); const urlRegion = searchParams.get("region"); + 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 resolvedInitialLayer: LayerSelection = + urlLayer === "l1" || urlLayer === "l2" ? urlLayer : "all"; const [chain, setChain] = useState(resolvedInitialChain); const [region, setRegion] = useState(resolvedInitialRegion); + const [layer, setLayer] = useState(resolvedInitialLayer); useEffect(() => { const url = new URL(window.location.href); syncParam(url, "chain", chain, chainOptions); syncParam(url, "region", region, regionOptions); + if (layer === "all") 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, chainOptions, regionOptions]); + }, [chain, region, layer, chainOptions, regionOptions]); const fallbackChain = chainOptions[0]?.value ?? null; const fallbackRegion = regionOptions[0]?.value ?? null; @@ -171,6 +178,29 @@ export function BenchmarkBody({ Object.values(variants)[0]; if (!benchmark) return null; + // L1/L2 filter counts, derived from the full unfiltered results so the + // pill counts are stable as the user toggles the filter. + const layerCounts = useMemo(() => { + let l1 = 0; + let l2 = 0; + for (const r of benchmark.results) { + if (r.layer === "l1") l1++; + else if (r.layer === "l2") l2++; + } + return { all: benchmark.results.length, l1, l2 }; + }, [benchmark.results]); + + // Filtered benchmark for the ledger table only. Charts above keep the + // full provider set so the chart vs ledger don't disagree on which + // providers exist. + const ledgerBenchmark = useMemo(() => { + if (layer === "all") return benchmark; + return { + ...benchmark, + results: benchmark.results.filter((r) => r.layer === layer), + }; + }, [benchmark, layer]); + const isDraft = benchmark.status === "draft"; const { fieldMin, fieldMedian, fieldMax, tailMin, tailMax, tailSpread } = computeFieldStats(benchmark.results); @@ -388,7 +418,10 @@ export function BenchmarkBody({ ? `Product ledger · sorted by ${activePanel.label}` : "Product ledger · sorted by p50"}

- + {layerCounts.all > 0 && (layerCounts.l1 > 0 || layerCounts.l2 > 0) && ( + + )} + {benchmark.unit !== "count" && diff --git a/src/components/layer-filter.tsx b/src/components/layer-filter.tsx new file mode 100644 index 00000000..1bf6d338 --- /dev/null +++ b/src/components/layer-filter.tsx @@ -0,0 +1,63 @@ +"use client"; + +/** + * Three-pill filter (All / L1 / L2) shown above the ledger table on + * benches that mix L1 and L2 providers (currently only `network-fees`). + * + * Pure presentation: holds no state. The parent (benchmark-body) owns + * the active layer and passes it down with the swap callback. The + * parent also decides whether to render this at all — when no provider + * carries a `layer` field, the filter stays hidden. + */ +export type LayerSelection = "all" | "l1" | "l2"; + +export function LayerFilter({ + selected, + onSelect, + counts, +}: { + selected: LayerSelection; + onSelect: (v: LayerSelection) => void; + counts: { all: number; l1: number; l2: number }; +}) { + return ( +
+ + Layer + + onSelect("all")} /> + onSelect("l1")} /> + onSelect("l2")} /> +
+ ); +} + +function Pill({ + label, + count, + active, + onClick, +}: { + label: string; + count: number; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/src/lib/spec-schema.ts b/src/lib/spec-schema.ts index 743cc9bb..63c8cb72 100644 --- a/src/lib/spec-schema.ts +++ b/src/lib/spec-schema.ts @@ -114,6 +114,12 @@ export const ProviderType = z.enum([ "relay", // relayer / settlement network (Relay, deBridge solver layer) ]); +/** Network-layer classification used by benches that mix L1 and L2 chains + * in the same provider set (e.g. network-fees). Powers the L1/L2/All + * toggle pill on the bench page so readers can split the leaderboard + * by execution layer without leaving the page. */ +export const ProviderLayer = z.enum(["l1", "l2"]); + const provider = z.object({ /** Stable identifier. also used as the metric label. */ slug, @@ -128,6 +134,10 @@ const provider = z.object({ /** Optional architectural category. When set, a badge appears next to * the name so readers understand the comparison. */ type: ProviderType.optional(), + /** Optional network-layer classification. When set, a bench that mixes + * L1 and L2 providers (network-fees) renders an L1/L2/All toggle pill + * that filters the ledger by this field. */ + layer: ProviderLayer.optional(), /** Optional secondary metric (e.g. "Chains covered") shown in the table. */ secondary: z .object({ label: z.string(), value: z.string() }) diff --git a/src/lib/spec.ts b/src/lib/spec.ts index 33ca2784..bc57f4a8 100644 --- a/src/lib/spec.ts +++ b/src/lib/spec.ts @@ -354,6 +354,7 @@ async function specToBenchmark( slug: p.slug, tag: p.tag, type: p.type, + layer: p.layer, ms: { p50: 0, p90: 0, p99: 0, mean: 0 }, successRate: 0, secondary: p.secondary, @@ -611,6 +612,7 @@ async function tryLoadLive( slug: p.slug, tag: p.tag, type: p.type, + layer: p.layer, ms: { p50, p90, p99, mean: mean ?? p50 }, slots: slotP50 != null && slotP99 != null @@ -754,6 +756,7 @@ function draftBenchmark( slug: p.slug, tag: p.tag, type: p.type, + layer: p.layer, ms: { p50: 0, p90: 0, p99: 0, mean: 0 }, successRate: 0, secondary: p.secondary, diff --git a/src/types/benchmark.ts b/src/types/benchmark.ts index b1c82fc7..9843454f 100644 --- a/src/types/benchmark.ts +++ b/src/types/benchmark.ts @@ -6,6 +6,11 @@ export type ProviderType = "protocol" | "aggregator" | "intent" | "relay"; +/** Network-layer classification used by benches that mix L1 and L2 chains + * in the same provider set (network-fees). Drives the L1/L2/All toggle + * pill rendered above the ledger table. */ +export type ProviderLayer = "l1" | "l2"; + /** * Per-provider data availability. Drives whether the leaderboard renders * the numbers or a "currently unavailable" stub. @@ -25,6 +30,10 @@ export type ProviderResult = { /** Architectural category. Drives the small badge next to the name so * readers know whether comparisons are apples-to-apples. */ type?: ProviderType; + /** Optional network-layer classification. When set on every provider of + * a bench, the bench page renders an L1/L2/All toggle pill above the + * ledger that filters rows by this field. */ + layer?: ProviderLayer; ms: { p50: number; p90: number; p99: number; mean: number }; /** Optional slot-level companion to ms. Used by Solana-native benches * where confirmation is a slot-level event (~400 ms granularity).