diff --git a/src/app/components/Delegations/Delegations.tsx b/src/app/components/Delegations/Delegations.tsx index ec63ad41..b2499d61 100644 --- a/src/app/components/Delegations/Delegations.tsx +++ b/src/app/components/Delegations/Delegations.tsx @@ -11,7 +11,7 @@ import { LoadingTableList } from "@/app/components/Loading/Loading"; import { DelegationsPointsProvider } from "@/app/context/api/DelegationsPointsProvider"; import { useError } from "@/app/context/Error/ErrorContext"; import { useWallet } from "@/app/context/wallet/WalletProvider"; -import { useDelegations } from "@/app/hooks/useDelegations"; +import { useDelegations } from "@/app/hooks/api/useDelegations"; import { useHealthCheck } from "@/app/hooks/useHealthCheck"; import { useAppState } from "@/app/state"; import { useDelegationState } from "@/app/state/DelegationState"; diff --git a/src/app/components/Modals/UnbondWithdrawModal.tsx b/src/app/components/Modals/UnbondWithdrawModal.tsx index e4069f03..9f3e65b3 100644 --- a/src/app/components/Modals/UnbondWithdrawModal.tsx +++ b/src/app/components/Modals/UnbondWithdrawModal.tsx @@ -1,6 +1,6 @@ import { IoMdClose } from "react-icons/io"; -import { useVersionByHeight } from "@/app/hooks/useVersions"; +import { useVersionByHeight } from "@/app/hooks/useVersionByHeight"; import { Delegation as DelegationInterface } from "@/app/types/delegations"; import { getNetworkConfig } from "@/config/network.config"; import { blocksToDisplayTime } from "@/utils/blocksToDisplayTime"; diff --git a/src/app/components/Stats/StakingCapItem.tsx b/src/app/components/Stats/StakingCapItem.tsx new file mode 100644 index 00000000..28b7b8b6 --- /dev/null +++ b/src/app/components/Stats/StakingCapItem.tsx @@ -0,0 +1,113 @@ +import { memo } from "react"; + +import { useAppState, type AppState } from "@/app/state"; +import { getNetworkConfig } from "@/config/network.config"; +import { satoshiToBtc } from "@/utils/btcConversions"; +import { maxDecimals } from "@/utils/maxDecimals"; + +import { StatItem } from "./StatItem"; +import { stakingTvlCap } from "./icons"; + +const { coinName } = getNetworkConfig(); + +type StakingCapAdapter = { + match: (state: AppState) => boolean; + adopt: (state: AppState) => JSX.Element | undefined; +}; + +const STAKING_CAP_ITEMS: StakingCapAdapter[] = [ + { + match: ({ isLoading, currentHeight, currentVersion }) => + !currentHeight || !currentVersion || isLoading, + adopt: ({ isLoading }) => ( + + ), + }, + + { + match: ({ isApprochingNextVersion, nextVersion }) => + isApprochingNextVersion && Boolean(nextVersion?.activationHeight), + adopt: ({ + nextVersion: { activationHeight = 0 } = {}, + currentHeight = 0, + }) => { + const remainingBlocks = activationHeight - currentHeight - 1; + + return ( + + ); + }, + }, + + { + match: ({ isApprochingNextVersion, nextVersion }) => + isApprochingNextVersion && Boolean(nextVersion?.stakingCapSat), + adopt: ({ nextVersion: { stakingCapSat = 0 } = {} }) => ( + + ), + }, + + { + match: ({ currentVersion }) => Boolean(currentVersion?.stakingCapHeight), + adopt: ({ + currentVersion: { stakingCapHeight = 0 } = {}, + currentHeight = 0, + }) => { + const numOfBlockLeft = stakingCapHeight - currentHeight; + + return ( + 0 + ? `closes in ${numOfBlockLeft} ${numOfBlockLeft == 1 ? "block" : "blocks"}` + : "closed" + } + /> + ); + }, + }, + + { + match: ({ currentVersion }) => Boolean(currentVersion?.stakingCapSat), + adopt: ({ currentVersion: { stakingCapSat = 0 } = {} }) => ( + + ), + }, + + { + match: () => true, + adopt: () => ( + + ), + }, +]; + +export const StakingCapItem = memo(() => { + const appState = useAppState(); + const stakingCapAdapter = STAKING_CAP_ITEMS.find(({ match }) => + match(appState), + ); + + return stakingCapAdapter?.adopt(appState); +}); + +StakingCapItem.displayName = "StakingCapItem"; diff --git a/src/app/components/Stats/StatItem.tsx b/src/app/components/Stats/StatItem.tsx new file mode 100644 index 00000000..62a7d001 --- /dev/null +++ b/src/app/components/Stats/StatItem.tsx @@ -0,0 +1,51 @@ +import { type JSX } from "react"; +import { AiOutlineInfoCircle } from "react-icons/ai"; +import { Tooltip } from "react-tooltip"; + +interface StatItemProps { + loading?: boolean; + icon: JSX.Element; + title: string; + value: string | number; + tooltip?: string; +} + +export const StatItem = ({ + loading, + icon, + title, + value, + tooltip, +}: StatItemProps) => ( +
+
+ {icon} +
+

{title}

+ {tooltip && ( + <> + + + + + + )} +
+
+ +
+

+ {loading ? ( + + ) : ( + {value} + )} +

+
+
+); diff --git a/src/app/components/Stats/Stats.tsx b/src/app/components/Stats/Stats.tsx index 39b28348..4142371c 100644 --- a/src/app/components/Stats/Stats.tsx +++ b/src/app/components/Stats/Stats.tsx @@ -1,224 +1,81 @@ -import Image from "next/image"; -import { Fragment, useEffect, useMemo, useState } from "react"; -import { AiOutlineInfoCircle } from "react-icons/ai"; -import { Tooltip } from "react-tooltip"; +import { memo } from "react"; -import { - StakingStats, - useStakingStats, -} from "@/app/context/api/StakingStatsProvider"; -import { useAppState } from "@/app/state"; -import { GlobalParamsVersion } from "@/app/types/globalParams"; +import { useSystemStats } from "@/app/hooks/api/useSystemStats"; import { getNetworkConfig } from "@/config/network.config"; import { satoshiToBtc } from "@/utils/btcConversions"; -import { ParamsWithContext } from "@/utils/globalParams"; import { maxDecimals } from "@/utils/maxDecimals"; -import confirmedTvl from "./icons/confirmed-tvl.svg"; -import delegations from "./icons/delegations.svg"; -import pendingStake from "./icons/pending-stake.svg"; -import stakers from "./icons/stakers.svg"; -import stakingTvlCap from "./icons/staking-tvl-cap.svg"; +import { confirmedTVL, delegations, pendingStake, stakers } from "./icons"; +import { StakingCapItem } from "./StakingCapItem"; +import { StatItem } from "./StatItem"; -const buildNextCapText = ( - coinName: string, - btcHeight: number, - nextVersion: GlobalParamsVersion, -) => { - const { stakingCapHeight, stakingCapSat, activationHeight } = nextVersion; - if (stakingCapHeight) { - const remainingBlocks = activationHeight - btcHeight - 1; - return { - title: "Staking Window", - value: `opens in ${remainingBlocks} ${remainingBlocks == 1 ? "block" : "blocks"}`, - }; - } else if (stakingCapSat) { - return { - title: "Next Staking TVL Cap", - value: `${maxDecimals(satoshiToBtc(stakingCapSat), 8)} ${coinName}`, - }; - } -}; +const { coinName } = getNetworkConfig(); -const buildStakingCapSection = ( - coinName: string, - btcHeight: number, - paramsCtx: ParamsWithContext, -) => { - const { currentVersion, nextVersion, isApprochingNextVersion } = paramsCtx; - if (!currentVersion) { - return; - } - if (isApprochingNextVersion && nextVersion) { - return buildNextCapText(coinName, btcHeight, nextVersion); - } - const { stakingCapHeight, stakingCapSat } = currentVersion; - if (stakingCapHeight) { - const numOfBlockLeft = stakingCapHeight - btcHeight; - return { - title: "Staking Window", - value: - numOfBlockLeft > 0 - ? `closes in ${numOfBlockLeft} ${numOfBlockLeft == 1 ? "block" : "blocks"}` - : "closed", - }; - } else if (stakingCapSat) { - return { - title: "Staking TVL Cap", - value: `${maxDecimals(satoshiToBtc(stakingCapSat), 8)} ${coinName}`, - }; - } -}; +const formatter = Intl.NumberFormat("en", { + notation: "compact", + maximumFractionDigits: 2, +}); -export const Stats: React.FC = () => { - const [stakingStats, setStakingStats] = useState({ - activeTVLSat: 0, - totalTVLSat: 0, - activeDelegations: 0, - totalDelegations: 0, - totalStakers: 0, - unconfirmedTVLSat: 0, - }); - const [isLoading, setIsLoading] = useState(true); - const stakingStatsProvider = useStakingStats(); - const appState = useAppState(); +export const Stats = memo(() => { + const { + data: { + activeTVLSat = 0, + unconfirmedTVLSat = 0, + activeDelegations = 0, + totalStakers = 0, + } = {}, + isLoading, + } = useSystemStats(); - const { coinName } = getNetworkConfig(); + return ( +
+
+ - // Load the data from staking stats provider - useEffect(() => { - if (stakingStatsProvider.data) { - setStakingStats(stakingStatsProvider.data); - } - setIsLoading( - stakingStatsProvider.isLoading || Boolean(appState?.isLoading), - ); - }, [stakingStatsProvider, appState?.isLoading]); +
- const stakingCapText = useMemo(() => { - if (!appState?.currentHeight) { - return { - title: "Staking TVL Cap", - value: "-", - }; - } + - const cap = buildStakingCapSection( - coinName, - appState.currentHeight, - appState, - ); +
- return ( - cap ?? { - title: "Staking TVL Cap", - value: "-", - } - ); - }, [coinName, appState]); + +
- const formatter = Intl.NumberFormat("en", { - notation: "compact", - maximumFractionDigits: 2, - }); +
+ - const sections = [ - [ - { - title: stakingCapText.title, - value: stakingCapText.value, - icon: stakingTvlCap, - }, - { - title: "Confirmed TVL", - value: stakingStats?.activeTVLSat - ? `${maxDecimals(satoshiToBtc(stakingStats.activeTVLSat), 8)} ${coinName}` - : 0, - icon: confirmedTvl, - }, - { - title: "Pending Stake", - value: stakingStats?.unconfirmedTVLSat - ? `${maxDecimals(satoshiToBtc(stakingStats.unconfirmedTVLSat - stakingStats.activeTVLSat), 8)} ${coinName}` - : 0, - icon: pendingStake, - tooltip: - stakingStats && - stakingStats.unconfirmedTVLSat - stakingStats.activeTVLSat < 0 - ? "Pending TVL can be negative when there are unbonding requests" - : undefined, - }, - ], - [ - { - title: "Delegations", - value: stakingStats?.activeDelegations - ? formatter.format(stakingStats.activeDelegations as number) - : 0, - icon: delegations, - tooltip: "Total number of stake delegations", - }, - { - title: "Stakers", - value: stakingStats?.totalStakers - ? formatter.format(stakingStats.totalStakers as number) - : 0, - icon: stakers, - }, - ], - ]; +
- return ( -
- {sections.map((section, index) => ( -
- {section.map((subSection, subIndex) => ( - -
-
- {subSection.title} -
-

- {subSection.title} -

- {subSection.tooltip && ( - <> - - - - - - )} -
-
-
-

- {isLoading ? ( - - ) : ( - {subSection.value} - )} -

-
-
- {subIndex !== section.length - 1 && ( -
- )} - - ))} -
- ))} + +
); -}; +}); + +Stats.displayName = "Stats"; diff --git a/src/app/components/Stats/icons/confirmed-tvl.svg b/src/app/components/Stats/icons/confirmed-tvl.svg deleted file mode 100644 index f12b9346..00000000 --- a/src/app/components/Stats/icons/confirmed-tvl.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/app/components/Stats/icons/delegations.svg b/src/app/components/Stats/icons/delegations.svg deleted file mode 100644 index 8a851aa3..00000000 --- a/src/app/components/Stats/icons/delegations.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/app/components/Stats/icons/index.tsx b/src/app/components/Stats/icons/index.tsx new file mode 100644 index 00000000..329e90f4 --- /dev/null +++ b/src/app/components/Stats/icons/index.tsx @@ -0,0 +1,89 @@ +export const confirmedTVL = ( + + + +); + +export const delegations = ( + + + +); + +export const pendingStake = ( + + + +); + +export const stakers = ( + + + +); + +export const stakingTvlCap = ( + + + +); diff --git a/src/app/components/Stats/icons/pending-stake.svg b/src/app/components/Stats/icons/pending-stake.svg deleted file mode 100644 index f268679d..00000000 --- a/src/app/components/Stats/icons/pending-stake.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/app/components/Stats/icons/stakers.svg b/src/app/components/Stats/icons/stakers.svg deleted file mode 100644 index 10eef4dd..00000000 --- a/src/app/components/Stats/icons/stakers.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/src/app/components/Stats/icons/staking-tvl-cap.svg b/src/app/components/Stats/icons/staking-tvl-cap.svg deleted file mode 100644 index 065afdc4..00000000 --- a/src/app/components/Stats/icons/staking-tvl-cap.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/app/hooks/useApi.ts b/src/app/hooks/api/useApi.ts similarity index 100% rename from src/app/hooks/useApi.ts rename to src/app/hooks/api/useApi.ts diff --git a/src/app/hooks/useBTCTipHeight.ts b/src/app/hooks/api/useBTCTipHeight.ts similarity index 81% rename from src/app/hooks/useBTCTipHeight.ts rename to src/app/hooks/api/useBTCTipHeight.ts index ab952f39..7c5e30e1 100644 --- a/src/app/hooks/useBTCTipHeight.ts +++ b/src/app/hooks/api/useBTCTipHeight.ts @@ -1,4 +1,4 @@ -import { useAPIQuery } from "@/app/hooks/useApi"; +import { useAPIQuery } from "@/app/hooks/api/useApi"; import { getTipHeight } from "@/utils/mempool_api"; export const BTC_TIP_HEIGHT_KEY = "BTC_TIP_HEIGHT"; diff --git a/src/app/hooks/useDelegations.ts b/src/app/hooks/api/useDelegations.ts similarity index 100% rename from src/app/hooks/useDelegations.ts rename to src/app/hooks/api/useDelegations.ts diff --git a/src/app/hooks/useNetworkFees.ts b/src/app/hooks/api/useNetworkFees.ts similarity index 85% rename from src/app/hooks/useNetworkFees.ts rename to src/app/hooks/api/useNetworkFees.ts index 2a4840c5..8632af1a 100644 --- a/src/app/hooks/useNetworkFees.ts +++ b/src/app/hooks/api/useNetworkFees.ts @@ -1,4 +1,4 @@ -import { useAPIQuery } from "@/app/hooks/useApi"; +import { useAPIQuery } from "@/app/hooks/api/useApi"; import { getNetworkFees } from "@/utils/mempool_api"; export const NETWORK_FEES_KEY = "NETWORK_FEES"; diff --git a/src/app/hooks/api/useSystemStats.ts b/src/app/hooks/api/useSystemStats.ts new file mode 100644 index 00000000..b2749736 --- /dev/null +++ b/src/app/hooks/api/useSystemStats.ts @@ -0,0 +1,11 @@ +import { getStats } from "@/app/api/getStats"; +import { useAPIQuery } from "@/app/hooks/api/useApi"; + +export const BTC_TIP_HEIGHT_KEY = "API_STATS"; + +export function useSystemStats() { + return useAPIQuery({ + queryKey: ["API_STATS"], + queryFn: getStats, + }); +} diff --git a/src/app/hooks/useUTXOs.ts b/src/app/hooks/api/useUTXOs.ts similarity index 94% rename from src/app/hooks/useUTXOs.ts rename to src/app/hooks/api/useUTXOs.ts index 99b880a6..c987eee0 100644 --- a/src/app/hooks/useUTXOs.ts +++ b/src/app/hooks/api/useUTXOs.ts @@ -1,6 +1,6 @@ import { ONE_MINUTE } from "@/app/constants"; import { useWallet } from "@/app/context/wallet/WalletProvider"; -import { useAPIQuery } from "@/app/hooks/useApi"; +import { useAPIQuery } from "@/app/hooks/api/useApi"; import { filterOrdinals } from "@/utils/utxo"; export const UTXO_KEY = "UTXO"; diff --git a/src/app/hooks/api/useVersions.ts b/src/app/hooks/api/useVersions.ts new file mode 100644 index 00000000..3ea629a6 --- /dev/null +++ b/src/app/hooks/api/useVersions.ts @@ -0,0 +1,14 @@ +import { getGlobalParams } from "@/app/api/getGlobalParams"; +import { useAPIQuery } from "@/app/hooks/api/useApi"; + +export const VERSIONS_KEY = "VERSIONS"; + +export function useVersions({ enabled = true }: { enabled?: boolean } = {}) { + const data = useAPIQuery({ + queryKey: [VERSIONS_KEY], + queryFn: getGlobalParams, + enabled, + }); + + return data; +} diff --git a/src/app/hooks/useVersionByHeight.ts b/src/app/hooks/useVersionByHeight.ts new file mode 100644 index 00000000..05967a89 --- /dev/null +++ b/src/app/hooks/useVersionByHeight.ts @@ -0,0 +1,14 @@ +import { useMemo } from "react"; + +import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; + +import { useVersions } from "./api/useVersions"; + +export function useVersionByHeight(height: number) { + const { data: versions } = useVersions(); + + return useMemo( + () => getCurrentGlobalParamsVersion(height, versions ?? []), + [versions, height], + ); +} diff --git a/src/app/hooks/useVersions.ts b/src/app/hooks/useVersions.ts deleted file mode 100644 index f1f812b3..00000000 --- a/src/app/hooks/useVersions.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useMemo } from "react"; - -import { getGlobalParams } from "@/app/api/getGlobalParams"; -import { useAPIQuery } from "@/app/hooks/useApi"; -import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; - -export const VERSIONS_KEY = "VERSIONS"; - -export function useVersions({ enabled = true }: { enabled?: boolean } = {}) { - const data = useAPIQuery({ - queryKey: [VERSIONS_KEY], - queryFn: getGlobalParams, - enabled, - }); - - return data; -} - -export function useVersionByHeight(height: number) { - const { data: versions } = useVersions(); - return useMemo( - () => getCurrentGlobalParamsVersion(height, versions ?? []), - [versions, height], - ); -} diff --git a/src/app/state/DelegationState.tsx b/src/app/state/DelegationState.tsx index 94dd9bf9..9b049837 100644 --- a/src/app/state/DelegationState.tsx +++ b/src/app/state/DelegationState.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, type PropsWithChildren } from "react"; import { useLocalStorage } from "usehooks-ts"; import { useWallet } from "@/app/context/wallet/WalletProvider"; -import { useDelegations } from "@/app/hooks/useDelegations"; +import { useDelegations } from "@/app/hooks/api/useDelegations"; import type { Delegation } from "@/app/types/delegations"; import { DelegationState as DelegationEnum } from "@/app/types/delegations"; import { createStateUtils } from "@/utils/createStateUtils"; diff --git a/src/app/state/index.tsx b/src/app/state/index.tsx index deb79286..a3623270 100644 --- a/src/app/state/index.tsx +++ b/src/app/state/index.tsx @@ -1,8 +1,8 @@ import { useMemo, type PropsWithChildren } from "react"; -import { useBTCTipHeight } from "@/app/hooks/useBTCTipHeight"; -import { useUTXOs } from "@/app/hooks/useUTXOs"; -import { useVersions } from "@/app/hooks/useVersions"; +import { useBTCTipHeight } from "@/app/hooks/api/useBTCTipHeight"; +import { useUTXOs } from "@/app/hooks/api/useUTXOs"; +import { useVersions } from "@/app/hooks/api/useVersions"; import { GlobalParamsVersion } from "@/app/types/globalParams"; import { createStateUtils } from "@/utils/createStateUtils"; import { getCurrentGlobalParamsVersion } from "@/utils/globalParams"; @@ -12,7 +12,7 @@ import { DelegationState } from "./DelegationState"; const STATE_LIST = [DelegationState]; -interface AppState { +export interface AppState { availableUTXOs?: UTXO[]; totalBalance: number; nextVersion?: GlobalParamsVersion;