Skip to content

Commit

Permalink
feat: add growthepie metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
wackerow committed Aug 23, 2024
1 parent 32d22c3 commit 2b6da02
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 53 deletions.
95 changes: 51 additions & 44 deletions src/components/StatsBoxGrid/useStatsBoxGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* TODO: Update metric for new homepage:
* - Replace TVL DeFi with "Total value held on Ethereum"
* - Replace Node Count with "Average transaction cost"
* - [ ] Replace TVL DeFi with "Total value held on Ethereum"
*/
import { useRouter } from "next/router"
import { useTranslation } from "next-i18next"
Expand All @@ -10,96 +9,104 @@ import type { AllMetricData, Lang, StatsBoxMetric } from "@/lib/types"

import { getLocaleForNumberFormat } from "@/lib/utils/translations"

const formatTotalStaked = (amount: number, locale: string): string => {
const formatLargeUSD = (value: number, locale: string): string => {
return new Intl.NumberFormat(locale, {
style: "currency",
currency: "USD",
notation: "compact",
minimumSignificantDigits: 3,
maximumSignificantDigits: 4,
}).format(amount)
}).format(value)
}

const formatTVL = (tvl: number, locale: string): string => {
const formatSmallUSD = (value: number, locale: string): string => {
return new Intl.NumberFormat(locale, {
style: "currency",
currency: "USD",
notation: "compact",
minimumSignificantDigits: 3,
maximumSignificantDigits: 4,
}).format(tvl)
minimumSignificantDigits: 2,
maximumSignificantDigits: 3,
}).format(value)
}

const formatTxs = (txs: number, locale: string): string => {
const formatLargeNumber = (value: number, locale: string): string => {
return new Intl.NumberFormat(locale, {
notation: "compact",
minimumSignificantDigits: 3,
maximumSignificantDigits: 4,
}).format(txs)
}

const formatNodes = (nodes: number, locale: string): string => {
return new Intl.NumberFormat(locale, {
minimumSignificantDigits: 3,
maximumSignificantDigits: 4,
}).format(nodes)
}).format(value)
}

export const useStatsBoxGrid = ({
totalEthStaked,
nodeCount,
totalValueLocked,
txCount,
txCostsMedianUsd,
ethPrice,
}: AllMetricData): StatsBoxMetric[] => {
const { t } = useTranslation("page-index")
const { locale } = useRouter()

const localeForNumberFormat = getLocaleForNumberFormat(locale! as Lang)

const totalEtherStaked =
"error" in totalEthStaked
? { error: totalEthStaked.error }
: {
value: formatTotalStaked(totalEthStaked.value, localeForNumberFormat),
}
const hasEthStakerAndPriceData =
"value" in totalEthStaked && "value" in ethPrice
const totalStakedInUsd = hasEthStakerAndPriceData
? totalEthStaked.value * ethPrice.value
: 0

const totalEtherStaked = !totalStakedInUsd
? {
error:
"error" in totalEthStaked
? totalEthStaked.error
: "error" in ethPrice
? ethPrice.error
: "",
}
: {
value: formatLargeUSD(totalStakedInUsd, localeForNumberFormat),
}

const valueLocked =
"error" in totalValueLocked
? { error: totalValueLocked.error }
: { value: formatTVL(totalValueLocked.value, localeForNumberFormat) }
: { value: formatLargeUSD(totalValueLocked.value, localeForNumberFormat) }

const txs =
"error" in txCount
? { error: txCount.error }
: { value: formatTxs(txCount.value, localeForNumberFormat) }
: { value: formatLargeNumber(txCount.value, localeForNumberFormat) }

const nodes =
"error" in nodeCount
? { error: nodeCount.error }
: { value: formatNodes(nodeCount.value, localeForNumberFormat) }
const medianTxCost =
"error" in txCostsMedianUsd
? { error: txCostsMedianUsd.error }
: { value: formatSmallUSD(txCostsMedianUsd.value, localeForNumberFormat) }

const metrics: StatsBoxMetric[] = [
{
apiProvider: "Dune Analytics",
apiUrl: "https://dune.com/",
label: t("page-index-network-stats-total-eth-staked"),
state: totalEtherStaked,
apiProvider: "DeFi Llama",
apiUrl: "https://defillama.com/",
label: t("page-index-network-stats-value-defi-description"),
state: valueLocked,
},
{
apiProvider: "Etherscan",
apiUrl: "https://etherscan.io/",
apiProvider: "GrowThePie",
apiUrl: "https://growthepie.xyz/",
label: t("page-index-network-stats-tx-day-description"),
state: txs,
},
{
apiProvider: "DeFi Llama",
apiUrl: "https://defillama.com/",
label: t("page-index-network-stats-value-defi-description"),
state: valueLocked,
apiProvider: "GrowThePie",
apiUrl: "https://growthepie.xyz/",
label: "Median transaction cost", // t("page-index-network-stats-nodes-description"),
state: medianTxCost,
},
{
apiProvider: "Etherscan",
apiUrl: "https://etherscan.io/nodetracker",
label: t("page-index-network-stats-nodes-description"),
state: nodes,
apiProvider: "Dune Analytics",
apiUrl: "https://dune.com/",
label: t("page-index-network-stats-total-eth-staked"),
state: totalEtherStaked,
},
]

Expand Down
17 changes: 17 additions & 0 deletions src/lib/api/fetchEthPrice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MetricReturnData } from "../types"

export const fetchEthPrice = async (): Promise<MetricReturnData> => {
try {
const data: { ethereum: { usd: number } } = await fetch(
"https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd"
).then((res) => res.json())
const {
ethereum: { usd },
} = data
if (!usd) throw new Error("Unable to fetch ETH price from CoinGecko")
return { value: usd }
} catch (error: unknown) {
console.error((error as Error).message)
return { error: (error as Error).message }
}
}
64 changes: 64 additions & 0 deletions src/lib/api/fetchGrowThePie.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { GrowThePieData } from "../types"

