From 302e0ecb2269898e8d1b4aec68c677167ca58b08 Mon Sep 17 00:00:00 2001 From: Connor Prussin Date: Thu, 12 Sep 2024 17:05:36 -0700 Subject: [PATCH 1/2] Reapply "feat(staking) address some feedback items" This reverts commit 7770e8eb5b03844bbc55752fab4b16e1d6a4f1f2. --- .../src/components/CopyButton/index.tsx | 77 ++++++ .../Header/current-stake-account.tsx | 30 +++ apps/staking/src/components/Header/index.tsx | 12 +- .../src/components/Header/logomark.svg | 4 + .../OracleIntegrityStaking/index.tsx | 250 +++++++++++------- apps/staking/src/components/Root/index.tsx | 3 +- apps/staking/src/config/server.ts | 10 +- apps/staking/src/hooks/use-api.tsx | 17 +- 8 files changed, 286 insertions(+), 117 deletions(-) create mode 100644 apps/staking/src/components/CopyButton/index.tsx create mode 100644 apps/staking/src/components/Header/current-stake-account.tsx create mode 100644 apps/staking/src/components/Header/logomark.svg diff --git a/apps/staking/src/components/CopyButton/index.tsx b/apps/staking/src/components/CopyButton/index.tsx new file mode 100644 index 0000000000..79ae043936 --- /dev/null +++ b/apps/staking/src/components/CopyButton/index.tsx @@ -0,0 +1,77 @@ +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 & { + 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 ( + + ); +}; diff --git a/apps/staking/src/components/Header/current-stake-account.tsx b/apps/staking/src/components/Header/current-stake-account.tsx new file mode 100644 index 0000000000..2f6858c523 --- /dev/null +++ b/apps/staking/src/components/Header/current-stake-account.tsx @@ -0,0 +1,30 @@ +"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) => { + const api = useApi(); + + return api.type === ApiStateType.Loaded ? ( +
+
+
Stake account:
+ + {api.account.address} + +
+
+ ) : // eslint-disable-next-line unicorn/no-null + null; +}; diff --git a/apps/staking/src/components/Header/index.tsx b/apps/staking/src/components/Header/index.tsx index 77cc549708..2a6c7b53ec 100644 --- a/apps/staking/src/components/Header/index.tsx +++ b/apps/staking/src/components/Header/index.tsx @@ -1,7 +1,9 @@ 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"; @@ -14,9 +16,13 @@ export const Header = ({ {...props} >
- - - + + + +
+ + +
diff --git a/apps/staking/src/components/Header/logomark.svg b/apps/staking/src/components/Header/logomark.svg new file mode 100644 index 0000000000..35b01dc7e6 --- /dev/null +++ b/apps/staking/src/components/Header/logomark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/staking/src/components/OracleIntegrityStaking/index.tsx b/apps/staking/src/components/OracleIntegrityStaking/index.tsx index d73ff30bae..66bc201d5c 100644 --- a/apps/staking/src/components/OracleIntegrityStaking/index.tsx +++ b/apps/staking/src/components/OracleIntegrityStaking/index.tsx @@ -26,6 +26,7 @@ import { DialogTrigger, TextField, Form, + Switch, } from "react-aria-components"; import { type States, StateType as ApiStateType } from "../../hooks/use-api"; @@ -34,6 +35,7 @@ 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"; @@ -111,7 +113,7 @@ export const OracleIntegrityStaking = ({

- You ({self}) + You - {self}

