소스 검색

Revert "feat(staking) address some feedback items"

Connor Prussin 1 년 전
부모
커밋
7770e8eb5b

+ 0 - 77
apps/staking/src/components/CopyButton/index.tsx

@@ -1,77 +0,0 @@
-import { ClipboardDocumentIcon, CheckIcon } from "@heroicons/react/24/outline";
-import clsx from "clsx";
-import { type ComponentProps, useCallback, useEffect, useState } from "react";
-import { Button } from "react-aria-components";
-
-import { useLogger } from "../../hooks/use-logger";
-
-type CopyButtonProps = ComponentProps<typeof Button> & {
-  text: string;
-};
-
-export const CopyButton = ({
-  text,
-  children,
-  className,
-  ...props
-}: CopyButtonProps) => {
-  const [isCopied, setIsCopied] = useState(false);
-  const logger = useLogger();
-  const copy = useCallback(() => {
-    // eslint-disable-next-line n/no-unsupported-features/node-builtins
-    navigator.clipboard
-      .writeText(text)
-      .then(() => {
-        setIsCopied(true);
-      })
-      .catch((error: unknown) => {
-        /* TODO do something here? */
-        logger.error(error);
-      });
-  }, [text, logger]);
-
-  useEffect(() => {
-    setIsCopied(false);
-  }, [text]);
-
-  useEffect(() => {
-    if (isCopied) {
-      const timeout = setTimeout(() => {
-        setIsCopied(false);
-      }, 2000);
-      return () => {
-        clearTimeout(timeout);
-      };
-    } else {
-      return;
-    }
-  }, [isCopied]);
-
-  return (
-    <Button
-      onPress={copy}
-      isDisabled={isCopied}
-      className={clsx(
-        "group -mt-0.5 rounded-md px-2 py-0.5 align-middle transition hover:bg-white/10 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400",
-        className,
-      )}
-      {...(isCopied && { "data-is-copied": true })}
-      {...props}
-    >
-      {(...args) => (
-        <>
-          <span className="align-middle">
-            {typeof children === "function" ? children(...args) : children}
-          </span>
-          <span className="relative ml-[0.25em] inline-block align-middle">
-            <span className="opacity-50 transition-opacity duration-100 group-data-[is-copied]:opacity-0">
-              <ClipboardDocumentIcon className="size-[1em]" />
-              <div className="sr-only">Copy code to clipboaord</div>
-            </span>
-            <CheckIcon className="absolute inset-0 text-green-600 opacity-0 transition-opacity duration-100 group-data-[is-copied]:opacity-100" />
-          </span>
-        </>
-      )}
-    </Button>
-  );
-};

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

@@ -1,30 +0,0 @@
-"use client";
-
-import clsx from "clsx";
-import { type HTMLProps } from "react";
-
-import { StateType as ApiStateType, useApi } from "../../hooks/use-api";
-import { CopyButton } from "../CopyButton";
-import { TruncatedKey } from "../TruncatedKey";
-
-export const CurrentStakeAccount = ({
-  className,
-  ...props
-}: HTMLProps<HTMLDivElement>) => {
-  const api = useApi();
-
-  return api.type === ApiStateType.Loaded ? (
-    <div className={clsx("grid place-content-center", className)} {...props}>
-      <div className="flex flex-col items-end text-xs md:flex-row md:items-baseline md:text-sm">
-        <div className="font-semibold">Stake account:</div>
-        <CopyButton
-          text={api.account.address.toBase58()}
-          className="-mr-2 ml-2 text-pythpurple-400 md:mr-0"
-        >
-          <TruncatedKey>{api.account.address}</TruncatedKey>
-        </CopyButton>
-      </div>
-    </div>
-  ) : // eslint-disable-next-line unicorn/no-null
-  null;
-};

+ 3 - 9
apps/staking/src/components/Header/index.tsx

@@ -1,9 +1,7 @@
 import clsx from "clsx";
 import type { HTMLAttributes } from "react";
 
