Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/[locale]/enterprise/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ import EnterprisePageJsonLD from "./page-jsonld"
import type { Case, EcosystemPlayer, Feature } from "./types"
import { parseActivity } from "./utils"

import { fetchBeaconchainEpoch } from "@/lib/api/fetchBeaconchainEpoch"
import { fetchEthereumStablecoinsMcap } from "@/lib/api/fetchEthereumStablecoinsMcap"
import { fetchEthPrice } from "@/lib/api/fetchEthPrice"
import { fetchEthStakedBeaconchain } from "@/lib/api/fetchEthStakedBeaconchain"
import { fetchGrowThePie } from "@/lib/api/fetchGrowThePie"
import EthGlyph from "@/public/images/assets/svgs/eth-diamond-rainbow.svg"
import heroImage from "@/public/images/heroes/enterprise-hero-white.png"
Expand Down Expand Up @@ -97,7 +97,7 @@ const loadData = dataLoader(
["growThePieData", fetchGrowThePie],
["ethereumStablecoins", fetchEthereumStablecoinsMcap],
["ethPrice", fetchEthPrice],
["totalEthStaked", fetchEthStakedBeaconchain],
["beaconchainEpoch", fetchBeaconchainEpoch],
],
BASE_TIME_UNIT * 1000
)
Expand All @@ -111,7 +111,7 @@ const Page = async ({ params }: { params: { locale: Lang } }) => {
{ txCount, txCostsMedianUsd },
stablecoinMarketCap,
ethPrice,
totalEthStaked,
{ totalEthStaked },
] = await loadData()