@@ -426,64 +428,45 @@ const PublisherList = ({ yieldRate, }: PublisherListProps) => { const [search, setSearch] = useState(""); + const [yoursFirst, setYoursFirst] = useState(true); const [sort, setSort] = useState({ field: SortField.PoolUtilization, - descending: false, + descending: true, }); const filter = useFilter({ sensitivity: "base", usage: "search" }); const [currentPage, setPage] = useState(0); - 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(), - ); - } - 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); + 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; + } } - } - }); - return sort.descending ? sorted.reverse() : sorted; - }, [publishers, search, sort.field, sort.descending, filter, yieldRate]); + const sortResult = doSort(a, b, yieldRate, sort.field); + return sort.descending ? sortResult * -1 : sortResult; + }), + [ + publishers, + search, + sort.field, + sort.descending, + filter, + yieldRate, + yoursFirst, + ], + ); const paginatedPublishers = useMemo( () => @@ -512,28 +495,42 @@ const PublisherList = ({ return (
-
-

{title}

+
+

{title}

- - -
- -
-
- - - -
-
+
+ + +
+ +
+
+ + + +
+
+ +
+ Show your positions first +
+
+
+
+ +
{filteredSortedPublishers.length > 0 ? ( @@ -544,7 +541,8 @@ const PublisherList = ({ field={SortField.PublisherName} sort={sort} setSort={updateSort} - className="pl-4 text-left sm:pl-10" + alignment="left" + className="pl-4 sm:pl-10" > Publisher @@ -553,7 +551,7 @@ const PublisherList = ({ sort={sort} setSort={updateSort} > - Self stake + {"Publisher's stake"} { + 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< @@ -648,6 +694,7 @@ type SortablePublisherTableHeaderProps = Omit< field: SortField; sort: { field: SortField; descending: boolean }; setSort: Dispatch>; + alignment?: "left" | "right"; }; const SortablePublisherTableHeader = ({ @@ -656,6 +703,7 @@ const SortablePublisherTableHeader = ({ setSort, children, className, + alignment, ...props }: SortablePublisherTableHeaderProps) => { const updateSort = useCallback(() => { @@ -670,20 +718,17 @@ const SortablePublisherTableHeader = ({ - {children} - + {children} + ); @@ -1045,25 +1090,30 @@ const NewApy = ({ return
{apy}%
; }; -type PublisherNameProps = Omit, "children"> & { +type PublisherNameProps = { + className?: string | undefined; children: PublisherProps["publisher"]; fullKey?: boolean | undefined; }; -const PublisherName = ({ children, fullKey, ...props }: PublisherNameProps) => ( - - {children.name ?? ( - <> - {fullKey === true && ( - - {children.publicKey.toBase58()} - - )} - {children.publicKey} - - )} - -); +const PublisherName = ({ children, fullKey, className }: PublisherNameProps) => + children.name ? ( + {children.name} + ) : ( + + {fullKey === true && ( + + {children.publicKey.toBase58()} + + )} + + {children.publicKey} + + + ); const useTransferActionForPublisher = ( action: ((publisher: PublicKey, amount: bigint) => Promise) | undefined, diff --git a/apps/staking/src/components/Root/index.tsx b/apps/staking/src/components/Root/index.tsx index 6f16a51743..63f48fc0e0 100644 --- a/apps/staking/src/components/Root/index.tsx +++ b/apps/staking/src/components/Root/index.tsx @@ -10,6 +10,7 @@ import { AMPLITUDE_API_KEY, WALLETCONNECT_PROJECT_ID, RPC, + HERMES_URL, IS_MAINNET, } from "../../config/server"; import { ApiProvider } from "../../hooks/use-api"; @@ -48,7 +49,7 @@ export const Root = ({ children }: Props) => ( : WalletAdapterNetwork.Devnet } > - + , "value" > & { - isMainnet: boolean; + hermesUrl: string; }; -export const ApiProvider = ({ isMainnet, ...props }: ApiProviderProps) => { - const state = useApiContext(isMainnet); +export const ApiProvider = ({ hermesUrl, ...props }: ApiProviderProps) => { + const state = useApiContext(hermesUrl); return ; }; -const useApiContext = (isMainnet: boolean) => { +const useApiContext = (hermesUrl: string) => { const loading = useRef(false); const wallet = useWallet(); const { connection } = useConnection(); @@ -195,9 +192,7 @@ const useApiContext = (isMainnet: boolean) => { signTransaction: wallet.signTransaction, }, }); - const hermesClient = new HermesClient( - isMainnet ? MAINNET_HERMES_URL : DEVNET_HERMES_URL, - ); + const hermesClient = new HermesClient(hermesUrl); setState(State[StateType.LoadingStakeAccounts](client, hermesClient)); api .getStakeAccounts(client) @@ -248,7 +243,7 @@ const useApiContext = (isMainnet: boolean) => { loading.current = false; }); } - }, [connection, setAccount, wallet, mutate, isMainnet]); + }, [connection, setAccount, wallet, mutate, hermesUrl]); useEffect(() => { reset(); From 68107d755e5900c9e9bd30ea85fd93f0224ea9e1 Mon Sep 17 00:00:00 2001 From: Connor Prussin Date: Thu, 12 Sep 2024 17:10:32 -0700 Subject: [PATCH 2/2] Use IS_PRODUCTION_SERVER instead of IS_MAINNET to control hermes url --- apps/staking/src/config/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/staking/src/config/server.ts b/apps/staking/src/config/server.ts index 360dea81c6..c4a9173fe7 100644 --- a/apps/staking/src/config/server.ts +++ b/apps/staking/src/config/server.ts @@ -39,7 +39,7 @@ export const IS_MAINNET = IS_PRODUCTION_SERVER || process.env.IS_MAINNET !== undefined; export const HERMES_URL = process.env.HERMES_URL ?? - (IS_MAINNET + (IS_PRODUCTION_SERVER ? "https://hermes.pyth.network" : "https://hermes-beta.pyth.network"); export const BLOCKED_REGIONS =