Skip to content

Commit fda7346

Browse files
committed
Deduplicate CoinGecko Ethereum metrics fetch
1 parent fb5a152 commit fda7346

5 files changed

Lines changed: 158 additions & 95 deletions

File tree

src/data-layer/fetchers/fetchEthPrice.ts

Lines changed: 0 additions & 47 deletions
This file was deleted.

src/data-layer/fetchers/fetchEthereumMarketcap.ts

Lines changed: 0 additions & 43 deletions
This file was deleted.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { EthPriceData, MetricReturnData } from "@/lib/types"
2+
3+
import { fetchRetry } from "./fetchRetry"
4+
5+
type SuccessfulEthPriceData = Extract<EthPriceData, { value: number }>
6+
type SuccessfulMetricReturnData = Extract<
7+
MetricReturnData,
8+
{ value: number }
9+
>
10+
11+
interface CoinGeckoEthereumResponse {
12+
ethereum?: {
13+
usd?: number
14+
usd_24h_change?: number | null
15+
usd_market_cap?: number
16+
}
17+
}
18+
19+
export interface EthereumMetricsData {
20+
ethPrice: SuccessfulEthPriceData
21+
ethereumMarketcap: SuccessfulMetricReturnData
22+
}
23+
24+
export function parseEthereumMetrics(
25+
data: CoinGeckoEthereumResponse,
26+
timestamp = Date.now()
27+
): EthereumMetricsData {
28+
const { usd, usd_24h_change, usd_market_cap } = data.ethereum ?? {}
29+
30+
if (typeof usd !== "number" || !Number.isFinite(usd)) {
31+
throw new Error("Unable to fetch ETH price from CoinGecko")
32+
}
33+
34+
if (
35+
typeof usd_market_cap !== "number" ||
36+
!Number.isFinite(usd_market_cap)
37+
) {
38+
throw new Error("Unable to fetch ETH market cap from CoinGecko")
39+
}
40+
41+
const percentChange24h =
42+
typeof usd_24h_change === "number" && Number.isFinite(usd_24h_change)
43+
? usd_24h_change / 100
44+
: undefined
45+
46+
return {
47+
ethPrice: { value: usd, timestamp, percentChange24h },
48+
ethereumMarketcap: { value: usd_market_cap, timestamp },
49+
}
50+
}
51+
52+
/**
53+
* Fetch Ethereum price, 24-hour change, and market cap in one CoinGecko call.
54+
*/
55+
export async function fetchEthereumMetrics(): Promise<EthereumMetricsData> {
56+
const apiKey = process.env.COINGECKO_API_KEY
57+
const params = new URLSearchParams({
58+
ids: "ethereum",
59+
vs_currencies: "usd",
60+
include_24hr_change: "true",
61+
include_market_cap: "true",
62+
})
63+
const url = `https://api.coingecko.com/api/v3/simple/price?${params}`
64+
65+
console.log("Starting Ethereum metrics fetch")
66+
67+
const response = await fetchRetry(url, {
68+
headers: apiKey ? { "x-cg-demo-api-key": apiKey } : undefined,
69+
})
70+
71+
if (!response.ok) {
72+
const { status } = response
73+
console.warn("CoinGecko fetch non-OK", { status })
74+
throw new Error(`CoinGecko responded with status ${status}`)
75+
}
76+
77+
const metrics = parseEthereumMetrics(
78+
(await response.json()) as CoinGeckoEthereumResponse
79+
)
80+
81+
console.log("Successfully fetched Ethereum metrics", {
82+
price: metrics.ethPrice.value,
83+
percentChange24h: metrics.ethPrice.percentChange24h,
84+
marketCap: metrics.ethereumMarketcap.value,
85+
timestamp: metrics.ethPrice.timestamp,
86+
})
87+
88+
return metrics
89+
}

