Kaynağa Gözat

feat(staking): add stats to navbar

Connor Prussin 1 yıl önce
ebeveyn
işleme
92efb88ede

+ 1 - 1
apps/staking/src/components/Header/current-stake-account.tsx

@@ -20,7 +20,7 @@ export const CurrentStakeAccount = ({
   return api.type === ApiStateType.Loaded && !isBlocked ? (
     <div
       className={clsx(
-        "hidden flex-col items-end justify-center text-xs xs:flex md:flex-row md:items-center md:text-sm",
+        "hidden flex-col items-end justify-center text-xs xs:flex xl:flex-row xl:items-center xl:text-sm",
         className,
       )}
       {...props}

+ 3 - 3
apps/staking/src/components/Header/help-menu.tsx

@@ -7,7 +7,7 @@ import {
 import { useState, useCallback } from "react";
 import { MenuTrigger, Button } from "react-aria-components";
 
-import { ProgramParameters } from "./program-pramaeters";
+import { ProgramParameters } from "./program-parameters";
 import { StateType, useApi } from "../../hooks/use-api";
 import { GeneralFaq } from "../GeneralFaq";
 import { GovernanceGuide } from "../GovernanceGuide";
@@ -45,9 +45,9 @@ export const HelpMenu = () => {
   return (
     <>
       <MenuTrigger>
-        <Button className="group -mx-2 flex flex-row items-center gap-2 rounded-sm p-2 transition hover:bg-white/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400 pressed:bg-white/10 sm:-mx-4 sm:px-4">
+        <Button className="group -mx-2 flex flex-row items-center gap-2 rounded-sm p-2 transition hover:bg-white/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400 pressed:bg-white/10 md:-mx-4 md:px-4">
           <QuestionMarkCircleIcon className="size-6 flex-none" />
-          <span className="sr-only xs:not-sr-only">Help</span>
+          <span className="sr-only md:not-sr-only">Help</span>
           <ChevronDownIcon className="size-4 flex-none opacity-60 transition duration-300 group-data-[pressed]:-rotate-180" />
         </Button>
         <Menu placement="bottom end">

+ 28 - 18
apps/staking/src/components/Header/index.tsx

@@ -5,6 +5,7 @@ import { CurrentStakeAccount } from "./current-stake-account";
 import { HelpMenu } from "./help-menu";
 import Logo from "./logo.svg";
 import Logomark from "./logomark.svg";
+import { Stats } from "./stats";
 import { Link } from "../Link";
 import { MaxWidth } from "../MaxWidth";
 import { WalletButton } from "../WalletButton";
@@ -13,22 +14,31 @@ export const Header = ({
   className,
   ...props
 }: Omit<HTMLAttributes<HTMLElement>, "children">) => (
-  <header className={clsx("sticky top-0 w-full lg:px-4", className)} {...props}>
-    <div className="border-b border-neutral-600/50 bg-pythpurple-800 lg:border-x">
-      <MaxWidth className="flex h-header items-center justify-between gap-2 lg:-mx-4">
-        <Link
-          href="/"
-          className="-mx-2 h-[calc(var(--header-height)_-_0.5rem)] rounded-sm p-2 text-pythpurple-100 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400"
-        >
-          <Logo className="hidden h-full sm:block" />
-          <Logomark className="h-full sm:hidden" />
-        </Link>
-        <div className="flex flex-none flex-row items-stretch gap-4 sm:gap-8">
-          <CurrentStakeAccount />
-          <WalletButton className="flex-none" />
-          <HelpMenu />
-        </div>
-      </MaxWidth>
-    </div>
-  </header>
+  <>
+    <header
+      className={clsx("sticky top-0 w-full lg:px-4", className)}
+      {...props}
+    >
+      <div className="border-b border-neutral-600/50 bg-pythpurple-800 lg:border-x">
+        <MaxWidth className="flex h-header items-center justify-between gap-2 lg:-mx-4">
+          <div className="flex flex-row items-center gap-8 lg:gap-12">
+            <Link
+              href="/"
+              className="-mx-2 h-[calc(var(--header-height)_-_0.5rem)] rounded-sm p-2 text-pythpurple-100 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400"
+            >
+              <Logo className="hidden h-full lg:block" />
+              <Logomark className="h-full lg:hidden" />
+            </Link>
+            <Stats className="hidden gap-4 sm:flex lg:gap-6" />
+          </div>
+          <div className="flex flex-none flex-row items-stretch gap-4 md:gap-8">
+            <CurrentStakeAccount />
+            <WalletButton className="flex-none" />
+            <HelpMenu />
+          </div>
+        </MaxWidth>
+      </div>
+    </header>
+    <Stats className="border-b border-neutral-600/50 py-4 text-center sm:hidden" />
+  </>
 );

+ 0 - 0
apps/staking/src/components/Header/program-pramaeters.tsx → apps/staking/src/components/Header/program-parameters.tsx


+ 81 - 0
apps/staking/src/components/Header/stats.tsx

@@ -0,0 +1,81 @@
+"use client";
+
+import { PythStakingClient } from "@pythnetwork/staking-sdk";
+import { useConnection } from "@solana/wallet-adapter-react";
+import { Connection } from "@solana/web3.js";
+import clsx from "clsx";
+import type { HTMLProps } from "react";
+
+import { StateType, useData } from "../../hooks/use-data";
+import { Tokens } from "../Tokens";
+
+const ONE_SECOND_IN_MS = 1000;
+const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
+const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;
+const INITIAL_REWARD_POOL_SIZE = 60_000_000_000_000n;
+
+export const Stats = ({ className, ...props }: HTMLProps<HTMLDivElement>) => {
+  const { connection } = useConnection();
+  const state = useData("poolStats", () => fetchStats(connection), {
+    refreshInterval: REFRESH_INTERVAL,
+  });
+
+  return (
+    <div className={clsx("flex flex-row items-stretch", className)} {...props}>
+      <div className="flex-1 sm:flex-none">
+        {state.type === StateType.Loaded ? (
+          <Tokens className="mb-1 text-xl font-semibold leading-none">
+            {state.data.totalStaked}
+          </Tokens>
+        ) : (
+          <Loading />
+        )}
+        <div className="text-xs leading-none text-pythpurple-400">
+          OIS Total Staked
+        </div>
+      </div>
+      <div className="border-l border-neutral-600/50" />
+      <div className="flex-1 sm:flex-none">
+        {state.type === StateType.Loaded ? (
+          <Tokens className="mb-1 text-xl font-semibold leading-none">
+            {state.data.rewardsDistributed}
+          </Tokens>
+        ) : (
+          <Loading />
+        )}
+        <div className="text-xs leading-none text-pythpurple-400">
+          OIS Rewards Distributed
+        </div>
+      </div>
+    </div>
+  );
+};
+
+const Loading = () => (
+  <div className="mb-1 h-5 w-10 animate-pulse rounded-md bg-white/30" />
+);
+
+const fetchStats = async (connection: Connection) => {
+  const client = new PythStakingClient({ connection });
+  const poolData = await client.getPoolDataAccount();
+  const totalDelegated = sum(
+    poolData.delState.map(
+      ({ totalDelegation, deltaDelegation }) =>
+        totalDelegation + deltaDelegation,
+    ),
+  );
+  const totalSelfStaked = sum(
+    poolData.selfDelState.map(
+      ({ totalDelegation, deltaDelegation }) =>
+        totalDelegation + deltaDelegation,
+    ),
+  );
+
+  return {
+    totalStaked: totalDelegated + totalSelfStaked,
+    rewardsDistributed: poolData.claimableRewards + INITIAL_REWARD_POOL_SIZE,
+  };
+};
+
+const sum = (values: bigint[]): bigint =>
+  values.reduce((acc, value) => acc + value, 0n);

+ 2 - 0
apps/staking/src/components/Tokens/index.tsx

@@ -1,3 +1,5 @@
+"use client";
+
 import clsx from "clsx";
 import * as dnum from "dnum";
 import { type ComponentProps, useMemo } from "react";

+ 2 - 1
apps/staking/src/components/WalletButton/index.tsx

@@ -295,7 +295,8 @@ const ButtonComponent = ({
   ...props
 }: ButtonComponentProps) => (
   <Button
-    className={clsx("w-36 text-sm sm:w-52 sm:text-base", className)}
+    className={clsx("w-36 text-sm lg:w-52 lg:text-base", className)}
+    size="nopad"
     {...props}
   >
     <WalletIcon className="size-4 flex-none opacity-60" />