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
2 changes: 1 addition & 1 deletion .claude/skills/data-layer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const DAILY: Task[] = [

const HOURLY: Task[] = [
[KEYS.ETH_PRICE, fetchEthPrice],
[KEYS.BEACONCHAIN_EPOCH, fetchBeaconChainEpoch],
[KEYS.BEACONCHAIN, fetchBeaconChain],
]
```

Expand Down
14 changes: 7 additions & 7 deletions app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ import { routing } from "@/i18n/routing"
import {
getAppsData,
getAttestantPosts,
getBeaconchainEpochData,
getBeaconchainData,
getEthPrice,
getEventsData,
getGrowThePieData,
Expand Down Expand Up @@ -139,7 +139,7 @@ const Page = async ({ params }: { params: PageParams }) => {
// Fetch data using the new data-layer functions (already cached)
const [
ethPrice,
beaconchainEpochData,
beaconchainData,
totalValueLocked,
growThePieData,
attestantPosts,
Expand All @@ -148,7 +148,7 @@ const Page = async ({ params }: { params: PageParams }) => {
eventsData,
] = await Promise.all([
getEthPrice(),
getBeaconchainEpochData(),
getBeaconchainData(),
getTotalValueLockedData(),
getGrowThePieData(),
getAttestantPosts(),
Expand All @@ -161,8 +161,8 @@ const Page = async ({ params }: { params: PageParams }) => {
if (!ethPrice) {
throw new Error("Failed to fetch ETH price data")
}
if (!beaconchainEpochData) {
throw new Error("Failed to fetch Beaconchain epoch data")
if (!beaconchainData) {
throw new Error("Failed to fetch Beaconchain data")
}
if (!totalValueLocked) {
throw new Error("Failed to fetch total value locked data")
Expand All @@ -186,8 +186,8 @@ const Page = async ({ params }: { params: PageParams }) => {
)
}

// Extract totalEthStaked from beaconchainEpochData
const { totalEthStaked } = beaconchainEpochData
// Extract totalEthStaked from beaconchainData
const { totalEthStaked } = beaconchainData

// Events - use empty array as fallback
const upcomingEvents = (eventsData ?? []).slice(0, 3)
Expand Down
16 changes: 5 additions & 11 deletions app/[locale]/staking/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,23 @@ import { getRequiredNamespacesForPage } from "@/lib/utils/translations"
import StakingPage from "./_components/staking"
import StakingPageJsonLD from "./page-jsonld"

import { getBeaconchainEpochData, getBeaconchainEthstoreData } from "@/lib/data"
import { getBeaconchainData } from "@/lib/data"

const Page = async ({ params }: { params: PageParams }) => {
const { locale } = params

setRequestLocale(locale)

// Fetch data using the new data-layer functions (already cached)
const [beaconchainEpochData, apr] = await Promise.all([
getBeaconchainEpochData(),
getBeaconchainEthstoreData(),
])
const beaconchainData = await getBeaconchainData()

// Handle null cases - throw error if required data is missing
if (!beaconchainEpochData) {
throw new Error("Failed to fetch Beaconchain epoch data")
}
if (!apr) {
throw new Error("Failed to fetch Beaconchain APR data")
if (!beaconchainData) {
throw new Error("Failed to fetch Beaconchain data")
}

// Extract values from data structures
const { totalEthStaked, validatorscount } = beaconchainEpochData
const { totalEthStaked, validatorscount, apr } = beaconchainData

const data: StakingStatsData = {
totalEthStaked: "value" in totalEthStaked ? totalEthStaked.value : 0,
Expand Down
63 changes: 63 additions & 0 deletions src/data-layer/fetchers/fetchBeaconChain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type {
BeaconchainEpochData,
EpochResponse,
EthStoreResponse,
MetricReturnData,
} from "@/lib/types"

export type BeaconChainData = BeaconchainEpochData & { apr: MetricReturnData }

/**
* Fetch beaconchain data from Beaconcha.in API.
* Combines epoch and ethstore endpoints (sequential to respect 1 req/sec limit).
*/
export async function fetchBeaconChain(): Promise<BeaconChainData> {
const base = "https://beaconcha.in"

console.log("Starting beaconchain data fetch")

// Fetch epoch data
const epochUrl = new URL("api/v1/epoch/latest", base).href
const epochResponse = await fetch(epochUrl)
if (!epochResponse.ok) {
const status = epochResponse.status
console.warn("Beaconcha.in epoch fetch non-OK", { status, url: epochUrl })
const error = `Beaconcha.in epoch responded with status ${status}`
throw new Error(error)
}
const epochJson: EpochResponse = await epochResponse.json()
const { validatorscount, eligibleether } = epochJson.data
const totalEthStaked = Math.floor(eligibleether * 1e-9)

// Wait 1s to respect rate limit
await new Promise((r) => setTimeout(r, 1000))

// Fetch ethstore data
const ethstoreUrl = new URL("api/v1/ethstore/latest", base).href
const ethstoreResponse = await fetch(ethstoreUrl)
if (!ethstoreResponse.ok) {
const status = ethstoreResponse.status
console.warn("Beaconcha.in ethstore fetch non-OK", {
status,
url: ethstoreUrl,
})
const error = `Beaconcha.in ethstore responded with status ${status}`
throw new Error(error)
}
const ethstoreJson: EthStoreResponse = await ethstoreResponse.json()
const apr = ethstoreJson.data.apr

const timestamp = Date.now()

console.log("Successfully fetched beaconchain data", {
totalEthStaked,
validatorscount,
apr,
})

return {
totalEthStaked: { value: totalEthStaked, timestamp },
validatorscount: { value: validatorscount, timestamp },
apr: { value: apr, timestamp },
}
}
40 changes: 0 additions & 40 deletions src/data-layer/fetchers/fetchBeaconChainEpoch.ts

This file was deleted.

35 changes: 0 additions & 35 deletions src/data-layer/fetchers/fetchBeaconChainEthstore.ts

This file was deleted.

5 changes: 2 additions & 3 deletions src/data-layer/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {
AppData,
BeaconchainEpochData,
BlobscanOverallStats,
BlockspaceData,
Commit,
Expand All @@ -16,6 +15,7 @@ import type {
} from "@/lib/types"
import type { CommunityEventsReturnType } from "@/lib/interfaces"

import type { BeaconChainData } from "./fetchers/fetchBeaconChain"
import type { CoinGeckoCoinMarketResponse } from "./fetchers/fetchStablecoinsData"
import { get } from "./storage"
import { KEYS } from "./tasks"
Expand All @@ -32,8 +32,7 @@ export const getCommunityPicks = () => get<CommunityPick[]>(KEYS.COMMUNITY_PICKS
export const getCalendarEvents = () => get<CommunityEventsReturnType>(KEYS.CALENDAR_EVENTS)
export const getRSSData = () => get<RSSItem[][]>(KEYS.RSS)
export const getAttestantPosts = () => get<RSSItem[]>(KEYS.POSTS)
export const getBeaconchainEpochData = () => get<BeaconchainEpochData>(KEYS.BEACONCHAIN_EPOCH)
export const getBeaconchainEthstoreData = () => get<MetricReturnData>(KEYS.BEACONCHAIN_ETHSTORE)
export const getBeaconchainData = () => get<BeaconChainData>(KEYS.BEACONCHAIN)
export const getBlobscanStats = () => get<BlobscanOverallStats>(KEYS.BLOBSCAN_STATS)
export const getEthereumMarketcapData = () => get<MetricReturnData>(KEYS.ETHEREUM_MARKETCAP)
export const getEthereumStablecoinsMcapData = () => get<MetricReturnData>(KEYS.ETHEREUM_STABLECOINS_MCAP)
Expand Down
4 changes: 0 additions & 4 deletions src/data-layer/mocks/fetch-beaconchain-ethstore.json

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
"validatorscount": {
"value": 993342,
"timestamp": 1765912003680
},
"apr": {
"value": 0.0281997826662808,
"timestamp": 1765912003680
}
}
}
5 changes: 2 additions & 3 deletions src/data-layer/mocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@
* for local development without needing to connect to Netlify Blobs.
*
* Generated: 2025-12-16T18:32:05.983Z
* Total files: 21
* Total files: 20
*/

export const mockTaskIds = [
"fetch-apps",
"fetch-beaconchain-epoch",
"fetch-beaconchain",
"fetch-events",
"fetch-beaconchain-ethstore",
"fetch-blobscan-stats",
"fetch-calendar-events",
"fetch-community-picks",
Expand Down
9 changes: 3 additions & 6 deletions src/data-layer/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
import { schedules } from "@trigger.dev/sdk/v3"

import { fetchApps } from "./fetchers/fetchApps"
import { fetchBeaconChainEpoch } from "./fetchers/fetchBeaconChainEpoch"
import { fetchBeaconChainEthstore } from "./fetchers/fetchBeaconChainEthstore"
import { fetchBeaconChain } from "./fetchers/fetchBeaconChain"
import { fetchBlobscanStats } from "./fetchers/fetchBlobscanStats"
import { fetchCalendarEvents } from "./fetchers/fetchCalendarEvents"
import { fetchCommunityPicks } from "./fetchers/fetchCommunityPicks"
Expand Down Expand Up @@ -45,8 +44,7 @@ export const KEYS = {
RSS: "fetch-rss",
GITHUB_REPO_DATA: "fetch-github-repo-data",
EVENTS: "fetch-events",
BEACONCHAIN_EPOCH: "fetch-beaconchain-epoch",
BEACONCHAIN_ETHSTORE: "fetch-beaconchain-ethstore",
BEACONCHAIN: "fetch-beaconchain",
BLOBSCAN_STATS: "fetch-blobscan-stats",
ETHEREUM_MARKETCAP: "fetch-ethereum-marketcap",
ETHEREUM_STABLECOINS_MCAP: "fetch-ethereum-stablecoins-mcap",
Expand Down Expand Up @@ -76,8 +74,7 @@ const DAILY: Task[] = [
]

const HOURLY: Task[] = [
[KEYS.BEACONCHAIN_EPOCH, fetchBeaconChainEpoch],
[KEYS.BEACONCHAIN_ETHSTORE, fetchBeaconChainEthstore],
[KEYS.BEACONCHAIN, fetchBeaconChain],
[KEYS.BLOBSCAN_STATS, fetchBlobscanStats],
[KEYS.ETHEREUM_MARKETCAP, fetchEthereumMarketcap],
[KEYS.ETHEREUM_STABLECOINS_MCAP, fetchEthereumStablecoinsMcap],
Expand Down
12 changes: 3 additions & 9 deletions src/lib/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,9 @@ export const getAttestantPosts = createCachedGetter(
CACHE_REVALIDATE_HOUR
)

export const getBeaconchainEpochData = createCachedGetter(
dataLayer.getBeaconchainEpochData,
["beaconchain-epoch-data"],
CACHE_REVALIDATE_HOUR
)

export const getBeaconchainEthstoreData = createCachedGetter(
dataLayer.getBeaconchainEthstoreData,
["beaconchain-ethstore-data"],
export const getBeaconchainData = createCachedGetter(
dataLayer.getBeaconchainData,
["beaconchain-data"],
CACHE_REVALIDATE_HOUR
)

Expand Down
33 changes: 14 additions & 19 deletions tests/unit/data-layer/getters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,30 +221,25 @@ test.describe("Data-Layer Getters", () => {
})

test.describe("Staking & Beaconchain", () => {
test("getBeaconchainEpochData returns epoch data or null", async () => {
const result = await dataLayer.getBeaconchainEpochData()
test("getBeaconchainData returns combined data or null", async () => {
const result = await dataLayer.getBeaconchainData()
if (result !== null) {
expect(result).toHaveProperty("totalEthStaked")
expect(result).toHaveProperty("validatorscount")
// Check for value property before accessing (MetricReturnData is ValueOrError<number>)
const totalEthStaked = result.totalEthStaked
if ("value" in totalEthStaked) {
expect(typeof totalEthStaked.value).toBe("number")
expect(totalEthStaked.value).toBeGreaterThan(0)
expect(result).toHaveProperty("apr")
// Check values (MetricReturnData is a union type)
if ("value" in result.totalEthStaked) {
expect(typeof result.totalEthStaked.value).toBe("number")
expect(result.totalEthStaked.value).toBeGreaterThan(0)
}
const validatorscount = result.validatorscount
if ("value" in validatorscount) {
expect(typeof validatorscount.value).toBe("number")
expect(validatorscount.value).toBeGreaterThan(0)
if ("value" in result.validatorscount) {
expect(typeof result.validatorscount.value).toBe("number")
expect(result.validatorscount.value).toBeGreaterThan(0)
}
if ("value" in result.apr) {
expect(typeof result.apr.value).toBe("number")
expect(result.apr.value).toBeGreaterThan(0)
}
}
})

test("getBeaconchainEthstoreData returns MetricReturnData or null", async () => {
const result = await dataLayer.getBeaconchainEthstoreData()
if (result !== null && "value" in result) {
expect(typeof result.value).toBe("number")
expect(result.value).toBeGreaterThan(0)
}
})

Expand Down