-
Block
+
Latest Block
{formatBlockNumber(blockNumber)}
diff --git a/src/components/pages/network/index.tsx b/src/components/pages/network/index.tsx
index 93e27fa..fde37ce 100644
--- a/src/components/pages/network/index.tsx
+++ b/src/components/pages/network/index.tsx
@@ -51,9 +51,11 @@ export default function Network() {
diff --git a/src/hooks/useNetworkDashboard.ts b/src/hooks/useNetworkDashboard.ts
index 26bc3d5..716b0e8 100644
--- a/src/hooks/useNetworkDashboard.ts
+++ b/src/hooks/useNetworkDashboard.ts
@@ -6,7 +6,7 @@
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { AppContext } from "../context/AppContext";
import { getNativeTokenPrice } from "../services/PriceService";
-import type { Block, NetworkStats, Transaction } from "../types";
+import type { Block, GasPrices, NetworkStats, Transaction } from "../types";
import { useDataService } from "./useDataService";
const REFRESH_INTERVAL = 10000; // 10 seconds
@@ -16,6 +16,7 @@ const TXS_TO_FETCH = 10;
export interface DashboardData {
price: number | null;
gasPrice: string | null;
+ gasPrices: GasPrices | null;
blockNumber: string | null;
latestBlocks: Block[];
latestTransactions: Transaction[];
@@ -27,6 +28,7 @@ export interface DashboardData {
const initialState: DashboardData = {
price: null,
gasPrice: null,
+ gasPrices: null,
blockNumber: null,
latestBlocks: [],
latestTransactions: [],
@@ -53,10 +55,11 @@ export function useNetworkDashboard(networkId: number): DashboardData {
isFetchingRef.current = true;
try {
- // Fetch network stats and price in parallel
+ // Fetch network stats, gas prices, and price in parallel
// For L2s, price is fetched from mainnet pools
- const [statsResult, priceResult] = await Promise.all([
+ const [statsResult, gasPricesResult, priceResult] = await Promise.all([
dataService.networkAdapter.getNetworkStats(),
+ dataService.networkAdapter.getGasPrices().catch(() => null),
rpcUrl
? getNativeTokenPrice(networkId, rpcUrl, mainnetRpcUrl || undefined)
: Promise.resolve(null),
@@ -106,6 +109,7 @@ export function useNetworkDashboard(networkId: number): DashboardData {
setData({
price: priceResult,
gasPrice: stats.currentGasPrice,
+ gasPrices: gasPricesResult?.data || null,
blockNumber: stats.currentBlockNumber,
latestBlocks: blocks,
latestTransactions: transactions,
diff --git a/src/services/adapters/ArbitrumAdapter/ArbitrumAdapter.ts b/src/services/adapters/ArbitrumAdapter/ArbitrumAdapter.ts
index 13a8442..f45b17b 100644
--- a/src/services/adapters/ArbitrumAdapter/ArbitrumAdapter.ts
+++ b/src/services/adapters/ArbitrumAdapter/ArbitrumAdapter.ts
@@ -26,6 +26,10 @@ export class ArbitrumAdapter extends NetworkAdapter {
this.initTxSearch(client as unknown as EthereumClient);
}
+ protected getClient(): EthereumClient {
+ return this.client as unknown as EthereumClient;
+ }
+
async getBlock(blockNumber: BlockNumberOrTag): Promise> {
const normalizedBlockNumber = normalizeBlockNumber(blockNumber);
const result = await this.client.getBlockByNumber(normalizedBlockNumber);
diff --git a/src/services/adapters/BNBAdapter/BNBAdapter.ts b/src/services/adapters/BNBAdapter/BNBAdapter.ts
index 038483f..250533e 100644
--- a/src/services/adapters/BNBAdapter/BNBAdapter.ts
+++ b/src/services/adapters/BNBAdapter/BNBAdapter.ts
@@ -26,6 +26,10 @@ export class BNBAdapter extends NetworkAdapter {
this.initTxSearch(client as unknown as EthereumClient);
}
+ protected getClient(): EthereumClient {
+ return this.client as unknown as EthereumClient;
+ }
+
async getBlock(blockNumber: BlockNumberOrTag): Promise> {
const normalizedBlockNumber = normalizeBlockNumber(blockNumber);
const result = await this.client.getBlockByNumber(normalizedBlockNumber);
diff --git a/src/services/adapters/BaseAdapter/BaseAdapter.ts b/src/services/adapters/BaseAdapter/BaseAdapter.ts
index 843fc9a..c7f0d7a 100644
--- a/src/services/adapters/BaseAdapter/BaseAdapter.ts
+++ b/src/services/adapters/BaseAdapter/BaseAdapter.ts
@@ -25,6 +25,10 @@ export class BaseAdapter extends NetworkAdapter {
this.initTxSearch(client as unknown as EthereumClient);
}
+ protected getClient(): EthereumClient {
+ return this.client as unknown as EthereumClient;
+ }
+
async getBlock(blockNumber: BlockNumberOrTag): Promise> {
const normalizedBlockNumber = normalizeBlockNumber(blockNumber);
const result = await this.client.getBlockByNumber(normalizedBlockNumber);
diff --git a/src/services/adapters/EVMAdapter/EVMAdapter.ts b/src/services/adapters/EVMAdapter/EVMAdapter.ts
index 0ed2f02..d0d0ae1 100644
--- a/src/services/adapters/EVMAdapter/EVMAdapter.ts
+++ b/src/services/adapters/EVMAdapter/EVMAdapter.ts
@@ -24,6 +24,10 @@ export class EVMAdapter extends NetworkAdapter {
this.client = client;
this.initTxSearch(client);
}
+
+ protected getClient(): EthereumClient {
+ return this.client;
+ }
async getBlock(blockNumber: BlockNumberOrTag): Promise> {
const normalizedBlockNumber = normalizeBlockNumber(blockNumber);
const result = await this.client.getBlockByNumber(normalizedBlockNumber);
diff --git a/src/services/adapters/NetworkAdapter.ts b/src/services/adapters/NetworkAdapter.ts
index c16f5f7..1545efb 100644
--- a/src/services/adapters/NetworkAdapter.ts
+++ b/src/services/adapters/NetworkAdapter.ts
@@ -6,7 +6,9 @@ import type {
NetworkStats,
DataWithMetadata,
AddressTransactionsResult,
+ GasPrices,
} from "../../types";
+import { extractData } from "./shared/extractData";
import { AddressTransactionSearch } from "../AddressTransactionSearch";
export type BlockTag = "latest" | "earliest" | "pending" | "finalized" | "safe";
@@ -52,6 +54,12 @@ export abstract class NetworkAdapter {
this.isLocalHost = networkId === 31337;
}
+ /**
+ * Get the Ethereum client for RPC calls
+ * Each adapter must implement this to provide its client
+ */
+ protected abstract getClient(): EthereumClient;
+
/**
* Initialize the transaction search service
* Call this in subclass constructors to enable binary search tx discovery
@@ -59,6 +67,7 @@ export abstract class NetworkAdapter {
protected initTxSearch(client: EthereumClient): void {
this.txSearch = new AddressTransactionSearch(client);
}
+
/**
* Get block by number or tag
* @param blockNumber - Block number (as number or hex string) or block tag
@@ -178,6 +187,88 @@ export abstract class NetworkAdapter {
*/
abstract getNetworkStats(): Promise>;
+ /**
+ * Get gas prices with tiers (Low/Average/High) using eth_feeHistory
+ * @returns Gas price tiers and base fee
+ */
+ async getGasPrices(): Promise> {
+ const client = this.getClient();
+
+ // Fetch fee history for last 20 blocks with 25th, 50th, 75th percentiles
+ const feeHistoryResult = await client.feeHistory("0x14", "latest", [25, 50, 75]);
+ const feeHistory = extractData<{
+ baseFeePerGas: string[];
+ gasUsedRatio: number[];
+ oldestBlock: string;
+ reward?: string[][];
+ }>(feeHistoryResult.data);
+
+ if (!feeHistory || !feeHistory.reward || feeHistory.reward.length === 0) {
+ // Fallback to simple gas price if feeHistory not available
+ const gasPriceResult = await client.gasPrice();
+ const gasPrice = extractData(gasPriceResult.data) || "0x0";
+ const blockNumResult = await client.blockNumber();
+ const blockNum = extractData(blockNumResult.data) || "0x0";
+
+ return {
+ data: {
+ low: gasPrice,
+ average: gasPrice,
+ high: gasPrice,
+ baseFee: gasPrice,
+ lastBlock: blockNum,
+ },
+ metadata: gasPriceResult.metadata as DataWithMetadata["metadata"],
+ };
+ }
+
+ // Calculate average priority fees across all blocks for each percentile
+ const rewards = feeHistory.reward;
+ let lowSum = BigInt(0);
+ let avgSum = BigInt(0);
+ let highSum = BigInt(0);
+ let count = 0;
+
+ for (const blockRewards of rewards) {
+ if (blockRewards && blockRewards.length >= 3) {
+ lowSum += BigInt(blockRewards[0] || "0x0");
+ avgSum += BigInt(blockRewards[1] || "0x0");
+ highSum += BigInt(blockRewards[2] || "0x0");
+ count++;
+ }
+ }
+
+ // Get the latest base fee (last element in baseFeePerGas array)
+ const baseFees = feeHistory.baseFeePerGas;
+ const latestBaseFee = baseFees[baseFees.length - 1] || "0x0";
+
+ // Calculate averages
+ const lowPriorityFee = count > 0 ? lowSum / BigInt(count) : BigInt(0);
+ const avgPriorityFee = count > 0 ? avgSum / BigInt(count) : BigInt(0);
+ const highPriorityFee = count > 0 ? highSum / BigInt(count) : BigInt(0);
+
+ // Total gas price = base fee + priority fee
+ const baseFeeNum = BigInt(latestBaseFee);
+ const low = baseFeeNum + lowPriorityFee;
+ const average = baseFeeNum + avgPriorityFee;
+ const high = baseFeeNum + highPriorityFee;
+
+ // Calculate last block number
+ const oldestBlock = BigInt(feeHistory.oldestBlock);
+ const lastBlock = oldestBlock + BigInt(baseFees.length - 1);
+
+ return {
+ data: {
+ low: `0x${low.toString(16)}`,
+ average: `0x${average.toString(16)}`,
+ high: `0x${high.toString(16)}`,
+ baseFee: latestBaseFee,
+ lastBlock: `0x${lastBlock.toString(16)}`,
+ },
+ metadata: feeHistoryResult.metadata as DataWithMetadata["metadata"],
+ };
+ }
+
/**
* Get latest N blocks
* @param count - Number of blocks to retrieve
diff --git a/src/services/adapters/OptimismAdapter/OptimismAdapter.ts b/src/services/adapters/OptimismAdapter/OptimismAdapter.ts
index 1f2863b..5f32631 100644
--- a/src/services/adapters/OptimismAdapter/OptimismAdapter.ts
+++ b/src/services/adapters/OptimismAdapter/OptimismAdapter.ts
@@ -25,6 +25,10 @@ export class OptimismAdapter extends NetworkAdapter {
this.initTxSearch(client as unknown as EthereumClient);
}
+ protected getClient(): EthereumClient {
+ return this.client as unknown as EthereumClient;
+ }
+
async getBlock(blockNumber: BlockNumberOrTag): Promise> {
const normalizedBlockNumber = normalizeBlockNumber(blockNumber);
const result = await this.client.getBlockByNumber(normalizedBlockNumber);
diff --git a/src/services/adapters/PolygonAdapter/PolygonAdapter.ts b/src/services/adapters/PolygonAdapter/PolygonAdapter.ts
index fd680ce..7aecf60 100644
--- a/src/services/adapters/PolygonAdapter/PolygonAdapter.ts
+++ b/src/services/adapters/PolygonAdapter/PolygonAdapter.ts
@@ -24,6 +24,11 @@ export class PolygonAdapter extends NetworkAdapter {
this.client = client;
this.initTxSearch(client as unknown as EthereumClient);
}
+
+ protected getClient(): EthereumClient {
+ return this.client as unknown as EthereumClient;
+ }
+
async getBlock(blockNumber: BlockNumberOrTag): Promise> {
const normalizedBlockNumber = normalizeBlockNumber(blockNumber);
const result = await this.client.getBlockByNumber(normalizedBlockNumber);
diff --git a/src/styles/components.css b/src/styles/components.css
index 587ab14..b9b941a 100644
--- a/src/styles/components.css
+++ b/src/styles/components.css
@@ -1968,11 +1968,6 @@
background: rgba(255, 255, 255, 0.05);
border: 1px solid color-mix(in srgb, var(--network-color, #888) 25%, transparent);
border-radius: 8px;
- transition: all 0.2s ease;
-}
-
-.network-block-indicator:hover {
- background: rgba(255, 255, 255, 0.08);
}
.network-block-pulse {
@@ -2033,6 +2028,15 @@
line-height: 1.2;
}
+.network-gas-tracker {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding-left: 8px;
+ border-left: 1px solid rgba(255, 255, 255, 0.1);
+ cursor: pointer;
+}
+
/* Pagination */
.pagination-container {
display: flex;
@@ -5118,6 +5122,7 @@
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
+ text-align: center;
}
.dashboard-stat-value {
@@ -5131,6 +5136,82 @@
opacity: 0.5;
}
+/* Clickable stat label */
+.dashboard-stat-label-link {
+ display: block;
+ font-size: 0.8rem;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: 8px;
+ text-align: center;
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.dashboard-stat-label-link:hover {
+ color: var(--color-primary);
+}
+
+/* Gas Price Tiers */
+.dashboard-stat-card-gas {
+ padding: 16px 12px;
+}
+
+.dashboard-gas-tiers {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.gas-tier {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ flex: 1;
+ padding: 6px 8px;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.gas-tier-label {
+ font-size: 0.7rem;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: 4px;
+}
+
+.gas-tier-value {
+ font-family: "Outfit", sans-serif;
+ font-size: 0.95rem;
+ font-weight: 600;
+}
+
+.gas-tier-low .gas-tier-label {
+ color: #4ade80;
+}
+
+.gas-tier-low .gas-tier-value {
+ color: #4ade80;
+}
+
+.gas-tier-avg .gas-tier-label {
+ color: #fbbf24;
+}
+
+.gas-tier-avg .gas-tier-value {
+ color: #fbbf24;
+}
+
+.gas-tier-high .gas-tier-label {
+ color: #f87171;
+}
+
+.gas-tier-high .gas-tier-value {
+ color: #f87171;
+}
+
/* Dashboard Tables Row */
.dashboard-tables-row {
display: grid;
@@ -5396,6 +5477,10 @@
font-size: 1.25rem;
}
+ .gas-tier-value {
+ font-size: 0.85rem;
+ }
+
.dashboard-table-row {
flex-wrap: wrap;
gap: 8px;
@@ -5598,3 +5683,251 @@
padding: 8px 12px;
}
}
+
+/* ============================================
+ Gas Tracker Page Styles
+ ============================================ */
+
+.gas-tracker-header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+.gas-tracker-back-link {
+ font-size: 0.9rem;
+ color: var(--color-primary-muted);
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+.gas-tracker-back-link:hover {
+ color: var(--color-primary);
+}
+
+.gas-tracker-section-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--color-text);
+ text-align: center;
+ margin: 0 0px 20px 0px;
+}
+
+.gas-tracker-section-subtitle {
+ font-size: 0.85rem;
+ color: var(--color-text-muted);
+ margin-bottom: 20px;
+ text-align: center;
+ margin: 0 0px 20px 0px;
+}
+
+/* Gas Price Cards */
+.gas-price-cards {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+ margin-bottom: 20px;
+}
+
+.gas-price-card {
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 12px;
+ padding: 24px;
+ text-align: center;
+}
+
+.gas-price-tier-label {
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: 8px;
+}
+
+.gas-price-tier-value {
+ font-family: "Outfit", sans-serif;
+ font-size: 1.75rem;
+ font-weight: 700;
+ margin-bottom: 8px;
+}
+
+.gas-price-tier-time {
+ font-size: 0.8rem;
+ color: var(--color-text-muted);
+}
+
+.gas-price-low .gas-price-tier-label,
+.gas-price-low .gas-price-tier-value {
+ color: #4ade80;
+}
+
+.gas-price-avg .gas-price-tier-label,
+.gas-price-avg .gas-price-tier-value {
+ color: #fbbf24;
+}
+
+.gas-price-high .gas-price-tier-label,
+.gas-price-high .gas-price-tier-value {
+ color: #f87171;
+}
+
+.gas-base-fee {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+ padding: 12px;
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 8px;
+ margin-bottom: 10px;
+}
+
+.gas-base-fee-label {
+ color: var(--color-text-muted);
+}
+
+.gas-base-fee-value {
+ font-weight: 600;
+ color: var(--color-text);
+}
+
+/* Transaction Cost Table */
+.tx-cost-table {
+ background: rgba(255, 255, 255, 0.02);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+.tx-cost-header {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1.5fr 1.5fr 1.5fr;
+ padding: 12px 16px;
+ background: rgba(255, 255, 255, 0.03);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--color-text-muted);
+}
+
+.tx-cost-row {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1.5fr 1.5fr 1.5fr;
+ padding: 16px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
+ transition: background 0.2s ease;
+}
+
+.tx-cost-row:last-child {
+ border-bottom: none;
+}
+
+.tx-cost-row:hover {
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.tx-cost-col-name {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.tx-cost-name {
+ font-weight: 600;
+ color: var(--color-text);
+}
+
+.tx-cost-desc {
+ font-size: 0.75rem;
+ color: var(--color-text-muted);
+}
+
+.tx-cost-col-gas {
+ font-family: "JetBrains Mono", monospace;
+ font-size: 0.85rem;
+ color: var(--color-text-muted);
+ display: flex;
+ align-items: center;
+}
+
+.tx-cost-col-low,
+.tx-cost-col-avg,
+.tx-cost-col-high {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.tx-cost-eth {
+ font-family: "JetBrains Mono", monospace;
+ font-size: 0.85rem;
+ color: var(--color-text);
+}
+
+.tx-cost-usd {
+ font-size: 0.75rem;
+ color: var(--color-text-muted);
+}
+
+.tx-cost-col-low .tx-cost-eth {
+ color: #4ade80;
+}
+
+.tx-cost-col-avg .tx-cost-eth {
+ color: #fbbf24;
+}
+
+.tx-cost-col-high .tx-cost-eth {
+ color: #f87171;
+}
+
+.gas-tracker-updated {
+ text-align: center;
+ font-size: 0.8rem;
+ color: var(--color-text-muted);
+ padding-top: 16px;
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+/* Gas Tracker Responsive */
+@media (max-width: 900px) {
+ .gas-price-cards {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .tx-cost-header,
+ .tx-cost-row {
+ grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
+ }
+}
+
+@media (max-width: 640px) {
+ .gas-tracker-header {
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .gas-price-cards {
+ grid-template-columns: 1fr;
+ }
+
+ .gas-price-card {
+ padding: 16px;
+ }
+
+ .gas-price-tier-value {
+ font-size: 1.5rem;
+ }
+
+ .tx-cost-table {
+ overflow-x: auto;
+ }
+
+ .tx-cost-header,
+ .tx-cost-row {
+ min-width: 600px;
+ }
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index 3de9c6b..4333c33 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -12,6 +12,14 @@ export interface NetworkStats {
metadata: any;
}
+export interface GasPrices {
+ low: string; // Wei hex - 25th percentile priority fee
+ average: string; // Wei hex - 50th percentile priority fee
+ high: string; // Wei hex - 75th percentile priority fee
+ baseFee: string; // Current base fee (EIP-1559)
+ lastBlock: string; // Block number hex
+}
+
export interface Block {
difficulty: string;
extraData: string;
diff --git a/src/utils/formatUtils.ts b/src/utils/formatUtils.ts
new file mode 100644
index 0000000..67a687e
--- /dev/null
+++ b/src/utils/formatUtils.ts
@@ -0,0 +1,58 @@
+/**
+ * Gas price formatting result with separate value and unit
+ */
+export interface FormattedGasPrice {
+ value: string;
+ unit: string;
+}
+
+/**
+ * Formats a gas price from wei (hex string) to the most appropriate unit.
+ * Automatically selects Gwei, Mwei, Kwei, or wei based on the value.
+ * Handles L2 networks like Optimism that have very low gas prices.
+ *
+ * @param weiHex - Gas price in wei as a hex string (e.g., "0x3b9aca00")
+ * @returns Object with formatted value and unit, or combined string
+ */
+export function formatGasPrice(weiHex: string): FormattedGasPrice {
+ try {
+ const wei = BigInt(weiHex);
+ const weiNum = Number(wei);
+ const gwei = weiNum / 1e9;
+
+ // Use Gwei for normal gas prices (>= 0.01 Gwei)
+ if (gwei >= 0.01) {
+ return { value: gwei.toFixed(2), unit: "Gwei" };
+ }
+
+ // Use Mwei for very low gas prices (L2s like Optimism)
+ const mwei = weiNum / 1e6;
+ if (mwei >= 0.01) {
+ return { value: mwei.toFixed(2), unit: "Mwei" };
+ }
+
+ // Use Kwei for extremely low gas prices
+ const kwei = weiNum / 1e3;
+ if (kwei >= 0.01) {
+ return { value: kwei.toFixed(2), unit: "Kwei" };
+ }
+
+ // Fallback to wei
+ return { value: weiNum.toFixed(0), unit: "wei" };
+ } catch {
+ return { value: "—", unit: "Gwei" };
+ }
+}
+
+/**
+ * Formats a gas price from wei (hex string) to a combined string with unit.
+ * Convenience wrapper around formatGasPrice for cases where a single string is needed.
+ *
+ * @param weiHex - Gas price in wei as a hex string, or null
+ * @returns Formatted string like "1.23 Gwei" or "—" if invalid
+ */
+export function formatGasPriceWithUnit(weiHex: string | null): string {
+ if (!weiHex) return "—";
+ const { value, unit } = formatGasPrice(weiHex);
+ return `${value} ${unit}`;
+}