const metrics = await parseActivity({
Expand Down
6 changes: 3 additions & 3 deletions app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ import { getActivity, getUpcomingEvents } from "./utils"
import { routing } from "@/i18n/routing"
import { getABTestAssignment } from "@/lib/ab-testing/server"
import { fetchCommunityEvents } from "@/lib/api/calendarEvents"
import { fetchBeaconchainEpoch } from "@/lib/api/fetchBeaconchainEpoch"
import { fetchEthPrice } from "@/lib/api/fetchEthPrice"
import { fetchEthStakedBeaconchain } from "@/lib/api/fetchEthStakedBeaconchain"
import { fetchGrowThePie } from "@/lib/api/fetchGrowThePie"
import { fetchAttestantPosts } from "@/lib/api/fetchPosts"
import { fetchRSS } from "@/lib/api/fetchRSS"
Expand Down Expand Up @@ -142,7 +142,7 @@ const REVALIDATE_TIME = BASE_TIME_UNIT * 1
const loadData = dataLoader(
[
["ethPrice", fetchEthPrice],
["totalEthStaked", fetchEthStakedBeaconchain],
["beaconchainEpoch", fetchBeaconchainEpoch],
["totalValueLocked", fetchTotalValueLocked],
["growThePieData", fetchGrowThePie],
["communityEvents", fetchCommunityEvents],
Expand All @@ -168,7 +168,7 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {

const [
ethPrice,
totalEthStaked,
{ totalEthStaked },
totalValueLocked,
growThePieData,
communityEvents,
Expand Down
51 changes: 14 additions & 37 deletions app/[locale]/staking/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@ import {
setRequestLocale,
} from "next-intl/server"

import {
CommitHistory,
EpochResponse,
EthStoreResponse,
Lang,
StakingStatsData,
} from "@/lib/types"
import { CommitHistory, Lang, StakingStatsData } from "@/lib/types"

import I18nProvider from "@/components/I18nProvider"

Expand All @@ -25,40 +19,17 @@ import { BASE_TIME_UNIT } from "@/lib/constants"
import StakingPage from "./_components/staking"
import StakingPageJsonLD from "./page-jsonld"

const fetchBeaconchainData = async (): Promise<StakingStatsData> => {
// Fetch Beaconcha.in data
const base = "https://beaconcha.in"
const { href: ethstore } = new URL("api/v1/ethstore/latest", base)
const { href: epoch } = new URL("api/v1/epoch/latest", base)

// Get current APR from ethstore endpoint
const ethStoreResponse = await fetch(ethstore)
if (!ethStoreResponse.ok)
throw new Error("Network response from Beaconcha.in ETHSTORE was not ok")
const ethStoreResponseJson: EthStoreResponse = await ethStoreResponse.json()
const {
data: { apr },
} = ethStoreResponseJson

// Get total eligible ETH staked and total active validators from latest epoch endpoint
const epochResponse = await fetch(epoch)
if (!epochResponse.ok)
throw new Error("Network response from Beaconcha.in EPOCH was not ok")
const epochResponseJson: EpochResponse = await epochResponse.json()
const {
data: { validatorscount, eligibleether: eligibleGwei },
} = epochResponseJson

const totalEthStaked = Math.floor(eligibleGwei * 1e-9)

return { totalEthStaked, validatorscount, apr }
}
import { fetchBeaconchainEpoch } from "@/lib/api/fetchBeaconchainEpoch"
import { fetchBeaconchainEthstore } from "@/lib/api/fetchBeaconchainEthstore"

// In seconds
const REVALIDATE_TIME = BASE_TIME_UNIT * 1

const loadData = dataLoader(
[["stakingStatsData", fetchBeaconchainData]],
[
["beaconchainEpoch", fetchBeaconchainEpoch],
["beaconchainApr", fetchBeaconchainEthstore],
],
REVALIDATE_TIME * 1000
)

Expand All @@ -67,7 +38,13 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {

setRequestLocale(locale)

const [data] = await loadData()
const [{ totalEthStaked, validatorscount }, apr] = await loadData()

const data: StakingStatsData = {
totalEthStaked: "value" in totalEthStaked ? totalEthStaked.value : 0,
validatorscount: "value" in validatorscount ? validatorscount.value : 0,
apr: "value" in apr ? apr.value : 0,
}

// Get i18n messages
const allMessages = await getMessages({ locale })
Expand Down
2 changes: 1 addition & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@
path = "en/developers/tutorials/creating-a-wagmi-ui-for-your-contract/"

[functions]
included_files = ["i18n.config.json", "src/intl/**/*", "src/data/mocks/**/*"]
included_files = ["i18n.config.json", "src/intl/**/*", "src/data/mocks/**/*"]
4 changes: 3 additions & 1 deletion src/components/Staking/StakingStatsBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ const StakingStatsBox = ({ data }: StakingStatsBoxProps) => {

// Helper functions
const formatInteger = (amount: number): string =>
new Intl.NumberFormat(localeForStatsBoxNumbers).format(amount)
amount
? new Intl.NumberFormat(localeForStatsBoxNumbers).format(amount)
: "—"

const formatPercentage = (amount: number): string =>
new Intl.NumberFormat(localeForStatsBoxNumbers, {
Expand Down
1 change: 1 addition & 0 deletions src/data/mocks/beaconchainApr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "value": 0.025, "timestamp": 1759687080325 }
4 changes: 4 additions & 0 deletions src/data/mocks/beaconchainEpoch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"totalEthStaked": { "value": 35000000, "timestamp": 1759687080325 },
"validatorscount": { "value": 1000000, "timestamp": 1759687080325 }
}
2 changes: 1 addition & 1 deletion src/data/mocks/ethPrice.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"value":2651.16,"timestamp":1727788458138}
{ "value": 4000, "timestamp": 1759687080325 }
2 changes: 1 addition & 1 deletion src/data/mocks/totalEthStaked.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"value":34649295.539315075,"timestamp":1727788458553}
{ "value": 35000000, "timestamp": 1759687080325 }
62 changes: 62 additions & 0 deletions src/lib/api/fetchBeaconchainEpoch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { BeaconchainEpochData, EpochResponse } from "@/lib/types"

import { MAX_RETRIES } from "../constants"
import {
delayWithJitter,
fetchWithTimeoutAndRevalidation,
shouldStatusRetry,
sleep,
} from "../utils/data/utils"

export const fetchBeaconchainEpoch =
async (): Promise<BeaconchainEpochData> => {
const base = "https://beaconcha.in"
const endpoint = "api/v1/epoch/latest"
const { href } = new URL(endpoint, base)

const defaultErrorMessage = `Failed to fetch Beaconcha.in ${endpoint}`
const defaultError: BeaconchainEpochData = {
totalEthStaked: { error: defaultErrorMessage },
validatorscount: { error: defaultErrorMessage },
}

for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
const response = await fetchWithTimeoutAndRevalidation(href)
if (!response.ok) {
const status = response.status
const shouldRetry = attempt < MAX_RETRIES && shouldStatusRetry(status)
if (shouldRetry) {
await sleep(delayWithJitter())
continue
}
console.warn("Beaconcha.in fetch non-OK", { status, url: href })
const error = `Beaconcha.in responded with status ${status}`
return { totalEthStaked: { error }, validatorscount: { error } }
}
const json: EpochResponse = await response.json()
const { validatorscount, eligibleether } = json.data
const totalEthStaked = Math.floor(eligibleether * 1e-9) // `eligibleether` value returned in `gwei`
const timestamp = Date.now()
return {
totalEthStaked: { value: totalEthStaked, timestamp },
validatorscount: { value: validatorscount, timestamp },
}
} catch (err: unknown) {
const isLastAttempt = attempt >= MAX_RETRIES
if (isLastAttempt) {
console.error("Beaconcha.in fetch failed", {
name: err instanceof Error ? err.name : undefined,
message: err instanceof Error ? err.message : String(err),
url: href,
})
return defaultError
}
await sleep(delayWithJitter())
}
}

return defaultError
}

export default fetchBeaconchainEpoch
50 changes: 50 additions & 0 deletions src/lib/api/fetchBeaconchainEthstore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { EthStoreResponse, MetricReturnData } from "@/lib/types"

import { MAX_RETRIES } from "../constants"
import {
delayWithJitter,
fetchWithTimeoutAndRevalidation,
shouldStatusRetry,
sleep,
} from "../utils/data/utils"

export const fetchBeaconchainEthstore = async (): Promise<MetricReturnData> => {
const base = "https://beaconcha.in"
const endpoint = "api/v1/ethstore/latest"
const { href } = new URL(endpoint, base)

for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
const response = await fetchWithTimeoutAndRevalidation(href)
if (!response.ok) {
const status = response.status
const shouldRetry = attempt < MAX_RETRIES && shouldStatusRetry(status)
if (shouldRetry) {
await sleep(delayWithJitter())
continue
}
console.warn("Beaconcha.in fetch non-OK", { status, url: href })
return { error: `Beaconcha.in responded with status ${status}` }
}

const json: EthStoreResponse = await response.json()
const apr = json.data.apr
return { value: apr, timestamp: Date.now() }
} catch (err: unknown) {
const isLastAttempt = attempt >= MAX_RETRIES
if (isLastAttempt) {
console.error("Beaconcha.in fetch failed", {
name: err instanceof Error ? err.name : undefined,
message: err instanceof Error ? err.message : String(err),
url: href,
})
return { error: `Failed to fetch Beaconcha.in ${endpoint}` }
}
await sleep(delayWithJitter())
}
}

return { error: "Failed to fetch Beaconcha.in ethstore" }
}

export default fetchBeaconchainEthstore
19 changes: 0 additions & 19 deletions src/lib/api/fetchEthStakedBeaconchain.ts

This file was deleted.

7 changes: 6 additions & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,14 @@ export const COINGECKO_API_BASE_URL =
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&category="
export const COINGECKO_API_URL_PARAMS =
"&order=market_cap_desc&per_page=250&page=1&sparkline=false"
export const BASE_TIME_UNIT = 3600 // 1 hour
export const COLOR_MODE_STORAGE_KEY = "theme"

// API timing
export const BASE_TIME_UNIT = 3600 // (seconds) 1 hour
export const TIMEOUT_MS = 5000 // (milliseconds)
export const MAX_RETRIES = 1
export const RETRY_DELAY_BASE_MS = 250 // (milliseconds)

// Quiz Hub
export const PROGRESS_BAR_GAP = "4px"
export const PASSING_QUIZ_SCORE = 65
Expand Down
12 changes: 8 additions & 4 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,10 +519,14 @@ export type EthStakedResponse = {
}
}

export type EpochResponse = Data<{
validatorscount: number
eligibleether: number
}>
export type EpochResponse = Data<
Record<"eligibleether" | "validatorscount", number>
>

export type BeaconchainEpochData = Record<
"totalEthStaked" | "validatorscount",
MetricReturnData
>

export type StakingStatsData = {
totalEthStaked: number
Expand Down
Loading
Loading