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