From dfa4ca57b2808abcb082095927bbb3375c3d2d2e Mon Sep 17 00:00:00 2001 From: pietro-maximoff Date: Mon, 17 Nov 2025 10:28:10 +0100 Subject: [PATCH 1/4] SOV-5233: update rsk tvl --- .../components/ProtocolData/ProtocolData.tsx | 249 ++++++++++++++---- .../ProtocolData/hooks/useGetBOBVolume.ts | 57 ++-- .../ProtocolData/hooks/useGetLockedData.ts | 78 +++++- apps/frontend/src/utils/helpers.ts | 12 +- 4 files changed, 319 insertions(+), 77 deletions(-) diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.tsx b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.tsx index 8399b1bf7..e838b52c2 100644 --- a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.tsx +++ b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.tsx @@ -8,8 +8,11 @@ import { Accordion, Button, ButtonStyle } from '@sovryn/ui'; import { BOB_CHAIN_ID, RSK_CHAIN_ID } from '../../../../../config/chains'; import { AmountRenderer } from '../../../../2_molecules/AmountRenderer/AmountRenderer'; -import { NativeTokenAmount } from '../../../../2_molecules/NativeTokenAmount/NativeTokenAmount'; -import { USD } from '../../../../../constants/currencies'; +import { + BITCOIN, + BTC_RENDER_PRECISION, + USD, +} from '../../../../../constants/currencies'; import { translations } from '../../../../../locales/i18n'; import { decimalic } from '../../../../../utils/math'; import { USD_VALUE_PRECISION } from './ProtocolData.constants'; @@ -17,50 +20,153 @@ import styles from './ProtocolData.module.css'; import { useGetBOBVolume } from './hooks/useGetBOBVolume'; import { useGetLockedData } from './hooks/useGetLockedData'; import { useGetRSKVolume } from './hooks/useGetRSKVolume'; +import { useGetTokens } from './hooks/useGetTokens'; const pageTranslations = translations.landingPage.protocolDataSection; +/** Reject non-finite or absurd USD values; return "0" if invalid. */ +const sanitizeUsd = (raw: unknown, cap = 1e12) => { + const n = Number(raw ?? 0); + if (!isFinite(n) || n < 0 || n > cap) return '0'; + return String(n); +}; + +/** Pick a reasonable BTC/USD from token lists; validate rough range. */ +function pickBtcUsd( + tokens: Array<{ symbol?: string; usdPrice?: string | number }> = [], + symbols: string[], +): number | undefined { + for (const s of symbols) { + const tok = tokens.find( + t => + t?.symbol?.toUpperCase() === s.toUpperCase() && Number(t.usdPrice) > 0, + ); + const px = Number(tok?.usdPrice); + if (isFinite(px) && px >= 1_000 && px <= 200_000) return px; // wide but sane + } + return undefined; +} + export const ProtocolData: FC = () => { - const lockedData = useGetLockedData(RSK_CHAIN_ID); - const rskVolume = useGetRSKVolume(); + // --- Raw data hooks --------------------------------------------------------- + // RSK data (TVL parts + 24h volume) + const rskLocked = useGetLockedData(RSK_CHAIN_ID); + console.log(rskLocked); + + const rskVolumeRaw = useGetRSKVolume(); // 24h USD (string) + + // BOB data (TVL aggregate + 24h volume) + const bobLocked = useGetLockedData(BOB_CHAIN_ID); + console.log('bob locked', bobLocked); + + const bobVolumeRaw = useGetBOBVolume(); // 24h USD (string) - const bobLockedData = useGetLockedData(BOB_CHAIN_ID); - const bobVolume = useGetBOBVolume(); + // Tokens for BTC price sourcing + const { value: rskTokens } = useGetTokens(RSK_CHAIN_ID); + const { value: bobTokens } = useGetTokens(BOB_CHAIN_ID); const navigate = useNavigate(); const [open, toggle] = useReducer(v => !v, false); - const handleClick = useCallback(() => navigate('/stats'), [navigate]); - const rskTVL = useMemo( + // --- BTC/USD sources -------------------------------------------------------- + // Prefer RSK RBTC/WRBTC for stability; fallback to BOB BTC wrappers. + const rskBtcUsd = useMemo( + () => pickBtcUsd(rskTokens, ['RBTC', 'WRBTC', 'BTC']) ?? 0, + [rskTokens], + ); + const bobBtcUsd = useMemo( () => - decimalic(lockedData.tvlAmm?.totalUsd || '0') - .add(lockedData.tvlLending?.totalUsd || '0') - .add(lockedData.tvlMynt?.totalUsd || '0') - .add(lockedData.tvlProtocol?.totalUsd || '0') - .add(lockedData.tvlStaking?.totalUsd || '0') - .add(lockedData.tvlSubprotocols?.totalUsd || '0') - .add(lockedData.tvlZero?.totalUsd || '0') + pickBtcUsd(bobTokens, [ + 'WBTC', + 'tBTC', + 'UniBTC', + 'XSOLVBTC', + 'SolvBTC', + 'BTC', + ]) ?? 0, + [bobTokens], + ); + + // Use ONE global BTC/USD for totals (avoid double-scaling differences). + const globalBtcUsd = useMemo( + () => rskBtcUsd || bobBtcUsd || 0, + [rskBtcUsd, bobBtcUsd], + ); + + // --- RSK TVL (USD) aggregated from sub-buckets ------------------------------ + const rskTvlUsd = useMemo( + () => + decimalic(rskLocked?.tvlAmm?.totalUsd || 0) + .add(rskLocked?.tvlLending?.totalUsd || 0) + .add(rskLocked?.tvlMynt?.totalUsd || 0) + .add(rskLocked?.tvlProtocol?.totalUsd || 0) + .add(rskLocked?.tvlStaking?.totalUsd || 0) + .add(rskLocked?.tvlSubprotocols?.totalUsd || 0) + .add(rskLocked?.tvlZero?.totalUsd || 0) .toString(), - [ - lockedData.tvlAmm?.totalUsd, - lockedData.tvlLending?.totalUsd, - lockedData.tvlMynt?.totalUsd, - lockedData.tvlProtocol?.totalUsd, - lockedData.tvlStaking?.totalUsd, - lockedData.tvlSubprotocols?.totalUsd, - lockedData.tvlZero?.totalUsd, - ], + [rskLocked], + ); + + // --- BOB TVL (USD) from backend (already recomputed/clamped in hook) ------- + const bobTvlUsd = useMemo(() => { + // accept either total_usd or totalUsd from the hook + const raw = bobLocked.total_usd; + return sanitizeUsd(raw, 1e12); + }, [bobLocked]); + + // --- Per-chain TVL in BTC --------------------------------------------------- + const rskTvlBtc = useMemo( + () => (rskBtcUsd ? decimalic(rskTvlUsd).div(rskBtcUsd).toString() : '0'), + [rskTvlUsd, rskBtcUsd], + ); + const bobTvlBtc = useMemo( + () => (bobBtcUsd ? decimalic(bobTvlUsd).div(bobBtcUsd).toString() : '0'), + [bobTvlUsd, bobBtcUsd], + ); + + // --- 24h volumes (USD sanitized) ------------------------------------------- + const rskVolumeUsd = useMemo( + () => sanitizeUsd(rskVolumeRaw, 1e12), + [rskVolumeRaw], + ); + const bobVolumeUsd = useMemo( + () => sanitizeUsd(bobVolumeRaw, 1e12), + [bobVolumeRaw], + ); + + // --- Per-chain 24h volume in BTC ------------------------------------------- + const rskVolumeBtc = useMemo( + () => (rskBtcUsd ? decimalic(rskVolumeUsd).div(rskBtcUsd).toString() : '0'), + [rskVolumeUsd, rskBtcUsd], + ); + const bobVolumeBtc = useMemo( + () => (bobBtcUsd ? decimalic(bobVolumeUsd).div(bobBtcUsd).toString() : '0'), + [bobVolumeUsd, bobBtcUsd], ); - const total = useMemo(() => { - return { - lockedData: - Number(rskTVL || '0') + Number(bobLockedData.total_usd || '0'), - volumeData: Number(rskVolume || '0') + Number(bobVolume || '0'), - }; - }, [bobLockedData.total_usd, bobVolume, rskTVL, rskVolume]); + // --- Totals (USD) and convert once with global BTC/USD ---------------------- + const totalTvlUsd = useMemo( + () => decimalic(rskTvlUsd).add(bobTvlUsd).toString(), + [rskTvlUsd, bobTvlUsd], + ); + const totalVolUsd = useMemo( + () => decimalic(rskVolumeUsd).add(bobVolumeUsd).toString(), + [rskVolumeUsd, bobVolumeUsd], + ); + + const totalTvlBtc = useMemo( + () => + globalBtcUsd ? decimalic(totalTvlUsd).div(globalBtcUsd).toString() : '0', + [totalTvlUsd, globalBtcUsd], + ); + const totalVolBtc = useMemo( + () => + globalBtcUsd ? decimalic(totalVolUsd).div(globalBtcUsd).toString() : '0', + [totalVolUsd, globalBtcUsd], + ); + // --- Render ----------------------------------------------------------------- return (
@@ -79,41 +185,50 @@ export const ProtocolData: FC = () => { + {/* TOTAL TVL (All Networks) — BTC + USD (one BTC/USD) */}
{t(pageTranslations.totalValueLockedAllNetworks)}
+ + {/* BTC total */}
-
+ {/* USD total */}
+ {/* TOTAL 24H VOLUME (All Networks) — BTC + USD (one BTC/USD) */}
{t(pageTranslations.volumeAllNetworks)}
-
- +
+ {/* 24h volume in USD */}
@@ -123,18 +238,26 @@ export const ProtocolData: FC = () => { } children={
+ {/* RSK ROW */}
{t(pageTranslations.tvlRskNetwork)}
+ + {/* TVL (RSK) in BTC */}
- +
+ {/* TVL (RSK) in USD */}
@@ -145,16 +268,20 @@ export const ProtocolData: FC = () => {
{t(pageTranslations.volumeRskNetwork)}
+ + {/* 24h Volume (RSK) in BTC */}
-
+ {/* 24h Volume (RSK) in USD */}
@@ -162,20 +289,26 @@ export const ProtocolData: FC = () => {
+ {/* BOB ROW */}
{t(pageTranslations.tvlBobNetwork)}
-
- +
+ + {/* TVL (BOB) in USD */}
@@ -186,16 +319,20 @@ export const ProtocolData: FC = () => {
{t(pageTranslations.volumeBobNetwork)}
+ + {/* 24h Volume (BOB) in BTC */}
-
+ {/* 24h Volume (BOB) in USD */}
diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetBOBVolume.ts b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetBOBVolume.ts index e9ffa967b..29b11f4c7 100644 --- a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetBOBVolume.ts +++ b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetBOBVolume.ts @@ -1,3 +1,4 @@ +// useGetBOBVolume.ts import { useMemo } from 'react'; import axios from 'axios'; @@ -15,21 +16,30 @@ import { decimalic } from '../../../../../../utils/math'; import { useGetTokens } from './useGetTokens'; const indexer = getIndexerUrl(); -console.log(indexer + 'sdex/volume'); + +// optional hard overrides if token list is wrong +const DECIMALS_OVERRIDE: Record = { + // lowercased addresses + // SOV + '0xba20a5e63eeefffa6fd365e7e540628f8fc61474': 18, + // WBTC + '0x0555e30da8f98308edb960aa94c0db47230d2b9c': 8, + // add others here if the indexer list is wrong +}; + +const isSanePrice = (p: number) => isFinite(p) && p > 0 && p < 200_000; export const useGetBOBVolume = () => { const { currentChainId } = useChainStore(); const { value: tokens } = useGetTokens(BOB_CHAIN_ID); + const { value: volumes } = useCacheCall( `sdex/volume`, BOB_CHAIN_ID, async () => { const { data } = await axios.get(indexer + 'sdex/volume', { - params: { - chainId: Number(BOB_CHAIN_ID), - }, + params: { chainId: Number(BOB_CHAIN_ID) }, }); - return (data?.data || []) as { token: string; volume: string }[]; }, [currentChainId], @@ -37,24 +47,35 @@ export const useGetBOBVolume = () => { ); return useMemo(() => { - if (!tokens.length || !volumes.length) { - return '0'; - } + if (!tokens.length || !volumes.length) return '0'; let sum = decimalic(0); - volumes.forEach(volumeData => { - const token = tokens.find(t => - areAddressesEqual(t.address, volumeData.token), - ); + for (const row of volumes) { + const token = tokens.find(t => areAddressesEqual(t.address, row.token)); + if (!token) continue; - if (token) { - const volume = decimalic(volumeData.volume).toString(); - sum = sum.add( - decimalic(token.usdPrice).mul(formatUnits(volume, token.decimals)), - ); + const addr = token.address.toLowerCase(); + const decimals = DECIMALS_OVERRIDE[addr] ?? Number(token.decimals ?? 18); + const price = Number(token.usdPrice); + + // drop broken feeds like POWA ~ 1e-7, or NaN/negative + if (!isSanePrice(price)) continue; + + // raw volume is in base units; normalize with decimals + let units = '0'; + try { + units = formatUnits(row.volume, decimals); + } catch { + continue; } - }); + + const usd = decimalic(price).mul(units); + // drop absurd contributions (protect against single bad token) + if (Number(usd) > 1e12 || !isFinite(Number(usd))) continue; + + sum = sum.add(usd); + } return sum.toString(); }, [tokens, volumes]); diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetLockedData.ts b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetLockedData.ts index 0d818edf7..2a000ae9e 100644 --- a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetLockedData.ts +++ b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetLockedData.ts @@ -4,9 +4,12 @@ import axios, { Canceler } from 'axios'; import { ChainId } from '@sovryn/ethers-provider'; +import { BOB_CHAIN_ID } from '../../../../../../config/chains'; + import { DATA_REFRESH_INTERVAL } from '../../../../../../constants/general'; import { useChainStore } from '../../../../../../hooks/useChainStore'; import { useInterval } from '../../../../../../hooks/useInterval'; +import { decimalic } from '../../../../../../utils/math'; import { DEFAULT_LOCKED_DATA, LOCKED_DATA_URL, @@ -33,9 +36,80 @@ export const useGetLockedData = (chainId?: ChainId) => { cancelToken, }) .then(result => { - if (result.data.data) { - setLockedData(result.data.data); + const data = result?.data?.data; + if (!data) return; + + // --- BOB sanitizer: recompute total from parts; drop insane entries --- + if (Number(chainId || currentChainId) === Number(BOB_CHAIN_ID)) { + // BTC-ish symbols where we validate unit price hard + const CLAMP_BTC_SYMBOLS = new Set([ + 'WBTC', + 'WBTC.OLD', + 'UniBTC', + 'tBTC', + 'XSOLVBTC', + 'SolvBTC', + 'BTC', + 'RBTC', + 'WRBTC', + ]); + + const MAX_UNIT_PRICE_USD = 200_000; // very generous BTC cap + const MAX_ASSET_USD = 1e9; // per-asset USD clamp (1B) + + let safeTotal = decimalic(0); + + // Only sum per-asset; ignore bucket totalUsd fields + const buckets = [ + 'tvlSdex', + 'tvlLending', + 'tvlProtocol', + 'tvlStaking', + 'tvlSubprotocols', + 'tvlAmm', + 'tvlZero', + 'tvlMynt', + ]; + + for (const key of buckets) { + const bucket = (data as any)[key]; + if (!bucket || typeof bucket !== 'object') continue; + + for (const [assetKey, assetVal] of Object.entries(bucket)) { + // skip bucket-level totals like "totalUsd" + if (assetKey === 'totalUsd' || assetKey === 'total_usd') continue; + + const v: any = assetVal; + if (!v || typeof v !== 'object') continue; + + const sym = String(v.assetName || assetKey || '').toUpperCase(); + const balance = Number(v.balance ?? v.amount ?? 0); + let usd = Number(v.balanceUsd ?? v.totalUsd ?? v.usd ?? 0); + + if (!Number.isFinite(usd) || usd <= 0) continue; + if (usd > MAX_ASSET_USD) continue; + + // For BTC-like, validate unit price + if ( + CLAMP_BTC_SYMBOLS.has(sym) && + Number.isFinite(balance) && + balance > 0 + ) { + const unit = usd / balance; + if (!Number.isFinite(unit) || unit > MAX_UNIT_PRICE_USD) + continue; + } + + safeTotal = safeTotal.add(usd); + } + } + + // Write the sanitized totals back so the UI necessarily picks them up + (data as any).total_usd = safeTotal.toString(); + (data as any).totalUsd = (data as any).total_usd; } + + setLockedData(data); }) .catch(() => {}); }, [chainId, currentChainId]); diff --git a/apps/frontend/src/utils/helpers.ts b/apps/frontend/src/utils/helpers.ts index 31f6ab352..a9abd5102 100644 --- a/apps/frontend/src/utils/helpers.ts +++ b/apps/frontend/src/utils/helpers.ts @@ -163,7 +163,7 @@ export const composeGas = (priceInGwei: Decimalish, limitInWei: Decimalish) => .div(10 ** 18); export const isMobileDevice = () => { - const config = resolveConfig(tailwindConfig); + const config = resolveConfig(tailwindConfig as any); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const widthToCheck: string = config?.theme?.screens.md; // value will be in format "768px" @@ -222,3 +222,13 @@ export const scrollToElement = (ref: RefObject) => { }); } }; + +export function sanitizeUsd(v: unknown, cap = 1e9): number { + const n = Number(v); + if (!Number.isFinite(n) || n < 0 || n > cap) return 0; + return n; +} + +export function safeAddUsd(a: unknown, b: unknown, cap = 1e9): string { + return String(sanitizeUsd(a, cap) + sanitizeUsd(b, cap)); +} From 1aa1bed549837393c643a936b0bf955d92aa6cc8 Mon Sep 17 00:00:00 2001 From: pietro-maximoff Date: Wed, 19 Nov 2025 17:39:17 +0100 Subject: [PATCH 2/4] chore: update tvl bob rsk --- .../components/ProtocolData/ProtocolData.tsx | 97 +++++++++---------- .../ProtocolData/ProtocolData.utils.tsx | 37 +++++++ .../ProtocolData/hooks/useGetBOBVolume.ts | 24 +++-- .../ProtocolData/hooks/useGetLockedData.ts | 2 +- .../ProtocolData/hooks/useGetRSKVolume.ts | 6 +- 5 files changed, 105 insertions(+), 61 deletions(-) create mode 100644 apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.utils.tsx diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.tsx b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.tsx index e838b52c2..2d5d1903d 100644 --- a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.tsx +++ b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.tsx @@ -17,6 +17,7 @@ import { translations } from '../../../../../locales/i18n'; import { decimalic } from '../../../../../utils/math'; import { USD_VALUE_PRECISION } from './ProtocolData.constants'; import styles from './ProtocolData.module.css'; +import { pickBtcUsd, safeBucketUsd, sanitizeUsd } from './ProtocolData.utils'; import { useGetBOBVolume } from './hooks/useGetBOBVolume'; import { useGetLockedData } from './hooks/useGetLockedData'; import { useGetRSKVolume } from './hooks/useGetRSKVolume'; @@ -24,40 +25,14 @@ import { useGetTokens } from './hooks/useGetTokens'; const pageTranslations = translations.landingPage.protocolDataSection; -/** Reject non-finite or absurd USD values; return "0" if invalid. */ -const sanitizeUsd = (raw: unknown, cap = 1e12) => { - const n = Number(raw ?? 0); - if (!isFinite(n) || n < 0 || n > cap) return '0'; - return String(n); -}; - -/** Pick a reasonable BTC/USD from token lists; validate rough range. */ -function pickBtcUsd( - tokens: Array<{ symbol?: string; usdPrice?: string | number }> = [], - symbols: string[], -): number | undefined { - for (const s of symbols) { - const tok = tokens.find( - t => - t?.symbol?.toUpperCase() === s.toUpperCase() && Number(t.usdPrice) > 0, - ); - const px = Number(tok?.usdPrice); - if (isFinite(px) && px >= 1_000 && px <= 200_000) return px; // wide but sane - } - return undefined; -} - export const ProtocolData: FC = () => { - // --- Raw data hooks --------------------------------------------------------- // RSK data (TVL parts + 24h volume) const rskLocked = useGetLockedData(RSK_CHAIN_ID); - console.log(rskLocked); const rskVolumeRaw = useGetRSKVolume(); // 24h USD (string) // BOB data (TVL aggregate + 24h volume) const bobLocked = useGetLockedData(BOB_CHAIN_ID); - console.log('bob locked', bobLocked); const bobVolumeRaw = useGetBOBVolume(); // 24h USD (string) @@ -69,7 +44,6 @@ export const ProtocolData: FC = () => { const [open, toggle] = useReducer(v => !v, false); const handleClick = useCallback(() => navigate('/stats'), [navigate]); - // --- BTC/USD sources -------------------------------------------------------- // Prefer RSK RBTC/WRBTC for stability; fallback to BOB BTC wrappers. const rskBtcUsd = useMemo( () => pickBtcUsd(rskTokens, ['RBTC', 'WRBTC', 'BTC']) ?? 0, @@ -94,28 +68,57 @@ export const ProtocolData: FC = () => { [rskBtcUsd, bobBtcUsd], ); - // --- RSK TVL (USD) aggregated from sub-buckets ------------------------------ - const rskTvlUsd = useMemo( - () => - decimalic(rskLocked?.tvlAmm?.totalUsd || 0) - .add(rskLocked?.tvlLending?.totalUsd || 0) - .add(rskLocked?.tvlMynt?.totalUsd || 0) - .add(rskLocked?.tvlProtocol?.totalUsd || 0) - .add(rskLocked?.tvlStaking?.totalUsd || 0) - .add(rskLocked?.tvlSubprotocols?.totalUsd || 0) - .add(rskLocked?.tvlZero?.totalUsd || 0) - .toString(), - [rskLocked], - ); + const rskTvlUsd = useMemo(() => { + if (!rskLocked) { + return '0'; + } + + // Aggregate from buckets, applying numeric caps per bucket + const fromBuckets = decimalic(0) + .add(safeBucketUsd(rskLocked.tvlAmm)) + .add(safeBucketUsd(rskLocked.tvlLending)) + .add(safeBucketUsd(rskLocked.tvlMynt)) + .add(safeBucketUsd(rskLocked.tvlProtocol)) + .add(safeBucketUsd(rskLocked.tvlStaking)) + .add(safeBucketUsd(rskLocked.tvlSubprotocols)) + .add(safeBucketUsd(rskLocked.tvlZero)); + + //sanity-check against raw total_usd/totalUsd from backend + const rawTotal = Number( + (rskLocked as any)?.total_usd ?? (rskLocked as any)?.totalUsd ?? 0, + ); + const safeRawTotal = + !isFinite(rawTotal) || rawTotal < 0 || rawTotal > 1e9 ? 0 : rawTotal; + + if (safeRawTotal > 0) { + const diff = Math.abs(fromBuckets.toNumber() - safeRawTotal); + const relDiff = diff / safeRawTotal; + + // Log if discrepancy > ~3% (indexer vs graph-wrapper) + if (relDiff > 0.03) { + // eslint-disable-next-line no-console + console.warn('[ProtocolData] RSK TVL mismatch', { + buckets: fromBuckets.toString(), + total_usd: safeRawTotal.toString(), + relDiff, + }); + } + } + + return fromBuckets.toString(); + }, [rskLocked]); - // --- BOB TVL (USD) from backend (already recomputed/clamped in hook) ------- const bobTvlUsd = useMemo(() => { - // accept either total_usd or totalUsd from the hook - const raw = bobLocked.total_usd; - return sanitizeUsd(raw, 1e12); + if (!bobLocked) { + return '0'; + } + + const raw = + (bobLocked as any)?.total_usd ?? (bobLocked as any)?.totalUsd ?? 0; + + return sanitizeUsd(raw, 1e9); }, [bobLocked]); - // --- Per-chain TVL in BTC --------------------------------------------------- const rskTvlBtc = useMemo( () => (rskBtcUsd ? decimalic(rskTvlUsd).div(rskBtcUsd).toString() : '0'), [rskTvlUsd, rskBtcUsd], @@ -125,7 +128,6 @@ export const ProtocolData: FC = () => { [bobTvlUsd, bobBtcUsd], ); - // --- 24h volumes (USD sanitized) ------------------------------------------- const rskVolumeUsd = useMemo( () => sanitizeUsd(rskVolumeRaw, 1e12), [rskVolumeRaw], @@ -135,7 +137,6 @@ export const ProtocolData: FC = () => { [bobVolumeRaw], ); - // --- Per-chain 24h volume in BTC ------------------------------------------- const rskVolumeBtc = useMemo( () => (rskBtcUsd ? decimalic(rskVolumeUsd).div(rskBtcUsd).toString() : '0'), [rskVolumeUsd, rskBtcUsd], @@ -145,7 +146,6 @@ export const ProtocolData: FC = () => { [bobVolumeUsd, bobBtcUsd], ); - // --- Totals (USD) and convert once with global BTC/USD ---------------------- const totalTvlUsd = useMemo( () => decimalic(rskTvlUsd).add(bobTvlUsd).toString(), [rskTvlUsd, bobTvlUsd], @@ -166,7 +166,6 @@ export const ProtocolData: FC = () => { [totalVolUsd, globalBtcUsd], ); - // --- Render ----------------------------------------------------------------- return (
diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.utils.tsx b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.utils.tsx new file mode 100644 index 000000000..c80fcd15b --- /dev/null +++ b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/ProtocolData.utils.tsx @@ -0,0 +1,37 @@ +import { decimalic } from '../../../../../utils/math'; + +/** Reject non-finite or absurd USD values; return "0" if invalid. */ +export const sanitizeUsd = (raw: unknown, cap = 1e12) => { + const n = Number(raw ?? 0); + if (!isFinite(n) || n < 0 || n > cap) return '0'; + return String(n); +}; + +/** Numeric guard for TVL buckets (RSK/BOB). Values > cap are treated as 0. */ +export const sanitizeTvlBucket = (raw: unknown, cap = 1e9): string => { + const n = Number(raw ?? 0); + if (!isFinite(n) || n < 0 || n > cap) return '0'; + return String(n); +}; + +/** Safely convert TVL bucket totalUsd -> decimalic with cap. */ +export const safeBucketUsd = ( + bucket?: { totalUsd?: string | number }, + cap = 1e9, +) => decimalic(sanitizeTvlBucket(bucket?.totalUsd, cap)); + +/** Pick a reasonable BTC/USD from token lists; validate rough range. */ +export const pickBtcUsd = ( + tokens: Array<{ symbol?: string; usdPrice?: string | number }> = [], + symbols: string[], +): number | undefined => { + for (const s of symbols) { + const tok = tokens.find( + t => + t?.symbol?.toUpperCase() === s.toUpperCase() && Number(t.usdPrice) > 0, + ); + const px = Number(tok?.usdPrice); + if (isFinite(px) && px >= 1_000 && px <= 200_000) return px; // wide but sane + } + return undefined; +}; diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetBOBVolume.ts b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetBOBVolume.ts index 29b11f4c7..8db0e3d6d 100644 --- a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetBOBVolume.ts +++ b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetBOBVolume.ts @@ -1,4 +1,3 @@ -// useGetBOBVolume.ts import { useMemo } from 'react'; import axios from 'axios'; @@ -17,17 +16,19 @@ import { useGetTokens } from './useGetTokens'; const indexer = getIndexerUrl(); -// optional hard overrides if token list is wrong +// hard overrides if token list is wrong const DECIMALS_OVERRIDE: Record = { - // lowercased addresses // SOV '0xba20a5e63eeefffa6fd365e7e540628f8fc61474': 18, // WBTC '0x0555e30da8f98308edb960aa94c0db47230d2b9c': 8, - // add others here if the indexer list is wrong + // POWA + '0xd0c2f08a873186db5cfb7b767db62bef9e495bff': 18, }; -const isSanePrice = (p: number) => isFinite(p) && p > 0 && p < 200_000; +// Treat ultra-small prices as broken (e.g. POWA mispriced at ~1e-7 USD), +// and cap at a generous upper bound for safety. +const isSanePrice = (p: number) => isFinite(p) && p >= 1e-5 && p < 200_000; export const useGetBOBVolume = () => { const { currentChainId } = useChainStore(); @@ -59,7 +60,12 @@ export const useGetBOBVolume = () => { const decimals = DECIMALS_OVERRIDE[addr] ?? Number(token.decimals ?? 18); const price = Number(token.usdPrice); - // drop broken feeds like POWA ~ 1e-7, or NaN/negative + // skip insane decimals + if (!Number.isInteger(decimals) || decimals < 0 || decimals > 36) { + continue; + } + + // drop broken feeds like POWA ~ 1e-7, NaN, negative, etc. if (!isSanePrice(price)) continue; // raw volume is in base units; normalize with decimals @@ -71,8 +77,10 @@ export const useGetBOBVolume = () => { } const usd = decimalic(price).mul(units); - // drop absurd contributions (protect against single bad token) - if (Number(usd) > 1e12 || !isFinite(Number(usd))) continue; + + // drop absurd contributions (protect against a single bad token dominating) + const usdNum = Number(usd); + if (!isFinite(usdNum) || usdNum > 1e12) continue; sum = sum.add(usd); } diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetLockedData.ts b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetLockedData.ts index 2a000ae9e..a6f7b120d 100644 --- a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetLockedData.ts +++ b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetLockedData.ts @@ -39,7 +39,7 @@ export const useGetLockedData = (chainId?: ChainId) => { const data = result?.data?.data; if (!data) return; - // --- BOB sanitizer: recompute total from parts; drop insane entries --- + // BOB sanitizer: recompute total from parts; drop insane entries if (Number(chainId || currentChainId) === Number(BOB_CHAIN_ID)) { // BTC-ish symbols where we validate unit price hard const CLAMP_BTC_SYMBOLS = new Set([ diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetRSKVolume.ts b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetRSKVolume.ts index b62aea974..136323873 100644 --- a/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetRSKVolume.ts +++ b/apps/frontend/src/app/5_pages/LandingPage/components/ProtocolData/hooks/useGetRSKVolume.ts @@ -36,11 +36,11 @@ export const useGetRSKVolume = () => { }, cancelToken, }) - .then(result => { + .then(result => setVolumeData({ usd: result.data?.data?.total_volume_usd || 0, - }); - }) + }), + ) .catch(() => {}); }, []); From 9007298ccd8608e63a9ae24b833bbf006ad548e3 Mon Sep 17 00:00:00 2001 From: pietro-maximoff Date: Wed, 19 Nov 2025 17:46:46 +0100 Subject: [PATCH 3/4] chore: cleaunp --- apps/frontend/src/utils/helpers.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/frontend/src/utils/helpers.ts b/apps/frontend/src/utils/helpers.ts index a9abd5102..c08132e35 100644 --- a/apps/frontend/src/utils/helpers.ts +++ b/apps/frontend/src/utils/helpers.ts @@ -222,13 +222,3 @@ export const scrollToElement = (ref: RefObject) => { }); } }; - -export function sanitizeUsd(v: unknown, cap = 1e9): number { - const n = Number(v); - if (!Number.isFinite(n) || n < 0 || n > cap) return 0; - return n; -} - -export function safeAddUsd(a: unknown, b: unknown, cap = 1e9): string { - return String(sanitizeUsd(a, cap) + sanitizeUsd(b, cap)); -} From b0f62672f1cfed18a1c13f57a08024b4fe7210e8 Mon Sep 17 00:00:00 2001 From: Pietro <74987028+pietro-maximoff@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:31:58 +0100 Subject: [PATCH 4/4] Create smart-countries-grow.md --- .changeset/smart-countries-grow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/smart-countries-grow.md diff --git a/.changeset/smart-countries-grow.md b/.changeset/smart-countries-grow.md new file mode 100644 index 000000000..04298e789 --- /dev/null +++ b/.changeset/smart-countries-grow.md @@ -0,0 +1,5 @@ +--- +"frontend": patch +--- + +Fix Protocol Data