diff --git a/app/[locale]/resources/_components/resources.tsx b/app/[locale]/resources/_components/resources.tsx
index d8d2e50476d..cc4bf63424c 100644
--- a/app/[locale]/resources/_components/resources.tsx
+++ b/app/[locale]/resources/_components/resources.tsx
@@ -26,13 +26,24 @@ import { useTranslation } from "@/hooks/useTranslation"
import heroImg from "@/public/images/heroes/guides-hub-hero.jpg"
interface ResourcesPageProps {
txCostsMedianUsd: MetricReturnData
+ totalBlobs: string
+ avgBlobFee: number
}
const EVENT_CATEGORY = "dashboard"
-const ResourcesPage = ({ txCostsMedianUsd }: ResourcesPageProps) => {
+const ResourcesPage = ({
+ txCostsMedianUsd,
+ totalBlobs,
+ avgBlobFee,
+}: ResourcesPageProps) => {
const { t } = useTranslation("page-resources")
- const resourceSections = useResources({ txCostsMedianUsd })
+
+ const resourceSections = useResources({
+ txCostsMedianUsd,
+ totalBlobs,
+ avgBlobFee,
+ })
const activeSection = useActiveHash(
resourceSections.map(({ key }) => key),
"0% 0% -70% 0%"
@@ -114,7 +125,7 @@ const ResourcesPage = ({ txCostsMedianUsd }: ResourcesPageProps) => {
{boxes.map(({ title, metric, items, className }) => (
{
{title}
-
+
{metric && metric}
{items.map((item) => (
))}
-
+
))}
diff --git a/app/[locale]/resources/page.tsx b/app/[locale]/resources/page.tsx
index b8db41a4b41..e397ffee7f8 100644
--- a/app/[locale]/resources/page.tsx
+++ b/app/[locale]/resources/page.tsx
@@ -17,13 +17,17 @@ import { BASE_TIME_UNIT } from "@/lib/constants"
import ResourcesPage from "./_components/resources"
+import { fetchBlobscanStats } from "@/lib/api/fetchBlobscanStats"
import { fetchGrowThePie } from "@/lib/api/fetchGrowThePie"
// In seconds
const REVALIDATE_TIME = BASE_TIME_UNIT * 1
const loadData = dataLoader(
- [["growThePieData", fetchGrowThePie]],
+ [
+ ["growThePieData", fetchGrowThePie],
+ ["blobscanOverallStats", fetchBlobscanStats],
+ ],
REVALIDATE_TIME * 1000
)
@@ -38,12 +42,24 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
const messages = pick(allMessages, requiredNamespaces)
// Load data
- const [growThePieData] = await loadData()
+ const [growThePieData, blobscanOverallStats] = await loadData()
+
const { txCostsMedianUsd } = growThePieData
+ const { totalBlobs, avgBlobFee } = blobscanOverallStats
+
+ const formattedTotalBlobs = new Intl.NumberFormat(undefined, {
+ notation: "compact",
+ maximumFractionDigits: 1,
+ }).format(totalBlobs)
+
return (
-
+
)
}
diff --git a/src/components/Resources/useResources.tsx b/src/components/Resources/useResources.tsx
index 0ad4218477a..8f11b5a77d8 100644
--- a/src/components/Resources/useResources.tsx
+++ b/src/components/Resources/useResources.tsx
@@ -13,9 +13,11 @@ import { getLocaleForNumberFormat } from "@/lib/utils/translations"
import BigNumber from "../BigNumber"
import RadialChart from "../RadialChart"
+import { BaseLink } from "../ui/Link"
import type { DashboardBox, DashboardSection } from "./types"
+import { useEthPrice } from "@/hooks/useEthPrice"
import { useTranslation } from "@/hooks/useTranslation"
import IconBeaconchain from "@/public/images/resources/beaconcha-in.png"
import IconBlobsGuru from "@/public/images/resources/blobsguru.png"
@@ -54,16 +56,24 @@ const formatSmallUSD = (value: number, locale: string): string =>
new Intl.NumberFormat(locale, {
style: "currency",
currency: "USD",
- notation: "compact",
- minimumSignificantDigits: 2,
- maximumSignificantDigits: 2,
}).format(value)
-export const useResources = ({ txCostsMedianUsd }): DashboardSection[] => {
+export const useResources = ({
+ txCostsMedianUsd,
+ totalBlobs,
+ avgBlobFee,
+}): DashboardSection[] => {
const { t } = useTranslation("page-resources")
const locale = useLocale()
const localeForNumberFormat = getLocaleForNumberFormat(locale! as Lang)
+ const ethPrice = useEthPrice()
+ const avgBlobFeeUsd = formatSmallUSD(
+ // Converting value from wei to USD
+ avgBlobFee * 1e-18 * ethPrice,
+ localeForNumberFormat
+ )
+
const medianTxCost =
"error" in txCostsMedianUsd
? { error: txCostsMedianUsd.error }
@@ -74,6 +84,48 @@ export const useResources = ({ txCostsMedianUsd }): DashboardSection[] => {
const [timeToNextBlock, setTimeToNextBlock] = useState(12)
+ const [scalingUpgradeCountdown, setPectraCountdown] = useState(
+ "Loading..."
+ )
+
+ useEffect(() => {
+ // Countdown time for Scaling Upgrade to the final date of May 7 2025
+ const scalingUpgradeDate = new Date("2025-05-07T00:00:00Z")
+ const scalingUpgradeDateTime = scalingUpgradeDate.getTime()
+ const SECONDS = 1000
+ const MINUTES = SECONDS * 60
+ const HOURS = MINUTES * 60
+ const DAYS = HOURS * 24
+
+ const countdown = () => {
+ const now = Date.now()
+ const timeLeft = scalingUpgradeDateTime - now
+
+ // If the date has past, set the countdown to null
+ if (timeLeft < 0) return setPectraCountdown(null)
+
+ const daysLeft = Math.floor(timeLeft / DAYS)
+ const hoursLeft = Math.floor((timeLeft % DAYS) / HOURS)
+ const minutesLeft = Math.floor((timeLeft % HOURS) / MINUTES)
+ const secondsLeft = Math.floor((timeLeft % MINUTES) / SECONDS)
+
+ setPectraCountdown(
+ `${daysLeft}days :: ${hoursLeft}h ${minutesLeft}m ${secondsLeft}s`
+ )
+ }
+ 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
+ }, [])
+
useEffect(() => {
const genesisTime = new Date("2020-12-01T12:00:23Z").getTime()
const updateTime = () => {
@@ -350,7 +402,26 @@ export const useResources = ({ txCostsMedianUsd }): DashboardSection[] => {
const scalingBoxes: DashboardBox[] = [
{
title: t("page-resources-roadmap-title"),
- // TODO: Add metric
+ metric: (
+
+
Latest upgrade
+
+ Pectra
+
+
+ {scalingUpgradeCountdown ? (
+ scalingUpgradeCountdown
+ ) : (
+
+ Live Since April 2025
+
+ )}
+
+
+ ),
items: [
{
title: "Ethereum Roadmap",
@@ -362,7 +433,22 @@ export const useResources = ({ txCostsMedianUsd }): DashboardSection[] => {
},
{
title: t("page-resources-blobs-title"),
- // TODO: Add metric
+ metric: (
+
+
+
+ {totalBlobs}
+
+
Total blobs
+
+
+
+ {avgBlobFeeUsd}
+
+
Average Blob Fee
+
+
+ ),
items: [
{
title: "Blob Scan",
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/lib/api/fetchBlobscanStats.ts b/src/lib/api/fetchBlobscanStats.ts
new file mode 100644
index 00000000000..851ad925f02
--- /dev/null
+++ b/src/lib/api/fetchBlobscanStats.ts
@@ -0,0 +1,57 @@
+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
+}
+
+type BlobscanOverallStatsErr = {
+ message: string
+ code: string
+ issues: [message: string]
+}
+
+/**
+ * Fetch the overall stats from Blobscan
+ *
+ * @see https://api.blobscan.com/#/stats/stats-getOverallStats
+ *
+ */
+export const fetchBlobscanStats = async () => {
+ const data = await fetch("https://api.blobscan.com/stats/overall").then(
+ (res) => responseHandler(res)
+ )
+
+ return data
+}
+
+type BlobscanResponse =
+ | (Omit & {
+ json: () => BlobscanOverallStats | PromiseLike
+ })
+ | (Omit & {
+ json: () => BlobscanOverallStatsErr | PromiseLike
+ })
+
+const responseHandler = async (response: Response) => {
+ const res = await (response as BlobscanResponse).json()
+
+ if ("message" in res) {
+ throw Error(`Code ${res.code}: Failed to fetch Blobscan Overall Stats`, {
+ cause: res.message,
+ })
+ }
+ return res
+}