diff --git a/.changeset/sweet-beans-speak.md b/.changeset/sweet-beans-speak.md new file mode 100644 index 000000000..ca095446e --- /dev/null +++ b/.changeset/sweet-beans-speak.md @@ -0,0 +1,5 @@ +--- +"@blobscan/web": minor +--- + +Get environment variables from API endpoint. diff --git a/apps/web/sentry.client.config.ts b/apps/web/sentry.client.config.ts index d268d406c..74a300120 100644 --- a/apps/web/sentry.client.config.ts +++ b/apps/web/sentry.client.config.ts @@ -4,11 +4,23 @@ import * as Sentry from "@sentry/nextjs"; -import { env } from "./src/env.mjs"; - -Sentry.init({ - dsn: env.NEXT_PUBLIC_SENTRY_DSN_WEB, - environment: env.NEXT_PUBLIC_NETWORK_NAME, - tracesSampleRate: 1, - debug: false, -}); +const initSentry = async () => { + try { + const request = await fetch("/api/env"); + const env = await request.json(); + + const dns = env["PUBLIC_SENTRY_DSN_WEB"]; + const environment = env["PUBLIC_NETWORK_NAME"]; + + Sentry.init({ + dsn: dns, + environment, + tracesSampleRate: 1, + debug: false, + }); + } catch (error) { + console.error("Error during Sentry initialization", error); + } +}; + +initSentry(); diff --git a/apps/web/src/components/BlobscanVersionInfo.tsx b/apps/web/src/components/BlobscanVersionInfo.tsx index ab9a63efd..a212e1c5c 100644 --- a/apps/web/src/components/BlobscanVersionInfo.tsx +++ b/apps/web/src/components/BlobscanVersionInfo.tsx @@ -1,29 +1,21 @@ -import { env } from "~/env.mjs"; +import { useEnv } from "~/providers/Env"; import { Link } from "./Link"; -function getVersionData(): { url: string; label: string } { - if (env.NEXT_PUBLIC_BLOBSCAN_RELEASE) { - return { - url: `https://github.com/Blobscan/blobscan/releases/tag/${env.NEXT_PUBLIC_BLOBSCAN_RELEASE}`, - label: env.NEXT_PUBLIC_BLOBSCAN_RELEASE, - }; - } +export const BlobscanVersionInfo: React.FC = () => { + const { env } = useEnv(); - if (env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA) { - return { - url: `https://github.com/Blobscan/blobscan/commit/${env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA}`, - label: env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA.slice(0, 7), - }; - } + let url = "https://github.com/Blobscan/blobscan/"; + let label = "Development"; - return { - url: "https://github.com/Blobscan/blobscan/", - label: "Development", - }; -} + if (env && env.PUBLIC_BLOBSCAN_RELEASE) { + url = `https://github.com/Blobscan/blobscan/releases/tag/${env.PUBLIC_BLOBSCAN_RELEASE}`; + label = env.PUBLIC_BLOBSCAN_RELEASE; + } -export const BlobscanVersionInfo: React.FC = () => { - const { url, label } = getVersionData(); + if (env && env.PUBLIC_VERCEL_GIT_COMMIT_SHA) { + url = `https://github.com/Blobscan/blobscan/commit/${env.PUBLIC_VERCEL_GIT_COMMIT_SHA}`; + label = env.PUBLIC_VERCEL_GIT_COMMIT_SHA.slice(0, 7); + } return (
diff --git a/apps/web/src/components/ExplorerDetails.tsx b/apps/web/src/components/ExplorerDetails.tsx index 3c0cd051c..0cb2dde12 100644 --- a/apps/web/src/components/ExplorerDetails.tsx +++ b/apps/web/src/components/ExplorerDetails.tsx @@ -7,8 +7,8 @@ import Skeleton from "react-loading-skeleton"; import { formatTtl } from "@blobscan/dates"; import { api } from "~/api-client"; -import { env } from "~/env.mjs"; import Gas from "~/icons/gas.svg"; +import { useEnv } from "~/providers/Env"; import { capitalize, formatNumber } from "~/utils"; import { EtherUnitDisplay } from "./Displays/EtherUnitDisplay"; @@ -45,11 +45,20 @@ export function ExplorerDetails({ placement }: ExplorerDetailsProps) { const { data: blobStoragesState } = api.blobStoragesState.getState.useQuery(); const { data: latestBlock } = api.block.getLatestBlock.useQuery(); + const { env } = useEnv(); + const explorerDetailsItems: ExplorerDetailsItemProps[] = []; if (placement === "top") { explorerDetailsItems.push( - { name: "Network", value: capitalize(env.NEXT_PUBLIC_NETWORK_NAME) }, + { + name: "Network", + value: env ? ( + capitalize(env.PUBLIC_NETWORK_NAME) + ) : ( + + ), + }, { name: "Blob gas price", icon: , diff --git a/apps/web/src/components/Filters/RollupFilter.tsx b/apps/web/src/components/Filters/RollupFilter.tsx index f22657a13..38e67c30c 100644 --- a/apps/web/src/components/Filters/RollupFilter.tsx +++ b/apps/web/src/components/Filters/RollupFilter.tsx @@ -1,44 +1,22 @@ import { useRef } from "react"; import type { FC } from "react"; -import { getChainRollups } from "@blobscan/rollups"; - import { Dropdown } from "~/components/Dropdown"; import type { DropdownProps, Option } from "~/components/Dropdown"; -import { RollupIcon } from "~/components/RollupIcon"; -import { env } from "~/env.mjs"; -import type { Rollup } from "~/types"; -import { capitalize, getChainIdByName } from "~/utils"; -import { RollupBadge } from "../Badges/RollupBadge"; -type RollupFilterProps = Pick & { +type RollupFilterProps = Pick< + DropdownProps, + "selected" | "disabled" | "options" +> & { onChange(newRollups: Option[]): void; selected: Option[] | null; }; -const chainId = getChainIdByName(env.NEXT_PUBLIC_NETWORK_NAME); -const rollups = chainId ? getChainRollups(chainId) : []; - -export const ROLLUP_OPTIONS = rollups.map( - ([name, addresses]) => - ({ - value: addresses, - selectedLabel: ( - - ), - label: ( -
- -
{capitalize(name)}
-
- ), - } satisfies Option) -) satisfies Option[]; - export const RollupFilter: FC = function ({ onChange, selected, disabled, + options, }) { const noneIsSelected = useRef(false); @@ -66,7 +44,7 @@ export const RollupFilter: FC = function ({ return ( { + const chainId = env && getChainIdByName(env.PUBLIC_NETWORK_NAME); + const rollups = chainId ? getChainRollups(chainId) : []; + + return rollups.map( + ([name, addresses]) => + ({ + value: addresses, + selectedLabel: ( + + ), + label: ( +
+ +
{capitalize(name)}
+
+ ), + } satisfies Option) + ); + }, [env]); + useEffect(() => { const { sort } = queryParams.paginationParams; const { @@ -177,7 +205,7 @@ export const Filters: FC = function () { const newFilters: Partial = {}; if (from) { - const rollupOptions = ROLLUP_OPTIONS.filter((opt) => { + const rollupOptions_ = rollupOptions.filter((opt) => { const fromAddresses = from?.split(FROM_ADDRESSES_FORMAT_SEPARATOR); const rollupOptionAddresses = Array.isArray(opt.value) ? opt.value @@ -185,13 +213,13 @@ export const Filters: FC = function () { return ( rollupOptionAddresses.filter((rollupAddress) => - fromAddresses?.includes(rollupAddress) + fromAddresses?.includes(rollupAddress as string) ).length > 0 ); }); - if (rollupOptions) { - newFilters.rollups = rollupOptions; + if (rollupOptions_) { + newFilters.rollups = rollupOptions_; } } @@ -225,7 +253,7 @@ export const Filters: FC = function () { } dispatch({ type: "UPDATE", payload: newFilters }); - }, [queryParams]); + }, [queryParams, rollupOptions]); return ( @@ -265,6 +293,7 @@ export const Filters: FC = function () {
{ + const { env } = useEnv(); + + const navigationItems = useMemo(() => { + const networkName = env ? env.PUBLIC_NETWORK_NAME : undefined; + const publicSupportedNetworks = env + ? env.PUBLIC_SUPPORTED_NETWORKS + : undefined; + + return networkName && publicSupportedNetworks + ? getNavigationItems(networkName, publicSupportedNetworks) + : undefined; + }, [env]); + + if (!navigationItems) { + return ; + } + return (
- {NAVIGATION_ITEMS.map((item) => + {navigationItems.map((item) => isExpandibleNavigationItem(item) ? ( ) : ( diff --git a/apps/web/src/components/SidebarNavigationMenu.tsx b/apps/web/src/components/SidebarNavigationMenu.tsx index cdf24e0d3..8b38574f1 100644 --- a/apps/web/src/components/SidebarNavigationMenu.tsx +++ b/apps/web/src/components/SidebarNavigationMenu.tsx @@ -1,10 +1,12 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { Bars3Icon } from "@heroicons/react/24/solid"; import cn from "classnames"; +import Skeleton from "react-loading-skeleton"; +import { useEnv } from "~/providers/Env"; import { BlobscanLogo } from "./BlobscanLogo"; import { Collapsable } from "./Collapsable"; import { IconButton } from "./IconButton"; @@ -12,7 +14,7 @@ import { Rotable } from "./Rotable"; import { SidePanel, useSidePanel } from "./SidePanel"; import { ThemeModeButton } from "./ThemeModeButton"; import type { ExpandibleNavigationItem, NavigationItem } from "./content"; -import { isExpandibleNavigationItem, NAVIGATION_ITEMS } from "./content"; +import { isExpandibleNavigationItem, getNavigationItems } from "./content"; export function SidebarNavigationMenu({ className }: { className?: string }) { const [open, setOpen] = useState(false); @@ -21,6 +23,18 @@ export function SidebarNavigationMenu({ className }: { className?: string }) { const closeSidebar = useCallback(() => setOpen(false), []); + const { env } = useEnv(); + + const navigationItems = useMemo(() => { + const networkName = env ? env.PUBLIC_NETWORK_NAME : undefined; + const publicSupportedNetworks = env + ? env.PUBLIC_SUPPORTED_NETWORKS + : undefined; + return networkName && publicSupportedNetworks + ? getNavigationItems(networkName, publicSupportedNetworks) + : undefined; + }, [env]); + return (
@@ -30,20 +44,24 @@ export function SidebarNavigationMenu({ className }: { className?: string }) {
- {NAVIGATION_ITEMS.map((item, i) => - isExpandibleNavigationItem(item) ? ( - - ) : ( - + {navigationItems ? ( + navigationItems.map((item, i) => + isExpandibleNavigationItem(item) ? ( + + ) : ( + + ) ) + ) : ( + )}
diff --git a/apps/web/src/components/content.tsx b/apps/web/src/components/content.tsx index e7de44735..10333e27e 100644 --- a/apps/web/src/components/content.tsx +++ b/apps/web/src/components/content.tsx @@ -6,7 +6,6 @@ import { Squares2X2Icon, } from "@heroicons/react/24/solid"; -import { env } from "~/env.mjs"; import EthereumIcon from "~/icons/ethereum.svg"; import { buildBlocksRoute, @@ -15,17 +14,15 @@ import { buildAllStatsRoute, } from "~/utils"; -function resolveApiUrl(): string { - if (env.NEXT_PUBLIC_NETWORK_NAME === "mainnet") { +function resolveApiUrl(networkName: string): string { + if (networkName === "mainnet") { return "https://api.blobscan.com"; } - return `https://api.${env.NEXT_PUBLIC_NETWORK_NAME}.blobscan.com`; + return `https://api.${networkName}.blobscan.com`; } -type Network = typeof env.NEXT_PUBLIC_NETWORK_NAME; - -const NETWORKS_FIRST_BLOB_NUMBER: Record = { +const NETWORKS_FIRST_BLOB_NUMBER: Record = { mainnet: 19426589, holesky: 894735, sepolia: 5187052, @@ -34,8 +31,8 @@ const NETWORKS_FIRST_BLOB_NUMBER: Record = { devnet: 0, }; -export function getFirstBlobNumber(): number { - return NETWORKS_FIRST_BLOB_NUMBER[env.NEXT_PUBLIC_NETWORK_NAME]; +export function getFirstBlobNumber(networkName: string): number | undefined { + return NETWORKS_FIRST_BLOB_NUMBER[networkName]; } export type NavigationItem = { @@ -61,45 +58,48 @@ export function isExpandibleNavigationItem( return typeof item === "object" && item !== null && "items" in item; } -export const NAVIGATION_ITEMS: Array< - NavigationItem | ExpandibleNavigationItem -> = [ - { - label: "Blockchain", - icon: , - items: [ - { - label: "Blobs", - href: buildBlobsRoute(), - }, - { - label: "Blocks", - href: buildBlocksRoute(), - }, - { - label: "Transactions", - href: buildTransactionsRoute(), - }, - ], - }, - { - label: "Networks", - icon: , - items: JSON.parse(env.NEXT_PUBLIC_SUPPORTED_NETWORKS || "[]"), - }, - { - label: "Stats", - icon: , - href: buildAllStatsRoute(), - }, - { - label: "API", - icon: , - href: resolveApiUrl(), - }, - { - label: "Docs", - icon: , - href: "https://docs.blobscan.com", - }, -]; +export const getNavigationItems = ( + networkName: string, + publicSupportedNetworks: string +): Array => { + return [ + { + label: "Blockchain", + icon: , + items: [ + { + label: "Blobs", + href: buildBlobsRoute(), + }, + { + label: "Blocks", + href: buildBlocksRoute(), + }, + { + label: "Transactions", + href: buildTransactionsRoute(), + }, + ], + }, + { + label: "Networks", + icon: , + items: JSON.parse(publicSupportedNetworks || "[]"), + }, + { + label: "Stats", + icon: , + href: buildAllStatsRoute(), + }, + { + label: "API", + icon: , + href: resolveApiUrl(networkName), + }, + { + label: "Docs", + icon: , + href: "https://docs.blobscan.com", + }, + ]; +}; diff --git a/apps/web/src/env.mjs b/apps/web/src/env.mjs index 0e6d34f9a..61f10ee28 100644 --- a/apps/web/src/env.mjs +++ b/apps/web/src/env.mjs @@ -1,13 +1,6 @@ import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; -// See booleanSchema from packages/zod/src/schemas.ts -// We need to redefine it because we can't import ts files from here -const booleanSchema = z - .string() - .refine((s) => s === "true" || s === "false") - .transform((s) => s === "true"); - const networkSchema = z.enum([ "mainnet", "holesky", @@ -17,6 +10,31 @@ const networkSchema = z.enum([ "devnet", ]); +const clientEnvVars = { + NEXT_PUBLIC_BEACON_BASE_URL: z + .string() + .url() + .default("https://beaconcha.in/"), + NEXT_PUBLIC_BLOBSCAN_RELEASE: z.string().optional(), + NEXT_PUBLIC_EXPLORER_BASE_URL: z + .string() + .url() + .default("https://etherscan.io/"), + NEXT_PUBLIC_NETWORK_NAME: networkSchema.default("mainnet"), + NEXT_PUBLIC_SENTRY_DSN_WEB: z.string().url().optional(), + NEXT_PUBLIC_POSTHOG_ID: z.string().optional(), + NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), + NEXT_PUBLIC_SUPPORTED_NETWORKS: z + .string() + .default( + '[{"label":"Ethereum Mainnet","href":"https://blobscan.com/"},{"label":"Gnosis","href":"https://gnosis.blobscan.com/"},{"label":"Holesky Testnet","href":"https://holesky.blobscan.com/"},{"label":"Sepolia Testnet","href":"https://sepolia.blobscan.com/"}]' + ), + NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: z.boolean().default(false), + NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA: z.string().optional(), +}; + +export const clientEnvVarsSchema = z.object(clientEnvVars); + export const env = createEnv({ /** * Specify your server-side environment variables schema here. This way you can ensure the app isn't @@ -26,34 +44,11 @@ export const env = createEnv({ DATABASE_URL: z.string().url(), FEEDBACK_WEBHOOK_URL: z.string().optional(), NODE_ENV: z.enum(["development", "test", "production"]), - METRICS_ENABLED: booleanSchema.default("false"), - TRACES_ENABLED: booleanSchema.default("false"), + METRICS_ENABLED: z.boolean().default(false), + TRACES_ENABLED: z.boolean().default(false), }, - /** - * Specify your client-side environment variables schema here. - * For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`. - */ client: { - NEXT_PUBLIC_BEACON_BASE_URL: z - .string() - .url() - .default("https://beaconcha.in/"), - NEXT_PUBLIC_BLOBSCAN_RELEASE: z.string().optional(), - NEXT_PUBLIC_EXPLORER_BASE_URL: z - .string() - .url() - .default("https://etherscan.io/"), - NEXT_PUBLIC_NETWORK_NAME: networkSchema.default("mainnet"), - NEXT_PUBLIC_SENTRY_DSN_WEB: z.string().url().optional(), - NEXT_PUBLIC_POSTHOG_ID: z.string().optional(), - NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), - NEXT_PUBLIC_SUPPORTED_NETWORKS: z - .string() - .default( - '[{"label":"Ethereum Mainnet","href":"https://blobscan.com/"},{"label":"Gnosis","href":"https://gnosis.blobscan.com/"},{"label":"Holesky Testnet","href":"https://holesky.blobscan.com/"},{"label":"Sepolia Testnet","href":"https://sepolia.blobscan.com/"}]' - ), - NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED: booleanSchema.default("false"), - NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA: z.string().optional(), + ...clientEnvVars, }, /** * Destructure all variables from `process.env` to make sure they aren't tree-shaken away. diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index 17347bd03..58e422f87 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -19,26 +19,33 @@ import { SkeletonTheme } from "react-loading-skeleton"; import AppLayout from "~/components/AppLayout/AppLayout"; import { FeedbackWidget } from "~/components/FeedbackWidget/FeedbackWidget"; import { api } from "~/api-client"; -import { env } from "~/env.mjs"; import { useIsMounted } from "~/hooks/useIsMounted"; import { BlobDecoderWorkerProvider } from "~/providers/BlobDecoderWorker"; - -if (typeof window !== "undefined" && env.NEXT_PUBLIC_POSTHOG_ID) { - posthog.init(env.NEXT_PUBLIC_POSTHOG_ID, { - api_host: env.NEXT_PUBLIC_POSTHOG_HOST, - person_profiles: "identified_only", - loaded: (posthog) => { - if (window.location.hostname.includes("localhost")) { - posthog.debug(); - } - }, - }); -} +import { EnvProvider, useEnv } from "~/providers/Env"; function App({ Component, pageProps }: NextAppProps) { const { resolvedTheme } = useTheme(); const isMounted = useIsMounted(); const router = useRouter(); + const { env } = useEnv(); + + useEffect(() => { + if ( + typeof window !== "undefined" && + env && + env.PUBLIC_POSTHOG_ID !== undefined + ) { + posthog.init(env.PUBLIC_POSTHOG_ID, { + api_host: env.PUBLIC_POSTHOG_HOST, + person_profiles: "identified_only", + loaded: (posthog) => { + if (window.location.hostname.includes("localhost")) { + posthog.debug(); + } + }, + }); + } + }, [env]); useEffect(() => { const handleRouteChange = () => { @@ -85,7 +92,7 @@ function App({ Component, pageProps }: NextAppProps) { - {env.NEXT_PUBLIC_VERCEL_ANALYTICS_ENABLED && } + {env && env.PUBLIC_VERCEL_ANALYTICS_ENABLED && } ); @@ -95,7 +102,9 @@ function AppWrapper(props: NextAppProps) { return ( - + + + ); diff --git a/apps/web/src/pages/api/env/index.ts b/apps/web/src/pages/api/env/index.ts new file mode 100644 index 000000000..1189625ce --- /dev/null +++ b/apps/web/src/pages/api/env/index.ts @@ -0,0 +1,33 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { env } from "@blobscan/env"; + +import { clientEnvVarsSchema } from "../../../env.mjs"; + +export default async function (req: NextApiRequest, res: NextApiResponse) { + const method = req.method; + + switch (method) { + case "GET": { + const parsedEnv = clientEnvVarsSchema.safeParse(env); + + if (!parsedEnv.success) { + return res.status(400).json({ + error: "Error parsing client side environment variables", + details: parsedEnv.error.format(), + }); + } + + const clientEnv = Object.fromEntries( + Object.entries(parsedEnv.data) + .filter(([key]) => key.startsWith("NEXT_")) + .map(([key, value]) => [key.replace(/^NEXT_/, ""), value]) + ); + + return res.status(200).json(clientEnv); + } + + default: + return res.status(405).json({ error: "Method not allowed" }); + } +} diff --git a/apps/web/src/pages/block/[id].tsx b/apps/web/src/pages/block/[id].tsx index b963760d6..1e26ce42a 100644 --- a/apps/web/src/pages/block/[id].tsx +++ b/apps/web/src/pages/block/[id].tsx @@ -16,6 +16,7 @@ import { BlockStatus } from "~/components/Status"; import { getFirstBlobNumber } from "~/components/content"; import { api } from "~/api-client"; import NextError from "~/pages/_error"; +import { useEnv } from "~/providers/Env"; import type { BlockWithExpandedBlobsAndTransactions } from "~/types"; import { BLOB_GAS_LIMIT_PER_BLOCK, @@ -56,6 +57,154 @@ const Block: NextPage = function () { const { data: latestBlock } = api.block.getLatestBlock.useQuery(); const blockNumber = blockData ? blockData.number : undefined; + const { env } = useEnv(); + const networkName = env ? env.PUBLIC_NETWORK_NAME : undefined; + + const detailsFields: DetailsLayoutProps["fields"] | undefined = + useMemo(() => { + if (blockData) { + const totalBlockBlobSize = blockData?.transactions.reduce( + (acc, { blobs }) => { + const totalBlobsSize = blobs.reduce( + (blobAcc, { size }) => blobAcc + size, + 0 + ); + + return acc + totalBlobsSize; + }, + 0 + ); + + const firstBlobNumber = networkName + ? getFirstBlobNumber(networkName) + : undefined; + + const previousBlockHref = + firstBlobNumber && blockNumber && firstBlobNumber < blockNumber + ? `/block_neighbor?blockNumber=${blockNumber}&direction=prev` + : undefined; + + return [ + { + name: "Block Height", + helpText: + "Also referred to as the Block Number, the block height represents the length of the blockchain and increases with each newly added block.", + value: ( +
+ {blockData.number} + {blockNumber !== undefined && previousBlockHref && ( + + )} +
+ ), + }, + { + name: "Status", + helpText: "The finality status of the block.", + value: , + }, + { + name: "Hash", + helpText: "The hash of the block header.", + value: , + }, + { + name: "Timestamp", + helpText: "The time at which the block was created.", + value: ( +
+ {formatTimestamp(blockData.timestamp)} +
+ ), + }, + { + name: "Slot", + helpText: "The slot number of the block.", + value: ( + + {blockData.slot} + + ), + }, + { + name: "Blob size", + helpText: "Total amount of space used for blobs in this block.", + value: ( +
+ {formatBytes(totalBlockBlobSize)} + + ({formatNumber(totalBlockBlobSize / GAS_PER_BLOB)}{" "} + {pluralize("blob", totalBlockBlobSize / GAS_PER_BLOB)}) + +
+ ), + }, + { + name: "Blob Gas Price", + helpText: + "The cost per unit of blob gas used by the blobs in this block.", + value: , + }, + { + name: "Blob Gas Used", + helpText: `The total blob gas used by the blobs in this block, along with its percentage relative to both the total blob gas limit and the blob gas target (${( + TARGET_BLOB_GAS_PER_BLOCK / 1024 + ).toFixed(0)} KB).`, + value: , + }, + { + name: "Blob Gas Limit", + helpText: "The maximum blob gas limit for this block.", + value: ( +
+ {formatNumber(BLOB_GAS_LIMIT_PER_BLOCK)} + + ({formatNumber(MAX_BLOBS_PER_BLOCK)}{" "} + {pluralize("blob", MAX_BLOBS_PER_BLOCK)} per block) + +
+ ), + }, + { + name: "Blob As Calldata Gas", + helpText: + "The total gas that would have been used in this block if the blobs were sent as calldata.", + value: ( +
+ {formatNumber(blockData.blobAsCalldataGasUsed)} + + ( + + {formatNumber( + performDiv( + blockData.blobAsCalldataGasUsed, + blockData.blobGasUsed + ), + "standard", + { maximumFractionDigits: 2 } + )} + {" "} + times more expensive) + +
+ ), + }, + ]; + } + }, [blockData, networkName, latestBlock, blockNumber]); + if (error) { return ( Block not found
; } - let detailsFields: DetailsLayoutProps["fields"] | undefined; - - if (blockData) { - const totalBlockBlobSize = blockData?.transactions.reduce( - (acc, { blobs }) => { - const totalBlobsSize = blobs.reduce( - (blobAcc, { size }) => blobAcc + size, - 0 - ); - - return acc + totalBlobsSize; - }, - 0 - ); - - detailsFields = [ - { - name: "Block Height", - helpText: - "Also referred to as the Block Number, the block height represents the length of the blockchain and increases with each newly added block.", - value: ( -
- {blockData.number} - {blockNumber !== undefined && ( - - )} -
- ), - }, - { - name: "Status", - helpText: "The finality status of the block.", - value: , - }, - { - name: "Hash", - helpText: "The hash of the block header.", - value: , - }, - { - name: "Timestamp", - helpText: "The time at which the block was created.", - value: ( -
- {formatTimestamp(blockData.timestamp)} -
- ), - }, - { - name: "Slot", - helpText: "The slot number of the block.", - value: ( - - {blockData.slot} - - ), - }, - { - name: "Blob size", - helpText: "Total amount of space used for blobs in this block.", - value: ( -
- {formatBytes(totalBlockBlobSize)} - - ({formatNumber(totalBlockBlobSize / GAS_PER_BLOB)}{" "} - {pluralize("blob", totalBlockBlobSize / GAS_PER_BLOB)}) - -
- ), - }, - { - name: "Blob Gas Price", - helpText: - "The cost per unit of blob gas used by the blobs in this block.", - value: , - }, - { - name: "Blob Gas Used", - helpText: `The total blob gas used by the blobs in this block, along with its percentage relative to both the total blob gas limit and the blob gas target (${( - TARGET_BLOB_GAS_PER_BLOCK / 1024 - ).toFixed(0)} KB).`, - value: , - }, - { - name: "Blob Gas Limit", - helpText: "The maximum blob gas limit for this block.", - value: ( -
- {formatNumber(BLOB_GAS_LIMIT_PER_BLOCK)} - - ({formatNumber(MAX_BLOBS_PER_BLOCK)}{" "} - {pluralize("blob", MAX_BLOBS_PER_BLOCK)} per block) - -
- ), - }, - { - name: "Blob As Calldata Gas", - helpText: - "The total gas that would have been used in this block if the blobs were sent as calldata.", - value: ( -
- {formatNumber(blockData.blobAsCalldataGasUsed)} - - ( - - {formatNumber( - performDiv( - blockData.blobAsCalldataGasUsed, - blockData.blobGasUsed - ), - "standard", - { maximumFractionDigits: 2 } - )} - {" "} - times more expensive) - -
- ), - }, - ]; - } - return ( <> ; + +type ClientEnv = { + [Key in keyof ClientEnvVars as Key extends `NEXT_${infer Rest}` + ? Rest + : never]: ClientEnvVars[Key]; +}; + +interface EnvContextType { + env?: ClientEnv; +} + +const EnvContext = createContext({ env: undefined }); + +export const EnvProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [env, setEnv] = useState(); + + useEffect(() => { + const fetchEnv = async () => { + try { + const request = await fetch("/api/env"); + const data = await request.json(); + setEnv(data); + } catch (error) { + console.error( + "Error fetching environment variables from server side:", + error + ); + } + }; + + fetchEnv(); + }, []); + + return ( + {children} + ); +}; + +export const useEnv = () => { + const context = useContext(EnvContext); + if (!context) { + throw new Error("useEnv must be used within an EnvProvider"); + } + return context; +};