type DataItem = {
metric_key: string
origin_key: string
date: string
value: number
}

const TXCOSTS_MEDIAN_USD = "txcosts_median_usd"
const TXCOUNT = "txcount"

export const fetchGrowThePie = async (): Promise<GrowThePieData> => {
const url = "https://api.growthepie.xyz/v1/fundamentals_full.json"
try {
const response = await fetch(url)
if (!response.ok) {
console.log(response.status, response.statusText)
throw new Error("Failed to fetch GrowThePie data")
}
const data: DataItem[] = await response.json()

const mostRecentDate = data.reduce((latest, item) => {
const itemDate = new Date(item.date)
return itemDate > new Date(latest) ? item.date : latest
}, data[0].date)

const todaysData = data.filter(
(item) =>
item.date === mostRecentDate &&
[TXCOSTS_MEDIAN_USD, TXCOUNT].includes(item.metric_key)
)

let totalTxCount = 0
let weightedSum = 0

todaysData.forEach((item) => {
if (item.metric_key !== TXCOSTS_MEDIAN_USD) return

const txCountItem = todaysData.find(
(txItem) =>
txItem.metric_key === TXCOUNT && txItem.origin_key === item.origin_key
)
if (!txCountItem) return

totalTxCount += txCountItem.value
weightedSum += item.value * txCountItem.value
})

// The weighted average of txcosts_median_usd, by txcount on each network (origin_key)
const weightedAverage = totalTxCount ? weightedSum / totalTxCount : 0

return {
txCount: { value: totalTxCount },
txCostsMedianUsd: { value: weightedAverage },
}
} catch (error: unknown) {
console.error((error as Error).message)
return {
txCount: { error: (error as Error).message },
txCostsMedianUsd: { error: (error as Error).message },
}
}
}
9 changes: 7 additions & 2 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,11 +539,16 @@ export type MetricReturnData = ValueOrError<number>

export type StatsBoxState = ValueOrError<string>

export type GrowThePieMetricKey = "txCount" | "txCostsMedianUsd"

export type GrowThePieData = Record<GrowThePieMetricKey, MetricReturnData>

export type MetricName =
| "ethPrice" // Use with `totalEthStaked` to convert ETH to USD
| "totalEthStaked"
| "nodeCount"
| "totalValueLocked"
| "txCount"
| GrowThePieMetricKey

export type AllMetricData = Record<MetricName, MetricReturnData>

export type StatsBoxMetric = {
Expand Down
16 changes: 9 additions & 7 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,26 +78,26 @@ import SimpleDomainRegistryContent from "!!raw-loader!@/data/SimpleDomainRegistr
import SimpleTokenContent from "!!raw-loader!@/data/SimpleToken.sol"
import SimpleWalletContent from "!!raw-loader!@/data/SimpleWallet.sol"
import { fetchCommunityEvents } from "@/lib/api/calendarEvents"
import { fetchNodes } from "@/lib/api/fetchNodes"
import { fetchEthPrice } from "@/lib/api/fetchEthPrice"
import { fetchGrowThePie } from "@/lib/api/fetchGrowThePie"
import { fetchAttestantPosts } from "@/lib/api/fetchPosts"
import { fetchRSS } from "@/lib/api/fetchRSS"
import { fetchTotalEthStaked } from "@/lib/api/fetchTotalEthStaked"
import { fetchTotalValueLocked } from "@/lib/api/fetchTotalValueLocked"
import { fetchTxCount } from "@/lib/api/fetchTxCount"
import EventFallback from "@/public/images/events/event-placeholder.png"
import buildersImage from "@/public/images/heroes/developers-hub-hero.jpg"
import activityImage from "@/public/images/heroes/layer-2-hub-hero.jpg"
import learnImage from "@/public/images/heroes/learn-hub-hero.png"
import communityImage from "@/public/images/heroes/quizzes-hub-hero.png"
import hero from "@/public/images/home/hero.png"

const cachedFetchCommunityEvents = runOnlyOnce(fetchCommunityEvents)
const cachedEthPrice = runOnlyOnce(fetchEthPrice)
const cachedFetchTotalEthStaked = runOnlyOnce(fetchTotalEthStaked)
const cachedFetchNodes = runOnlyOnce(fetchNodes)
const cachedFetchTotalValueLocked = runOnlyOnce(fetchTotalValueLocked)
const cachedFetchTxCount = runOnlyOnce(fetchTxCount)
const cachedXmlBlogFeeds = runOnlyOnce(async () => await fetchRSS(XML_FEEDS))
const cachedAttestantBlog = runOnlyOnce(fetchAttestantPosts)
const cachedGrowThePieData = runOnlyOnce(fetchGrowThePie)
const cachedFetchCommunityEvents = runOnlyOnce(fetchCommunityEvents)

type Props = BasePageProps & {
communityEvents: CommunityEventsReturnType
Expand All @@ -106,11 +106,13 @@ type Props = BasePageProps & {
}

export const getStaticProps = (async ({ locale }) => {
const growThePieData = await cachedGrowThePieData()
const metricResults: AllMetricData = {
ethPrice: await cachedEthPrice(),
totalEthStaked: await cachedFetchTotalEthStaked(),
nodeCount: await cachedFetchNodes(),
totalValueLocked: await cachedFetchTotalValueLocked(),
txCount: await cachedFetchTxCount(),
txCount: growThePieData.txCount,
txCostsMedianUsd: growThePieData.txCostsMedianUsd,
}

const communityEvents = await cachedFetchCommunityEvents()
Expand Down

0 comments on commit 2b6da02

Please sign in to comment.