-import { CurrentStakeAccount } from "./current-stake-account";
 import Logo from "./logo.svg";
-import Logomark from "./logomark.svg";
 import { MaxWidth } from "../MaxWidth";
 import { WalletButton } from "../WalletButton";
 
@@ -16,13 +14,9 @@ export const Header = ({
     {...props}
   >
     <div className="border-b border-neutral-600/50 bg-pythpurple-800 sm:border-x">
-      <MaxWidth className="flex h-16 items-center justify-between gap-2 sm:-mx-4">
-        <Logo className="hidden max-h-full py-4 text-pythpurple-100 sm:block" />
-        <Logomark className="max-h-full py-4 text-pythpurple-100 sm:hidden" />
-        <div className="flex flex-none flex-row items-stretch gap-8">
-          <CurrentStakeAccount />
-          <WalletButton className="flex-none" />
-        </div>
+      <MaxWidth className="flex h-16 items-center justify-between gap-8 sm:-mx-4">
+        <Logo className="max-h-full py-4 text-pythpurple-100" />
+        <WalletButton className="flex-none" />
       </MaxWidth>
     </div>
   </header>

+ 0 - 4
apps/staking/src/components/Header/logomark.svg

@@ -1,4 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 1176 1474">
-  <path d="M734.994 589.59c0 81.401-65.821 147.402-147 147.402v147.401c162.358 0 294-132.001 294-294.803 0-162.801-131.642-294.803-294-294.803-53.521 0-103.784 14.334-147 39.467-87.894 50.934-147 146.202-147 255.336v737.01l132.174 132.53 14.826 14.87V589.59c0-81.401 65.821-147.401 147-147.401s147 66 147 147.401Z"/>
-  <path d="M588 0C480.891 0 380.498 28.734 294 78.934c-55.383 32.067-104.981 72.934-147 120.735C55.516 303.603 0 440.138 0 589.606v442.204l147 147.4V589.606c0-130.934 56.779-248.602 147-329.603 42.418-38.001 92.216-68.001 147-87.334 45.942-16.401 95.474-25.267 147-25.267 243.537 0 441 198.002 441 442.204 0 244.203-197.463 442.204-441 442.204v147.4c324.783 0 588-264 588-589.604C1176 264.003 912.783 0 588 0Z"/>
-</svg>

+ 100 - 150
apps/staking/src/components/OracleIntegrityStaking/index.tsx

@@ -26,7 +26,6 @@ import {
   DialogTrigger,
   TextField,
   Form,
-  Switch,
 } from "react-aria-components";
 
 import { type States, StateType as ApiStateType } from "../../hooks/use-api";
@@ -35,7 +34,6 @@ import {
   useAsync,
 } from "../../hooks/use-async";
 import { Button } from "../Button";
-import { CopyButton } from "../CopyButton";
 import { ModalDialog } from "../ModalDialog";
 import { ProgramSection } from "../ProgramSection";
 import { SparkChart } from "../SparkChart";
@@ -113,7 +111,7 @@ export const OracleIntegrityStaking = ({
           <div className="relative w-full overflow-x-auto">
             <div className="sticky left-0 mb-4 flex flex-row items-center justify-between px-4 sm:px-10 sm:pb-4 sm:pt-6">
               <h3 className="text-2xl font-light">
-                You - <PublisherName fullKey>{self}</PublisherName>
+                You (<PublisherName>{self}</PublisherName>)
               </h3>
               <div className="flex flex-row items-center gap-4">
                 <ReassignStakeAccountButton self={self} api={api} />
@@ -428,45 +426,64 @@ const PublisherList = ({
   yieldRate,
 }: PublisherListProps) => {
   const [search, setSearch] = useState("");
-  const [yoursFirst, setYoursFirst] = useState(true);
   const [sort, setSort] = useState({
     field: SortField.PoolUtilization,
-    descending: true,
+    descending: false,
   });
   const filter = useFilter({ sensitivity: "base", usage: "search" });
   const [currentPage, setPage] = useState(0);
-  const filteredSortedPublishers = useMemo(
-    () =>
-      publishers
-        .filter(
-          (publisher) =>
-            filter.contains(publisher.publicKey.toBase58(), search) ||
-            (publisher.name !== undefined &&
-              filter.contains(publisher.name, search)),
-        )
-        .sort((a, b) => {
-          if (yoursFirst) {
-            const aHasPositions = hasAnyPositions(a);
-            const bHasPositions = hasAnyPositions(b);
-            if (aHasPositions && !bHasPositions) {
-              return -1;
-            } else if (bHasPositions && !aHasPositions) {
-              return 1;
-            }
+  const filteredSortedPublishers = useMemo(() => {
+    const sorted = publishers
+      .filter(
+        (publisher) =>
+          filter.contains(publisher.publicKey.toBase58(), search) ||
+          (publisher.name !== undefined &&
+            filter.contains(publisher.name, search)),
+      )
+      .sort((a, b) => {
+        switch (sort.field) {
+          case SortField.PublisherName: {
+            return (a.name ?? a.publicKey.toBase58()).localeCompare(
+              b.name ?? b.publicKey.toBase58(),
+            );
           }
-          const sortResult = doSort(a, b, yieldRate, sort.field);
-          return sort.descending ? sortResult * -1 : sortResult;
-        }),
-    [
-      publishers,
-      search,
-      sort.field,
-      sort.descending,
-      filter,
-      yieldRate,
-      yoursFirst,
-    ],
-  );
+          case SortField.APY: {
+            return (
+              calculateApy({
+                isSelf: false,
+                selfStake: a.selfStake,
+                poolCapacity: a.poolCapacity,
+                poolUtilization: a.poolUtilization,
+                yieldRate,
+              }) -
+              calculateApy({
+                isSelf: false,
+                selfStake: b.selfStake,
+                poolCapacity: b.poolCapacity,
+                poolUtilization: b.poolUtilization,
+                yieldRate,
+              })
+            );
+          }
+          case SortField.NumberOfFeeds: {
+            return Number(a.numFeeds - b.numFeeds);
+          }
+          case SortField.PoolUtilization: {
+            return Number(
+              a.poolUtilization * b.poolCapacity -
+                b.poolUtilization * a.poolCapacity,
+            );
+          }
+          case SortField.QualityRanking: {
+            return Number(a.qualityRanking - b.qualityRanking);
+          }
+          case SortField.SelfStake: {
+            return Number(a.selfStake - b.selfStake);
+          }
+        }
+      });
+    return sort.descending ? sorted.reverse() : sorted;
+  }, [publishers, search, sort.field, sort.descending, filter, yieldRate]);
 
   const paginatedPublishers = useMemo(
     () =>
@@ -495,42 +512,28 @@ const PublisherList = ({
 
   return (
     <div className="relative w-full overflow-x-auto">
-      <div className="sticky left-0 mb-4 flex flex-col gap-4 px-4 sm:px-10 sm:pb-4 sm:pt-6 md:flex-row md:items-center md:justify-between md:gap-12">
-        <h3 className="flex-none text-2xl font-light">{title}</h3>
+      <div className="sticky left-0 mb-4 flex flex-row items-center justify-between gap-6 px-4 text-2xl sm:px-10 sm:pb-4 sm:pt-6">
+        <h3 className="font-light">{title}</h3>
 
-        <div className="flex flex-none grow flex-col items-end gap-2 lg:flex-row-reverse lg:items-center lg:justify-start lg:gap-10 xl:gap-16">
-          <SearchField
-            value={search}
-            onChange={updateSearch}
-            aria-label="Search"
-            className="group relative w-full md:max-w-96"
-          >
-            <Input
-              className="group-focused:ring-0 group-focused:border-pythpurple-400 group-focused:outline-none w-full truncate border border-pythpurple-600 bg-pythpurple-600/10 py-2 pl-10 pr-8 focus:border-pythpurple-400 focus:outline-none focus:ring-0 focus-visible:border-pythpurple-400 focus-visible:outline-none focus-visible:ring-0 search-cancel:appearance-none search-decoration:appearance-none"
-              placeholder="Search"
-            />
-            <div className="absolute inset-y-0 left-4 grid place-content-center">
-              <MagnifyingGlassIcon className="size-4 text-pythpurple-400" />
-            </div>
-            <div className="absolute inset-y-0 right-2 grid place-content-center">
-              <BaseButton className="p-2 group-empty:hidden">
-                <XMarkIcon className="size-4" />
-              </BaseButton>
-            </div>
-          </SearchField>
-          <Switch
-            isSelected={yoursFirst}
-            onChange={setYoursFirst}
-            className="group flex cursor-pointer flex-row items-center gap-2"
-          >
-            <div className="whitespace-nowrap opacity-80">
-              Show your positions first
-            </div>
-            <div className="h-8 w-16 flex-none rounded-full border border-neutral-400/50 bg-neutral-800/50 p-1 transition group-data-[selected]:border-pythpurple-600 group-data-[selected]:bg-pythpurple-600/10">
-              <div className="aspect-square h-full rounded-full bg-neutral-400/50 transition group-data-[selected]:translate-x-8 group-data-[selected]:bg-pythpurple-600" />
-            </div>
-          </Switch>
-        </div>
+        <SearchField
+          value={search}
+          onChange={updateSearch}
+          aria-label="Search"
+          className="group relative w-full max-w-96"
+        >
+          <Input
+            className="group-focused:ring-0 group-focused:border-pythpurple-400 group-focused:outline-none w-full truncate border border-pythpurple-600 bg-pythpurple-600/10 py-2 pl-10 pr-8 focus:border-pythpurple-400 focus:outline-none focus:ring-0 focus-visible:border-pythpurple-400 focus-visible:outline-none focus-visible:ring-0 search-cancel:appearance-none search-decoration:appearance-none"
+            placeholder="Search"
+          />
+          <div className="absolute inset-y-0 left-4 grid place-content-center">
+            <MagnifyingGlassIcon className="size-4 text-pythpurple-400" />
+          </div>
+          <div className="absolute inset-y-0 right-2 grid place-content-center">
+            <BaseButton className="p-2 group-empty:hidden">
+              <XMarkIcon className="size-4" />
+            </BaseButton>
+          </div>
+        </SearchField>
       </div>
 
       {filteredSortedPublishers.length > 0 ? (
@@ -541,8 +544,7 @@ const PublisherList = ({
                 field={SortField.PublisherName}
                 sort={sort}
                 setSort={updateSort}
-                alignment="left"
-                className="pl-4 sm:pl-10"
+                className="pl-4 text-left sm:pl-10"
               >
                 Publisher
               </SortablePublisherTableHeader>
@@ -551,7 +553,7 @@ const PublisherList = ({
                 sort={sort}
                 setSort={updateSort}
               >
-                {"Publisher's stake"}
+                Self stake
               </SortablePublisherTableHeader>
               <SortablePublisherTableHeader
                 field={SortField.PoolUtilization}
@@ -636,54 +638,6 @@ const PublisherList = ({
   );
 };
 
-const doSort = (
-  a: PublisherProps["publisher"],
-  b: PublisherProps["publisher"],
-  yieldRate: bigint,
-  sortField: SortField,
-): number => {
-  switch (sortField) {
-    case SortField.PublisherName: {
-      return (a.name ?? a.publicKey.toBase58()).localeCompare(
-        b.name ?? b.publicKey.toBase58(),
-      );
-    }
-    case SortField.APY: {
-      return (
-        calculateApy({
-          isSelf: false,
-          selfStake: a.selfStake,
-          poolCapacity: a.poolCapacity,
-          poolUtilization: a.poolUtilization,
-          yieldRate,
-        }) -
-        calculateApy({
-          isSelf: false,
-          selfStake: b.selfStake,
-          poolCapacity: b.poolCapacity,
-          poolUtilization: b.poolUtilization,
-          yieldRate,
-        })
-      );
-    }
-    case SortField.NumberOfFeeds: {
-      return Number(a.numFeeds - b.numFeeds);
-    }
-    case SortField.PoolUtilization: {
-      const value = Number(
-        a.poolUtilization * b.poolCapacity - b.poolUtilization * a.poolCapacity,
-      );
-      return value === 0 ? Number(a.poolCapacity - b.poolCapacity) : value;
-    }
-    case SortField.QualityRanking: {
-      return Number(a.qualityRanking - b.qualityRanking);
-    }
-    case SortField.SelfStake: {
-      return Number(a.selfStake - b.selfStake);
-    }
-  }
-};
-
 const range = (length: number) => [...Array.from({ length }).keys()];
 
 type SortablePublisherTableHeaderProps = Omit<
@@ -694,7 +648,6 @@ type SortablePublisherTableHeaderProps = Omit<
   field: SortField;
   sort: { field: SortField; descending: boolean };
   setSort: Dispatch<SetStateAction<{ field: SortField; descending: boolean }>>;
-  alignment?: "left" | "right";
 };
 
 const SortablePublisherTableHeader = ({
@@ -703,7 +656,6 @@ const SortablePublisherTableHeader = ({
   setSort,
   children,
   className,
-  alignment,
   ...props
 }: SortablePublisherTableHeaderProps) => {
   const updateSort = useCallback(() => {
@@ -718,17 +670,20 @@ const SortablePublisherTableHeader = ({
       <PublisherTableHeader
         as={BaseButton}
         className={clsx(
-          "group size-full data-[sorted]:bg-black/20 data-[alignment=center]:data-[sorted]:px-2.5 data-[alignment=left]:text-left data-[alignment=right]:text-right focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400",
+          "flex size-full flex-row items-center gap-2 focus:outline-none focus-visible:ring-1 focus-visible:ring-pythpurple-400",
+          { "bg-black/20": sort.field === field },
           className,
         )}
         onPress={updateSort}
-        {...(sort.field === field && { "data-sorted": true })}
-        {...(sort.descending && { "data-descending": true })}
-        data-alignment={alignment ?? "center"}
         {...props}
       >
-        <span className="align-middle">{children}</span>
-        <ChevronUpIcon className="ml-2 hidden size-3 transition-transform group-data-[sorted]:inline group-data-[descending]:rotate-180" />
+        <span>{children}</span>
+        <ChevronUpIcon
+          className={clsx("size-4 transition-transform", {
+            "rotate-180": sort.descending,
+            "opacity-0": sort.field !== field,
+          })}
+        />
       </PublisherTableHeader>
     </th>
   );
@@ -1090,30 +1045,25 @@ const NewApy = ({
   return <div {...props}>{apy}%</div>;
 };
 
-type PublisherNameProps = {
-  className?: string | undefined;
+type PublisherNameProps = Omit<HTMLAttributes<HTMLSpanElement>, "children"> & {
   children: PublisherProps["publisher"];
   fullKey?: boolean | undefined;
 };
 
-const PublisherName = ({ children, fullKey, className }: PublisherNameProps) =>
-  children.name ? (
-    <span className={className}>{children.name}</span>
-  ) : (
-    <CopyButton
-      text={children.publicKey.toBase58()}
-      {...(className && { className })}
-    >
-      {fullKey === true && (
-        <code className="hidden 2xl:inline">
-          {children.publicKey.toBase58()}
-        </code>
-      )}
-      <TruncatedKey className={clsx({ "2xl:hidden": fullKey })}>
-        {children.publicKey}
-      </TruncatedKey>
-    </CopyButton>
-  );
+const PublisherName = ({ children, fullKey, ...props }: PublisherNameProps) => (
+  <span {...props}>
+    {children.name ?? (
+      <>
+        {fullKey === true && (
+          <code className="hidden 2xl:block">
+            {children.publicKey.toBase58()}
+          </code>
+        )}
+        <TruncatedKey className="2xl:hidden">{children.publicKey}</TruncatedKey>
+      </>
+    )}
+  </span>
+);
 
 const useTransferActionForPublisher = (
   action: ((publisher: PublicKey, amount: bigint) => Promise<void>) | undefined,

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

@@ -10,7 +10,6 @@ import {
   AMPLITUDE_API_KEY,
   WALLETCONNECT_PROJECT_ID,
   RPC,
-  HERMES_URL,
   IS_MAINNET,
 } from "../../config/server";
 import { ApiProvider } from "../../hooks/use-api";
@@ -49,7 +48,7 @@ export const Root = ({ children }: Props) => (
             : WalletAdapterNetwork.Devnet
         }
       >
-        <ApiProvider hermesUrl={HERMES_URL}>
+        <ApiProvider isMainnet={IS_MAINNET}>
           <html
             lang="en"
             dir="ltr"

+ 2 - 8
apps/staking/src/config/server.ts

@@ -34,14 +34,8 @@ export const AMPLITUDE_API_KEY = demandInProduction("AMPLITUDE_API_KEY");
 export const WALLETCONNECT_PROJECT_ID = demandInProduction(
   "WALLETCONNECT_PROJECT_ID",
 );
-export const RPC = demandInProduction("RPC");
-export const IS_MAINNET =
-  IS_PRODUCTION_SERVER || process.env.IS_MAINNET !== undefined;
-export const HERMES_URL =
-  process.env.HERMES_URL ??
-  (IS_MAINNET
-    ? "https://hermes.pyth.network"
-    : "https://hermes-beta.pyth.network");
+export const RPC = process.env.RPC;
+export const IS_MAINNET = process.env.IS_MAINNET !== undefined;
 export const BLOCKED_REGIONS =
   process.env.BLOCKED_REGIONS === undefined ||
   process.env.BLOCKED_REGIONS === ""

+ 11 - 6
apps/staking/src/hooks/use-api.tsx

@@ -20,6 +20,9 @@ import { useSWRConfig } from "swr";
 
 import * as api from "../api";
 
+const MAINNET_HERMES_URL = "https://hermes.pyth.network";
+const DEVNET_HERMES_URL = "https://hermes-beta.pyth.network";
+
 export enum StateType {
   NotLoaded,
   NoWallet,
@@ -140,16 +143,16 @@ type ApiProviderProps = Omit<
   ComponentProps<typeof ApiContext.Provider>,
   "value"
 > & {
-  hermesUrl: string;
+  isMainnet: boolean;
 };
 
-export const ApiProvider = ({ hermesUrl, ...props }: ApiProviderProps) => {
-  const state = useApiContext(hermesUrl);
+export const ApiProvider = ({ isMainnet, ...props }: ApiProviderProps) => {
+  const state = useApiContext(isMainnet);
 
   return <ApiContext.Provider value={state} {...props} />;
 };
 
-const useApiContext = (hermesUrl: string) => {
+const useApiContext = (isMainnet: boolean) => {
   const loading = useRef(false);
   const wallet = useWallet();
   const { connection } = useConnection();
@@ -192,7 +195,9 @@ const useApiContext = (hermesUrl: string) => {
           signTransaction: wallet.signTransaction,
         },
       });
-      const hermesClient = new HermesClient(hermesUrl);
+      const hermesClient = new HermesClient(
+        isMainnet ? MAINNET_HERMES_URL : DEVNET_HERMES_URL,
+      );
       setState(State[StateType.LoadingStakeAccounts](client, hermesClient));
       api
         .getStakeAccounts(client)
@@ -243,7 +248,7 @@ const useApiContext = (hermesUrl: string) => {
           loading.current = false;
         });
     }
-  }, [connection, setAccount, wallet, mutate, hermesUrl]);
+  }, [connection, setAccount, wallet, mutate, isMainnet]);
 
   useEffect(() => {
     reset();