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 +}