src/data-layer/tasks.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ import { fetchApps } from "./fetchers/fetchApps"
1515
import { fetchBlobStats } from "./fetchers/fetchBlobStats"
1616
import { fetchCalendarEvents } from "./fetchers/fetchCalendarEvents"
1717
import { fetchCommunityPicks } from "./fetchers/fetchCommunityPicks"
18-
import { fetchEthereumMarketcap } from "./fetchers/fetchEthereumMarketcap"
1918
import { fetchEthereumStablecoinsMcap } from "./fetchers/fetchEthereumStablecoinsMcap"
20-
import { fetchEthPrice } from "./fetchers/fetchEthPrice"
19+
import { fetchEthereumMetrics } from "./fetchers/fetchEthereumMetrics"
2120
import { fetchEvents } from "./fetchers/fetchEvents"
2221
import { fetchGasPrice } from "./fetchers/fetchGasPrice"
2322
import { fetchGFIs } from "./fetchers/fetchGFIs"
@@ -96,9 +95,7 @@ const DAILY: TaskDef[] = [
9695
]
9796

9897
const HOURLY: TaskDef[] = [
99-
[KEYS.ETHEREUM_MARKETCAP, fetchEthereumMarketcap],
10098
[KEYS.ETHEREUM_STABLECOINS_MCAP, fetchEthereumStablecoinsMcap],
101-
[KEYS.ETH_PRICE, fetchEthPrice],
10299
[KEYS.GAS_PRICE, fetchGasPrice],
103100
[KEYS.TOTAL_ETH_STAKED, fetchTotalEthStaked],
104101
[KEYS.TOTAL_VALUE_LOCKED, fetchTotalValueLocked],
@@ -124,9 +121,30 @@ function createDataTask([key, fetchFn]: TaskDef) {
124121
})
125122
}
126123

124+
const ethereumMetricsFetchTask = task({
125+
id: KEYS.ETH_PRICE,
126+
retry: {
127+
maxAttempts: 2,
128+
},
129+
catchError: async ({ error }) => {
130+
logger.error(`[${KEYS.ETH_PRICE}] failed`, { error })
131+
},
132+
run: async () => {
133+
const { ethPrice, ethereumMarketcap } = await fetchEthereumMetrics()
134+
135+
await Promise.all([
136+
set(KEYS.ETH_PRICE, ethPrice),
137+
set(KEYS.ETHEREUM_MARKETCAP, ethereumMarketcap),
138+
])
139+
140+
logger.info(`Stored ${KEYS.ETH_PRICE} + ${KEYS.ETHEREUM_MARKETCAP}`)
141+
return { keys: [KEYS.ETH_PRICE, KEYS.ETHEREUM_MARKETCAP] }
142+
},
143+
})
144+
127145
const weeklyFetchTasks = WEEKLY.map(createDataTask)
128146
const dailyFetchTasks = DAILY.map(createDataTask)
129-
const hourlyFetchTasks = HOURLY.map(createDataTask)
147+
const hourlyFetchTasks = [ethereumMetricsFetchTask, ...HOURLY.map(createDataTask)]
130148

131149
// Must export for trigger.dev to discover
132150
export const allFetchTasks = [
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { expect, test } from "@playwright/test"
2+
3+
import { parseEthereumMetrics } from "@/data-layer/fetchers/fetchEthereumMetrics"
4+
5+
test.describe("parseEthereumMetrics", () => {
6+
test("maps one CoinGecko response to both stored metric formats", () => {
7+
const timestamp = 1_700_000_000_000
8+
const result = parseEthereumMetrics(
9+
{
10+
ethereum: {
11+
usd: 3_500,
12+
usd_24h_change: 2.5,
13+
usd_market_cap: 420_000_000_000,
14+
},
15+
},
16+
timestamp
17+
)
18+
19+
expect(result.ethPrice).toEqual({
20+
value: 3_500,
21+
timestamp,
22+
percentChange24h: 0.025,
23+
})
24+
expect(result.ethereumMarketcap).toEqual({
25+
value: 420_000_000_000,
26+
timestamp,
27+
})
28+
})
29+
30+
test("allows CoinGecko to omit the 24-hour change", () => {
31+
const result = parseEthereumMetrics({
32+
ethereum: { usd: 3_500, usd_market_cap: 420_000_000_000 },
33+
})
34+
35+
expect(result.ethPrice.percentChange24h).toBeUndefined()
36+
})
37+
38+
for (const [name, response] of [
39+
["price", { ethereum: { usd_market_cap: 420_000_000_000 } }],
40+
["market cap", { ethereum: { usd: 3_500 } }],
41+
] as const) {
42+
test(`rejects a response without a valid ${name}`, () => {
43+
expect(() => parseEthereumMetrics(response)).toThrow()
44+
})
45+
}
46+
})

0 commit comments

Comments
 (0)