From e454fb86b9a6192233e7352da1d2512e8a2982a0 Mon Sep 17 00:00:00 2001 From: m1amgn Date: Wed, 27 May 2026 11:07:09 +0700 Subject: [PATCH] #615: changed on main page total fdv; added Top Performers & Highlights real functionality --- AGENTS.md | 64 +++++ CLAUDE.md | 64 +++++ messages/en.json | 19 +- messages/pt.json | 19 +- messages/ru.json | 19 +- .../[locale]/components/home/stats-table.tsx | 34 +-- .../components/home/top-performers.tsx | 60 +++-- src/app/services/headerInfo-service.ts | 42 ++++ src/app/services/top-performers-service.ts | 218 ++++++++++++++++++ src/utils/fdv-excluded-chains.ts | 5 + 10 files changed, 487 insertions(+), 57 deletions(-) create mode 100644 src/app/services/top-performers-service.ts create mode 100644 src/utils/fdv-excluded-chains.ts diff --git a/AGENTS.md b/AGENTS.md index 90dc70ce..494366b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -415,3 +415,67 @@ Before completing any code modification task, verify: - Generate docs: `npx gitnexus wiki` + +--- + +# Implementation Protocol + +Applies when a task is a **feature or touches 3+ files**. For smaller changes +(1-2 files, typo, question, research-only), skip this protocol and work normally. + +## Preconditions (first step) +- **git**: if the repo is under git, the commit rule is active. If not, skip + commits and note `Commit: N/A (no git)` in the diary. +- **clawmem**: check the index (`index_stats` / `status`). If not indexed, index + it first (`reindex`; add the project to `~/.config/clawmem/index.yml` if + needed), then continue. If indexing is impossible, mirror the durable summary + into the diary with `clawmem: N/A`. + +## 1. Prior-art search (before planning) +Search whether this was built or researched before: +- **clawmem**: `search` / `intent_search` / `find_similar` on the feature topic +- **git**: `git log --grep` and `git log -S` +- **tasks**: grep past diaries in `.tasks/` + +Record findings in the diary. Reuse what exists, or state why you build anew. + +## 2. Diary (`.tasks/`, gitignored) +Create `.tasks/YYYY-MM-DD-.md` first. One file per task. The agent only +creates and appends — it never deletes (the human cleans manually). + +Structure: +- **Plan**: checklist of tickets (T1, T2, …) +- **Prior art**: clawmem / git / tasks findings + conclusion +- **Per ticket on close**: what done, how, deviations from plan + why, research, + commit hash (if git), clawmem id + +## 3. Stages & commits +A **stage = an atomic, revertable unit** that can be described as one change. +Group tickets into a stage by this criterion, not by ticket count. + +On closing a stage → commit. The message is detailed and natural, the way a +person writes it: +- **what** changed (files / modules) +- **why** (the problem it solves) +- **how** (approach, key decisions) +- **deviations** from plan, and research if it shaped the decision + +The commit message MUST stay sterile: no task slug, no clawmem id, no mention of +`.tasks/`, clawmem, or this protocol, and no `Co-Authored-By` / "Generated with" +trailer. The commit is the only artifact that leaves the machine. + +## 4. clawmem (per stage + final) +- **Per stage**: a durable entry mirroring the commit content (what / how / + deviations / research) plus the commit hash. +- **On task completion**: pin a final summary (`memory_pin`) — outcome, key + decisions, pitfalls. This survives diary cleanup and is what the next task's + prior-art search finds. + +## 5. The linked graph (wiring lives on the private side only) +Join key = **commit hash** (a hash reveals nothing about the system). +- diary ticket stores: commit hash + clawmem id +- clawmem entry stores: commit hash + task slug +- commit stores: nothing pointing back + +From any node, reach the other two via the hash. The commit stays clean; the +working artifacts (slug, ids, diaries) never leave the machine. diff --git a/CLAUDE.md b/CLAUDE.md index b3e897ad..b22cfdb5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -484,3 +484,67 @@ Before completing any code modification task, verify: - Run `yarn build` before pushing + +--- + +# Implementation Protocol + +Applies when a task is a **feature or touches 3+ files**. For smaller changes +(1-2 files, typo, question, research-only), skip this protocol and work normally. + +## Preconditions (first step) +- **git**: if the repo is under git, the commit rule is active. If not, skip + commits and note `Commit: N/A (no git)` in the diary. +- **clawmem**: check the index (`index_stats` / `status`). If not indexed, index + it first (`reindex`; add the project to `~/.config/clawmem/index.yml` if + needed), then continue. If indexing is impossible, mirror the durable summary + into the diary with `clawmem: N/A`. + +## 1. Prior-art search (before planning) +Search whether this was built or researched before: +- **clawmem**: `search` / `intent_search` / `find_similar` on the feature topic +- **git**: `git log --grep` and `git log -S` +- **tasks**: grep past diaries in `.tasks/` + +Record findings in the diary. Reuse what exists, or state why you build anew. + +## 2. Diary (`.tasks/`, gitignored) +Create `.tasks/YYYY-MM-DD-.md` first. One file per task. The agent only +creates and appends — it never deletes (the human cleans manually). + +Structure: +- **Plan**: checklist of tickets (T1, T2, …) +- **Prior art**: clawmem / git / tasks findings + conclusion +- **Per ticket on close**: what done, how, deviations from plan + why, research, + commit hash (if git), clawmem id + +## 3. Stages & commits +A **stage = an atomic, revertable unit** that can be described as one change. +Group tickets into a stage by this criterion, not by ticket count. + +On closing a stage → commit. The message is detailed and natural, the way a +person writes it: +- **what** changed (files / modules) +- **why** (the problem it solves) +- **how** (approach, key decisions) +- **deviations** from plan, and research if it shaped the decision + +The commit message MUST stay sterile: no task slug, no clawmem id, no mention of +`.tasks/`, clawmem, or this protocol, and no `Co-Authored-By` / "Generated with" +trailer. The commit is the only artifact that leaves the machine. + +## 4. clawmem (per stage + final) +- **Per stage**: a durable entry mirroring the commit content (what / how / + deviations / research) plus the commit hash. +- **On task completion**: pin a final summary (`memory_pin`) — outcome, key + decisions, pitfalls. This survives diary cleanup and is what the next task's + prior-art search finds. + +## 5. The linked graph (wiring lives on the private side only) +Join key = **commit hash** (a hash reveals nothing about the system). +- diary ticket stores: commit hash + clawmem id +- clawmem entry stores: commit hash + task slug +- commit stores: nothing pointing back + +From any node, reach the other two via the hash. The commit stays clean; the +working artifacts (slug, ids, diaries) never leave the machine. diff --git a/messages/en.json b/messages/en.json index 5391b72b..60f22a93 100644 --- a/messages/en.json +++ b/messages/en.json @@ -119,13 +119,8 @@ "networksValue": "1000", "ecosystems": "Ecosystems", "ecosystemsValue": "2345", - "tvl": "TVL", - "tvlValue": "$74.6B", - "dominance": "Dominance:", - "pow": "POW 56%", - "cosmos": "Cosmos 56%", - "eth": "ETH 56%", - "polkadot": "Polkadot 56%" + "totalFdv": "Total FDV", + "dominance": "Dominance:" }, "links": { "forum": "Forum", @@ -141,8 +136,14 @@ "infrastructureText": "Built and run on our own off-grid Atlantic bare-metal servers. Open-source. Ad-free. Privacy-respecting. By the Citizen Web3 team.", "askExpert": "Ask the Validator Expert", "topPerformers": "Top Performers & Highlights", - "placeholderName": "Name", - "topPerformersNote": "Short and visual overview of the top performers.", + "highlights": { + "stakeTop": "Top Validators by Stake", + "delegatorsTop": "Top Validators by Delegators", + "priceGainers": "Top Networks — 24h Price Gainers", + "aprTop": "Top Networks by APR", + "fdvTop": "Top Networks by FDV", + "uptimeTop": "Top Validators by Uptime" + }, "quickTools": { "bestValidator": "Best Validator for Me", "maxYield": "Max Yield Right Now", diff --git a/messages/pt.json b/messages/pt.json index 0c21654a..ba34bd10 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -119,13 +119,8 @@ "networksValue": "1000", "ecosystems": "Ecossistemas", "ecosystemsValue": "2345", - "tvl": "TVL", - "tvlValue": "$74.6B", - "dominance": "Domínio:", - "pow": "POW 56%", - "cosmos": "Cosmos 56%", - "eth": "ETH 56%", - "polkadot": "Polkadot 56%" + "totalFdv": "FDV Total", + "dominance": "Domínio:" }, "links": { "forum": "Forum", @@ -141,8 +136,14 @@ "infrastructureText": "Criado e operado em nossos próprios servidores bare-metal off-grid no Atlântico. Open-source. Sem anúncios. Respeita a privacidade. Pela equipe Citizen Web3.", "askExpert": "Pergunte ao Especialista Validator", "topPerformers": "Principais desempenhos e destaques", - "placeholderName": "Nome", - "topPerformersNote": "Visão curta e visual dos principais destaques.", + "highlights": { + "stakeTop": "Principais validadores por stake", + "delegatorsTop": "Principais validadores por delegadores", + "priceGainers": "Principais redes — maiores altas de 24h", + "aprTop": "Principais redes por APR", + "fdvTop": "Principais redes por FDV", + "uptimeTop": "Principais validadores por uptime" + }, "quickTools": { "bestValidator": "Melhor validador para mim", "maxYield": "Maior rendimento agora", diff --git a/messages/ru.json b/messages/ru.json index 50570d06..6654652b 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -119,13 +119,8 @@ "networksValue": "1000", "ecosystems": "Экосистемы", "ecosystemsValue": "2345", - "tvl": "TVL", - "tvlValue": "$74.6B", - "dominance": "Доминирование:", - "pow": "POW 56%", - "cosmos": "Cosmos 56%", - "eth": "ETH 56%", - "polkadot": "Polkadot 56%" + "totalFdv": "Общий FDV", + "dominance": "Доминирование:" }, "links": { "forum": "Forum", @@ -141,8 +136,14 @@ "infrastructureText": "Построено и работает на наших собственных off-grid bare-metal серверах в Атлантике. Open-source. Без рекламы. Уважает приватность. Команда Citizen Web3.", "askExpert": "Спросите эксперта Validator", "topPerformers": "Лучшие показатели и основные моменты", - "placeholderName": "Имя", - "topPerformersNote": "Короткий и визуальный обзор лучших результатов.", + "highlights": { + "stakeTop": "Топ валидаторов по стейку", + "delegatorsTop": "Топ валидаторов по делегаторам", + "priceGainers": "Топ сетей — рост цены за 24ч", + "aprTop": "Топ сетей по APR", + "fdvTop": "Топ сетей по FDV", + "uptimeTop": "Топ валидаторов по аптайму" + }, "quickTools": { "bestValidator": "Лучший валидатор для меня", "maxYield": "Максимальная доходность сейчас", diff --git a/src/app/[locale]/components/home/stats-table.tsx b/src/app/[locale]/components/home/stats-table.tsx index c837785e..eeb958de 100644 --- a/src/app/[locale]/components/home/stats-table.tsx +++ b/src/app/[locale]/components/home/stats-table.tsx @@ -3,15 +3,19 @@ import { getTranslations } from 'next-intl/server'; import ecosystemService from '@/services/ecosystem-service'; import HeaderInfoService from '@/services/headerInfo-service'; import MetricPair from '@/components/home/metic-pair'; +import formatCash from '@/utils/format-cash'; const StatsTable = async () => { const t = await getTranslations('HomePage'); - const [headerInfo, ecosystems] = await Promise.all([ + const [headerInfo, ecosystems, fdvInfo] = await Promise.all([ HeaderInfoService.getValidatorsAndChains(), ecosystemService.getAll(), + HeaderInfoService.getFdvAndDominance(), ]); + const totalFdvValue = fdvInfo.totalFdv > 0 ? `$${formatCash(fdvInfo.totalFdv)}` : '—'; + const metricPairs = [ { label: t('stats.validators'), @@ -35,8 +39,6 @@ const StatsTable = async () => { }, ]; - const dominanceItems = ['pow', 'cosmos', 'eth', 'polkadot'] as const; - return (
@@ -54,22 +56,24 @@ const StatsTable = async () => { className="tvl-page-jump mt-2 flex w-full flex-col bg-table_row text-left transition-all duration-75 hover:bg-bgHover 2xl:flex-row" >
- {t('stats.tvl')} + className="flex min-h-24 cursor-pointer items-center whitespace-nowrap border-b border-bgSt py-7 pl-7 pr-4 font-sfpro text-5xl sm:min-h-16 sm:py-5 sm:pl-5 sm:text-3xl md:min-h-16 md:py-5 md:pl-10 md:pr-3 md:text-lg 2xl:w-48 2xl:border-r"> + {t('stats.totalFdv')}
{t('stats.tvlValue')} -
- {t('stats.dominance')} - {dominanceItems.map((item) => ( - - {t(`stats.${item}`)} - - ))} -
+ className="font-handjet text-5xl sm:text-3xl md:text-lg hover:text-highlight">{totalFdvValue} + {fdvInfo.dominance.length > 0 && ( +
+ {t('stats.dominance')} + {fdvInfo.dominance.map((item) => ( + + {item.name} {item.share.toFixed(1)}% + + ))} +
+ )}
diff --git a/src/app/[locale]/components/home/top-performers.tsx b/src/app/[locale]/components/home/top-performers.tsx index 91b4bf2f..6a452882 100644 --- a/src/app/[locale]/components/home/top-performers.tsx +++ b/src/app/[locale]/components/home/top-performers.tsx @@ -1,28 +1,58 @@ +import Image from 'next/image'; +import Link from 'next/link'; import { getTranslations } from 'next-intl/server'; +import icons from '@/components/icons'; +import topPerformersService, { HighlightRow } from '@/services/top-performers-service'; + +const rowClassName = + 'flex h-20 shrink-0 items-center gap-5 border-t border-bgSt px-6 hover:bg-bgHover last:border-b last:border-bgSt sm:h-14 sm:gap-4 sm:px-4 md:h-10 md:gap-5 md:px-3.5'; + +const RowContent = ({ row }: { row: HighlightRow }) => ( + <> + {row.name} + + {row.name} + + + {row.value} + + +); + const TopPerformers = async () => { const t = await getTranslations('HomePage'); + const { titleKey, rows } = await topPerformersService.getDailyHighlights(); return ( -
-
+
+
-

{t('topPerformers')}

+

+ {t('topPerformers')} +

+

{t(`highlights.${titleKey}`)}

-
- {Array.from({ length: 15 }).map((_, index) => ( -
- - - {t('placeholderName')} - -
- ))} +
+ {rows.map((row, index) => + row.href ? ( + + + + ) : ( +
+ +
+ ), + )}
); diff --git a/src/app/services/headerInfo-service.ts b/src/app/services/headerInfo-service.ts index 8728e371..049ae6d3 100644 --- a/src/app/services/headerInfo-service.ts +++ b/src/app/services/headerInfo-service.ts @@ -1,12 +1,54 @@ import db from '@/db'; +import { FDV_EXCLUDED_CHAINS_SET } from '@/utils/fdv-excluded-chains'; const getValidatorsAndChains = async (): Promise<{ chains: number; validators: number }> => { const [chains, validators] = await Promise.all([db.chain.count(), db.validator.count()]); return { chains, validators }; }; +interface DominanceItem { + name: string; + share: number; +} + +// Total FDV = sum of Tokenomics.fdv across chains (FDV = totalSupply * price, computed by update-fdv job). +// Dominance = each ecosystem's FDV share of the total. Only ecosystems with real data (fdv > 0) are returned. +const getFdvAndDominance = async (): Promise<{ totalFdv: number; dominance: DominanceItem[] }> => { + const chains = await db.chain.findMany({ + select: { + name: true, + ecosystem: true, + chainEcosystem: { select: { prettyName: true } }, + tokenomics: { select: { fdv: true } }, + }, + }); + + let totalFdv = 0; + const ecosystemFdv = new Map(); + + for (const chain of chains) { + const fdv = FDV_EXCLUDED_CHAINS_SET.has(chain.name) ? 0 : chain.tokenomics?.fdv ?? 0; + if (fdv <= 0) continue; + + totalFdv += fdv; + const ecosystemName = chain.chainEcosystem?.prettyName ?? chain.ecosystem; + ecosystemFdv.set(ecosystemName, (ecosystemFdv.get(ecosystemName) ?? 0) + fdv); + } + + if (totalFdv === 0) { + return { totalFdv: 0, dominance: [] }; + } + + const dominance = Array.from(ecosystemFdv.entries()) + .map(([name, value]) => ({ name, share: (value / totalFdv) * 100 })) + .sort((a, b) => b.share - a.share); + + return { totalFdv, dominance }; +}; + const HeaderInfoService = { getValidatorsAndChains, + getFdvAndDominance, }; export default HeaderInfoService; diff --git a/src/app/services/top-performers-service.ts b/src/app/services/top-performers-service.ts new file mode 100644 index 00000000..96617b23 --- /dev/null +++ b/src/app/services/top-performers-service.ts @@ -0,0 +1,218 @@ +import { Prisma } from '@prisma/client'; +import { unstable_cache } from 'next/cache'; + +import db from '@/db'; +import cutHash from '@/utils/cut-hash'; +import { FDV_EXCLUDED_CHAINS } from '@/utils/fdv-excluded-chains'; +import formatCash from '@/utils/format-cash'; + +// Top Performers & Highlights — one theme per UTC day, rotating through a fixed pool. +// Variant A: every theme is computable cross-chain from current data (no new indexer infra). + +export interface HighlightRow { + logoUrl: string | null; + name: string; + value: string; + href: string; +} + +export type HighlightThemeKey = + | 'stakeTop' + | 'delegatorsTop' + | 'priceGainers' + | 'aprTop' + | 'fdvTop' + | 'uptimeTop'; + +interface Theme { + id: HighlightThemeKey; + titleKey: HighlightThemeKey; // resolved against HomePage.highlights in the component + fetch: () => Promise; +} + +const ROW_LIMIT = 15; + +const validatorHref = (validatorId: number | null, operatorAddress: string): string => + validatorId ? `/validators/${validatorId}/${operatorAddress}/validator_passport/authz/withdraw_rewards` : ''; + +const validatorName = (moniker: string, operatorAddress: string): string => + moniker && !moniker.startsWith('0x') ? moniker : cutHash({ value: operatorAddress, cutLength: 10 }); + +const networkHref = (name: string): string => `/networks/${name}/overview`; + +// --- Network themes (one Tokenomics numeric column, ranked in DB) --- + +type NetworkField = 'fdv' | 'apr' | 'changesPerDay'; + +const networkTheme = async ( + field: NetworkField, + format: (value: number) => string, + where: Prisma.ChainWhereInput, +): Promise => { + const chains = await db.chain.findMany({ + where, + orderBy: { tokenomics: { [field]: 'desc' } } as Prisma.ChainOrderByWithRelationInput, + take: ROW_LIMIT, + select: { + name: true, + prettyName: true, + logoUrl: true, + tokenomics: { select: { fdv: true, apr: true, changesPerDay: true } }, + }, + }); + + return chains.map((chain) => ({ + logoUrl: chain.logoUrl, + name: chain.prettyName, + value: format(chain.tokenomics?.[field] ?? 0), + href: networkHref(chain.name), + })); +}; + +const fdvTop = (): Promise => + networkTheme('fdv', (value) => `$${formatCash(value)}`, { + name: { notIn: FDV_EXCLUDED_CHAINS }, + tokenomics: { fdv: { gt: 0 } }, + }); + +const aprTop = (): Promise => + networkTheme('apr', (value) => `${(value * 100).toFixed(2)}%`, { tokenomics: { apr: { gt: 0 } } }); + +const priceGainers = (): Promise => + networkTheme('changesPerDay', (value) => `+${value.toFixed(2)}%`, { tokenomics: { changesPerDay: { gt: 0 } } }); + +// --- Validator themes (Node columns) --- + +const delegatorsTop = async (): Promise => { + const nodes = await db.node.findMany({ + where: { jailed: false, delegatorsAmount: { not: null } }, + orderBy: { delegatorsAmount: 'desc' }, + take: ROW_LIMIT, + select: { + moniker: true, + operatorAddress: true, + validatorId: true, + delegatorsAmount: true, + chain: { select: { logoUrl: true } }, + }, + }); + + return nodes.map((node) => ({ + logoUrl: node.chain.logoUrl, + name: validatorName(node.moniker, node.operatorAddress), + value: (node.delegatorsAmount ?? 0).toLocaleString('en-US'), + href: validatorHref(node.validatorId, node.operatorAddress), + })); +}; + +const uptimeTop = async (): Promise => { + const nodes = await db.node.findMany({ + where: { jailed: false, uptime: { not: null } }, + orderBy: { uptime: 'desc' }, + take: ROW_LIMIT, + select: { + moniker: true, + operatorAddress: true, + validatorId: true, + uptime: true, + chain: { select: { logoUrl: true } }, + }, + }); + + return nodes.map((node) => ({ + logoUrl: node.chain.logoUrl, + name: validatorName(node.moniker, node.operatorAddress), + value: `${(node.uptime ?? 0).toFixed(2)}%`, + href: validatorHref(node.validatorId, node.operatorAddress), + })); +}; + +// Stake in USD is comparable across chains: tokens / 10^decimals * latestPrice. +// tokens is a string and price is per-chain, so the ranking is computed in JS. This scans +// all non-jailed nodes — getDailyHighlights is cached per 5 min so the scan is not per-request. +const stakeTop = async (): Promise => { + // Newest price per chain: chainId must lead orderBy so `distinct` keeps the latest row. + const prices = await db.price.findMany({ + distinct: ['chainId'], + orderBy: [{ chainId: 'asc' }, { createdAt: 'desc' }], + select: { chainId: true, value: true }, + }); + const priceByChain = new Map(prices.map((price) => [price.chainId, price.value])); + + const nodes = await db.node.findMany({ + where: { jailed: false }, + select: { + moniker: true, + operatorAddress: true, + validatorId: true, + tokens: true, + chainId: true, + chain: { select: { logoUrl: true, params: { select: { coinDecimals: true } } } }, + }, + }); + + const ranked = nodes + .map((node) => { + const price = priceByChain.get(node.chainId); + const decimals = node.chain.params?.coinDecimals; + if (!price || price <= 0 || decimals == null) return null; + + const usd = (Number(node.tokens) / 10 ** decimals) * price; + if (!Number.isFinite(usd) || usd <= 0) return null; + + return { + usd, + row: { + logoUrl: node.chain.logoUrl, + name: validatorName(node.moniker, node.operatorAddress), + value: `$${formatCash(usd)}`, + href: validatorHref(node.validatorId, node.operatorAddress), + }, + }; + }) + .filter((entry): entry is NonNullable => entry !== null) + .sort((a, b) => b.usd - a.usd) + .slice(0, ROW_LIMIT); + + return ranked.map((entry) => entry.row); +}; + +const themes: Theme[] = [ + { id: 'stakeTop', titleKey: 'stakeTop', fetch: stakeTop }, + { id: 'delegatorsTop', titleKey: 'delegatorsTop', fetch: delegatorsTop }, + { id: 'priceGainers', titleKey: 'priceGainers', fetch: priceGainers }, + { id: 'aprTop', titleKey: 'aprTop', fetch: aprTop }, + { id: 'fdvTop', titleKey: 'fdvTop', fetch: fdvTop }, + { id: 'uptimeTop', titleKey: 'uptimeTop', fetch: uptimeTop }, +]; + +// TODO(TEST): revert to 86_400_000 (1 day) and revalidate 300 after manual testing. +// Currently 60_000 (1 min) so the rotation is visible on reload. +const ROTATION_PERIOD_MS = 60_000; +const CACHE_REVALIDATE_SEC = 15; + +// Deterministic per-period rotation. If the period's theme has no data, advance to the next +// non-empty one so the section is never empty. Cached briefly (caps the stakeTop full-table scan). +const getDailyHighlights = unstable_cache( + async (): Promise<{ titleKey: HighlightThemeKey; rows: HighlightRow[] }> => { + const periodIndex = Math.floor(Date.now() / ROTATION_PERIOD_MS); + + for (let offset = 0; offset < themes.length; offset++) { + const theme = themes[(periodIndex + offset) % themes.length]; + const rows = await theme.fetch(); + if (rows.length > 0) { + return { titleKey: theme.titleKey, rows }; + } + } + + return { titleKey: themes[periodIndex % themes.length].titleKey, rows: [] }; + }, + ['top-performers-daily-highlights'], + { revalidate: CACHE_REVALIDATE_SEC }, +); + +const topPerformersService = { + getDailyHighlights, +}; + +export default topPerformersService; diff --git a/src/utils/fdv-excluded-chains.ts b/src/utils/fdv-excluded-chains.ts new file mode 100644 index 00000000..034c7792 --- /dev/null +++ b/src/utils/fdv-excluded-chains.ts @@ -0,0 +1,5 @@ +// Testnets whose FDV is bogus and must be excluded from FDV sums / rankings. +// Single source of truth — mirrors the inline exclusion in networks-list-item.tsx. +export const FDV_EXCLUDED_CHAINS = ['ethereum-sepolia', 'warden-testnet']; + +export const FDV_EXCLUDED_CHAINS_SET = new Set(FDV_EXCLUDED_CHAINS);