diff --git a/app/[locale]/resources/_components/SlotCountdown.tsx b/app/[locale]/resources/_components/SlotCountdown.tsx
index fbbfaf587a2..2a5bf5fa062 100644
--- a/app/[locale]/resources/_components/SlotCountdown.tsx
+++ b/app/[locale]/resources/_components/SlotCountdown.tsx
@@ -24,16 +24,18 @@ const SlotCountdownChart = ({ children }: { children: string }) => {
}, [])
return (
-
+
+
+
)
}
diff --git a/app/[locale]/resources/_components/UpgradeCountdown.tsx b/app/[locale]/resources/_components/UpgradeCountdown.tsx
new file mode 100644
index 00000000000..a9b478cccc5
--- /dev/null
+++ b/app/[locale]/resources/_components/UpgradeCountdown.tsx
@@ -0,0 +1,117 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import humanizeDuration from "humanize-duration"
+import { useLocale } from "next-intl"
+
+import type { NetworkUpgradeDetails } from "@/lib/types"
+
+import { BaseLink } from "@/components/ui/Link"
+
+import networkUpgradeSummaryData from "@/data/networkUpgradeSummaryData"
+
+const getLatestNetworkUpgradeDate = () => {
+ const entries = Object.entries(networkUpgradeSummaryData) as [
+ string,
+ NetworkUpgradeDetails,
+ ][]
+
+ const result = entries.reduce<[string | null, string | null]>(
+ (acc, [network, details]) => {
+ // include pending entries as long as they have a valid date string
+ if (typeof details.dateTimeAsString !== "string") return acc
+
+ const candidateTime = Date.parse(details.dateTimeAsString)
+ if (isNaN(candidateTime)) return acc
+
+ const [, accDate] = acc
+ if (!accDate) return [network, details.dateTimeAsString]
+
+ const accTime = Date.parse(accDate)
+ if (isNaN(accTime) || candidateTime > accTime) {
+ return [network, details.dateTimeAsString]
+ }
+
+ return acc
+ },
+ [null, null]
+ )
+
+ return result
+}
+
+const UpgradeCountdown = () => {
+ const locale = useLocale()
+ const [scalingUpgradeCountdown, setUpgradeCountdown] = useState<
+ string | null
+ >("Loading...")
+ const [upgrade, upgradeDate] = getLatestNetworkUpgradeDate()
+
+ useEffect(() => {
+ // Countdown time for Scaling Upgrade to the final date of May 7 2025
+ // const scalingUpgradeDate = new Date("2025-05-07T00:00:00Z")
+
+ const scalingUpgradeDate = new Date(upgradeDate || "2025-05-07T00:00:00Z")
+ const scalingUpgradeDateTime = scalingUpgradeDate.getTime()
+ const SECONDS = 1000
+
+ const countdown = () => {
+ const now = Date.now()
+ const timeLeft = scalingUpgradeDateTime - now
+
+ // If the date has past, set the countdown to null
+ if (timeLeft < 0) return setUpgradeCountdown(null)
+
+ const baseOptions = {
+ units: ["d", "h", "m", "s"],
+ round: true,
+ }
+
+ try {
+ setUpgradeCountdown(
+ humanizeDuration(timeLeft, {
+ ...baseOptions,
+ language: locale,
+ })
+ )
+ } catch {
+ setUpgradeCountdown(humanizeDuration(timeLeft, baseOptions))
+ }
+ }
+ countdown()
+
+ let interval: NodeJS.Timeout | undefined
+
+ if (scalingUpgradeCountdown !== null) {
+ // Only run the interval if the date has not passed
+ interval = setInterval(countdown, SECONDS)
+ }
+
+ return () => clearInterval(interval)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ if (!upgrade || !upgradeDate) return "—"
+ return (
+ <>
+
+ {upgrade.slice(0, 1).toUpperCase() + upgrade.slice(1)}
+
+
+ {scalingUpgradeCountdown ? (
+ scalingUpgradeCountdown
+ ) : (
+
+ Live Since{" "}
+ {new Intl.DateTimeFormat(locale, {}).format(new Date(upgradeDate))}
+
+ )}
+
+ >
+ )
+}
+
+export default UpgradeCountdown
diff --git a/app/[locale]/resources/page.tsx b/app/[locale]/resources/page.tsx
index 2799aac7661..8e813db2d94 100644
--- a/app/[locale]/resources/page.tsx
+++ b/app/[locale]/resources/page.tsx
@@ -26,6 +26,7 @@ import { ResourceItem, ResourcesContainer } from "./_components/ResourcesUI"
import ResourcesPageJsonLD from "./page-jsonld"
import { getResources } from "./utils"
+import { fetchBlobscanStats } from "@/lib/api/fetchBlobscanStats"
import { fetchGrowThePie } from "@/lib/api/fetchGrowThePie"
import heroImg from "@/public/images/heroes/guides-hub-hero.jpg"
@@ -34,7 +35,10 @@ const REVALIDATE_TIME = BASE_TIME_UNIT * 1
const EVENT_CATEGORY = "dashboard"
const loadData = dataLoader(
- [["growThePieData", fetchGrowThePie]],
+ [
+ ["growThePieData", fetchGrowThePie],
+ ["blobscanOverallStats", fetchBlobscanStats],
+ ],
REVALIDATE_TIME * 1000
)
@@ -44,10 +48,28 @@ const Page = async ({ params }: { params: PageParams }) => {
const t = await getTranslations({ locale, namespace: "page-resources" })
// Load data
- const [growThePieData] = await loadData()
+ const [growThePieData, blobscanOverallStats] = await loadData()
+
const { txCostsMedianUsd } = growThePieData
- const resourceSections = await getResources({ txCostsMedianUsd })
+ const blobStats =
+ "error" in blobscanOverallStats
+ ? {
+ avgBlobFee: "—",
+ totalBlobs: "—",
+ }
+ : {
+ avgBlobFee: blobscanOverallStats.value.avgBlobFee,
+ totalBlobs: new Intl.NumberFormat(undefined, {
+ notation: "compact",
+ maximumFractionDigits: 1,
+ }).format(blobscanOverallStats.value.totalBlobs),
+ }
+
+ const resourceSections = await getResources({
+ txCostsMedianUsd,
+ ...blobStats,
+ })
const commitHistoryCache: CommitHistory = {}
const { contributors } = await getAppPageContributorInfo(
diff --git a/app/[locale]/resources/utils.tsx b/app/[locale]/resources/utils.tsx
index 74381e6bf2a..dc1d85aec21 100644
--- a/app/[locale]/resources/utils.tsx
+++ b/app/[locale]/resources/utils.tsx
@@ -3,6 +3,7 @@ import { getLocale, getTranslations } from "next-intl/server"
import { Lang } from "@/lib/types"
+import BigNumber from "@/components/BigNumber"
import SectionIconArrowsFullscreen from "@/components/icons/arrows-fullscreen.svg"
import SectionIconEthGlyph from "@/components/icons/eth-glyph.svg"
import SectionIconEthWallet from "@/components/icons/eth-wallet.svg"
@@ -13,10 +14,9 @@ import { Spinner } from "@/components/ui/spinner"
import { formatSmallUSD } from "@/lib/utils/numbers"
import { getLocaleForNumberFormat } from "@/lib/utils/translations"
-import BigNumber from "../../../src/components/BigNumber"
-
import type { DashboardBox, DashboardSection } from "./types"
+import { fetchEthPrice } from "@/lib/api/fetchEthPrice"
import IconBeaconchain from "@/public/images/resources/beaconcha-in.png"
import IconBlobsGuru from "@/public/images/resources/blobsguru.png"
import IconBlocknative from "@/public/images/resources/blocknative.png"
@@ -65,13 +65,42 @@ const SlotCountdownChart = dynamic(
),
}
)
+
+const UpgradeCountdownFigure = dynamic(
+ () => import("./_components/UpgradeCountdown"),
+ {
+ ssr: false,
+ loading: () => (
+
+
+
+ ),
+ }
+)
+
export const getResources = async ({
txCostsMedianUsd,
+ totalBlobs,
+ avgBlobFee,
}): Promise => {
const locale = await getLocale()
const t = await getTranslations({ locale, namespace: "page-resources" })
const localeForNumberFormat = getLocaleForNumberFormat(locale as Lang)
+ const ethPrice = await fetchEthPrice()
+
+ const avgBlobFeeUsd =
+ "error" in ethPrice
+ ? { error: ethPrice.error }
+ : {
+ ...ethPrice,
+ value: formatSmallUSD(
+ // Converting value from wei to USD
+ avgBlobFee * 1e-18 * ethPrice.value,
+ localeForNumberFormat
+ ),
+ }
+
const medianTxCost =
"error" in txCostsMedianUsd
? { error: txCostsMedianUsd.error }
@@ -351,7 +380,14 @@ export const getResources = async ({
const scalingBoxes: DashboardBox[] = [
{
title: t("page-resources-roadmap-title"),
- // TODO: Add metric
+ metric: (
+
+
+ {t("page-resources-roadmap-metric-label")}
+
+
+
+ ),
items: [
{
title: "Ethereum Roadmap",
@@ -363,7 +399,19 @@ export const getResources = async ({
},
{
title: t("page-resources-blobs-title"),
- // TODO: Add metric
+ metric: (
+
+
+ {t("page-resources-blobs-metric-total-label")}
+
+
+ {t("page-resources-blobs-metric-fee-label")}
+
+
+ ),
items: [
{
title: "Blob Scan",
diff --git a/package.json b/package.json
index bdca301ef88..ec8769d4605 100644
--- a/package.json
+++ b/package.json
@@ -75,6 +75,7 @@
"gray-matter": "^4.0.3",
"howler": "^2.2.4",
"htmr": "^1.0.2",
+ "humanize-duration": "^3.33.1",
"lodash": "^4.17.21",
"lucide-react": "^0.516.0",
"next": "^14.2.32",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ff917f1717c..e5cfe3f09a4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -143,6 +143,9 @@ importers:
htmr:
specifier: ^1.0.2
version: 1.0.2(react@18.3.1)
+ humanize-duration:
+ specifier: ^3.33.1
+ version: 3.33.1
lodash:
specifier: ^4.17.21
version: 4.17.21
@@ -6246,6 +6249,9 @@ packages:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
+ humanize-duration@3.33.1:
+ resolution: {integrity: sha512-hwzSCymnRdFx9YdRkQQ0OYequXiVAV6ZGQA2uzocwB0F4309Ke6pO8dg0P8LHhRQJyVjGteRTAA/zNfEcpXn8A==}
+
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
@@ -17210,6 +17216,8 @@ snapshots:
human-signals@5.0.0: {}
+ humanize-duration@3.33.1: {}
+
husky@9.1.7: {}
icss-utils@5.1.0(postcss@8.5.4):
diff --git a/src/components/History/NetworkUpgradeSummary.tsx b/src/components/History/NetworkUpgradeSummary.tsx
index 16831fb8fe9..7301274af2a 100644
--- a/src/components/History/NetworkUpgradeSummary.tsx
+++ b/src/components/History/NetworkUpgradeSummary.tsx
@@ -9,7 +9,8 @@ import { Flex, Stack } from "@/components/ui/flex"
import { getLocaleForNumberFormat } from "@/lib/utils/translations"
-import NetworkUpgradeSummaryData from "../../data/NetworkUpgradeSummaryData"
+import networkUpgradeSummaryData from "@/data/networkUpgradeSummaryData"
+
import Emoji from "../Emoji"
import InlineLink from "../ui/Link"
@@ -32,7 +33,7 @@ const NetworkUpgradeSummary = ({ name }: NetworkUpgradeSummaryProps) => {
blockNumber,
epochNumber,
slotNumber,
- } = NetworkUpgradeSummaryData[name]
+ } = networkUpgradeSummaryData[name]
// TODO fix dateTimeAsString
// calculate date format only on the client side to avoid hydration issues
diff --git a/src/data/mocks/blobscanOverallStats.json b/src/data/mocks/blobscanOverallStats.json
new file mode 100644
index 00000000000..9eb9561c4c0
--- /dev/null
+++ b/src/data/mocks/blobscanOverallStats.json
@@ -0,0 +1,18 @@
+{
+ "avgBlobAsCalldataFee": 18402670294113620,
+ "avgBlobFee": 1337454615991715,
+ "avgBlobGasPrice": 4657716809.805255,
+ "avgMaxBlobGasFee": 19666167416.48503,
+ "totalBlobGasUsed": "875492278272",
+ "totalBlobAsCalldataGasUsed": "12165759474144",
+ "totalBlobFee": "4174952855822794358784",
+ "totalBlobAsCalldataFee": "57445149899315095107588",
+ "totalBlobs": 6679476,
+ "totalBlobSize": "875492278272",
+ "totalBlocks": 1664933,
+ "totalTransactions": 3121566,
+ "totalUniqueBlobs": 6575105,
+ "totalUniqueReceivers": 5361,
+ "totalUniqueSenders": 5941,
+ "updatedAt": "2025-03-25T11:45:00.590Z"
+}
diff --git a/src/data/NetworkUpgradeSummaryData.ts b/src/data/networkUpgradeSummaryData.ts
similarity index 98%
rename from src/data/NetworkUpgradeSummaryData.ts
rename to src/data/networkUpgradeSummaryData.ts
index ac1ddf6327d..8325faf617b 100644
--- a/src/data/NetworkUpgradeSummaryData.ts
+++ b/src/data/networkUpgradeSummaryData.ts
@@ -1,6 +1,6 @@
import type { NetworkUpgradeData } from "@/lib/types"
-const NetworkUpgradeSummaryData: NetworkUpgradeData = {
+const networkUpgradeSummaryData: NetworkUpgradeData = {
pectra: {
dateTimeAsString: "2025-05-07T10:05:11.000Z",
ethPriceInUSD: 2222,
@@ -228,4 +228,4 @@ const NetworkUpgradeSummaryData: NetworkUpgradeData = {
},
}
-export default NetworkUpgradeSummaryData
+export default networkUpgradeSummaryData
diff --git a/src/intl/en/page-resources.json b/src/intl/en/page-resources.json
index cdb5906d224..9858c99e19a 100644
--- a/src/intl/en/page-resources.json
+++ b/src/intl/en/page-resources.json
@@ -44,8 +44,11 @@
"page-resources-adoption-cryptowerk-description": "Ethereum adoption analytics based on Cryptwerk merchants database - map, countries, companies, businesses, categories, rating.",
"page-resources-adoption-reserves-description": "A dashboard for the Strategic Ethereum Reserve initiative.",
"page-resources-roadmap-title": "Ethereum Roadmap",
+ "page-resources-roadmap-metric-label": "Latest upgrade",
"page-resources-roadmap-ethroadmap-description": "Detailed visualization on Ethereum roadmap and the next network upgrade.",
"page-resources-blobs-title": "Blobs",
+ "page-resources-blobs-metric-total-label": "Total blobs",
+ "page-resources-blobs-metric-fee-label": "Average Blob Fee",
"page-resources-blobs-blobscan-description": "Comprehensive blob scanner.",
"page-resources-blobs-blobsguru-description": "Ethereum Blobs Explorer: Analyze L2 transactions & EIP-4844 data.",
"page-resources-nodes-title": "Nodes",
diff --git a/src/lib/api/fetchBlobscanStats.ts b/src/lib/api/fetchBlobscanStats.ts
new file mode 100644
index 00000000000..1886534e7b7
--- /dev/null
+++ b/src/lib/api/fetchBlobscanStats.ts
@@ -0,0 +1,38 @@
+import type { ValueOrError } from "../types"
+
+type BlobscanOverallStats = {
+ avgBlobAsCalldataFee: number
+ avgBlobFee: number
+ avgBlobGasPrice: number
+ avgMaxBlobGasFee: number
+ totalBlobGasUsed: string
+ totalBlobAsCalldataGasUsed: string
+ totalBlobFee: string
+ totalBlobAsCalldataFee: string
+ totalBlobs: number
+ totalBlobSize: string
+ totalBlocks: number
+ totalTransactions: number
+ totalUniqueBlobs: number
+ totalUniqueReceivers: number
+ totalUniqueSenders: number
+ updatedAt: string
+}
+
+/**
+ * Fetch the overall stats from Blobscan
+ *
+ * @see https://api.blobscan.com/#/stats/stats-getOverallStats
+ *
+ */
+export const fetchBlobscanStats = async (): Promise<
+ ValueOrError
+> => {
+ const response = await fetch("https://api.blobscan.com/stats/overall")
+
+ if (!response.ok) return { error: "Response for fetchBlobscanStats not okay" }
+
+ const [json]: [BlobscanOverallStats] = await response.json()
+
+ return { value: json, timestamp: Date.now() }
+}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 475c887ca31..df5de23af5c 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -862,7 +862,7 @@ export type FeedbackWidgetContextType = {
}
// Historical upgrades
-type NetworkUpgradeDetails = {
+export type NetworkUpgradeDetails = {
blockNumber?: number
epochNumber?: number
slotNumber?: number