diff --git a/apps/staking/package.json b/apps/staking/package.json index 975162d461..0f32a19fd2 100644 --- a/apps/staking/package.json +++ b/apps/staking/package.json @@ -34,10 +34,12 @@ "@solana/wallet-adapter-wallets": "0.19.10", "@solana/web3.js": "^1.95.2", "clsx": "^2.1.1", + "dnum": "^2.13.1", "next": "^14.2.5", "pino": "^9.3.2", "react": "^18.3.1", "react-aria": "^3.34.3", + "react-aria-components": "^1.3.3", "react-dom": "^18.3.1", "recharts": "^2.12.7", "swr": "^2.2.5", @@ -61,6 +63,8 @@ "postcss": "^8.4.40", "prettier": "^3.3.2", "tailwindcss": "^3.4.7", + "tailwindcss-animate": "^1.0.7", + "tailwindcss-react-aria-components": "^1.1.5", "typescript": "^5.5.4", "vercel": "^35.2.2" } diff --git a/apps/staking/src/api.ts b/apps/staking/src/api.ts index 2ecca906bd..7791855601 100644 --- a/apps/staking/src/api.ts +++ b/apps/staking/src/api.ts @@ -83,7 +83,7 @@ export type StakeDetails = ReturnType< >; export enum AccountHistoryItemType { - Deposit, + AddTokens, LockedDeposit, Withdrawal, RewardsCredited, @@ -97,7 +97,7 @@ export enum AccountHistoryItemType { } const AccountHistoryAction = { - Deposit: () => ({ type: AccountHistoryItemType.Deposit as const }), + AddTokens: () => ({ type: AccountHistoryItemType.AddTokens as const }), LockedDeposit: (unlockDate: Date) => ({ type: AccountHistoryItemType.LockedDeposit as const, unlockDate, @@ -506,7 +506,7 @@ const MOCK_DATA: Record = { const mkMockHistory = (): AccountHistory => [ { timestamp: new Date("2024-06-10T00:00:00Z"), - action: AccountHistoryAction.Deposit(), + action: AccountHistoryAction.AddTokens(), amount: 2_000_000n, accountTotal: 2_000_000n, availableRewards: 0n, diff --git a/apps/staking/src/components/AccountHistory/index.tsx b/apps/staking/src/components/AccountHistory/index.tsx index dbc6653c3d..817e2759a4 100644 --- a/apps/staking/src/components/AccountHistory/index.tsx +++ b/apps/staking/src/components/AccountHistory/index.tsx @@ -1,32 +1,26 @@ -import useSWR from "swr"; +import { ArrowPathIcon } from "@heroicons/react/24/outline"; import { type AccountHistoryAction, type StakeDetails, AccountHistoryItemType, StakeType, - loadAccountHistory, } from "../../api"; -import { useApiContext } from "../../hooks/use-api-context"; -import { LoadingSpinner } from "../LoadingSpinner"; +import { StateType, useAccountHistory } from "../../hooks/use-account-history"; 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; - export const AccountHistory = () => { - const history = useAccountHistoryData(); + const history = useAccountHistory(); switch (history.type) { - case DataStateType.NotLoaded: - case DataStateType.Loading: { - return ; + case StateType.NotLoaded: + case StateType.Loading: { + return ; } - case DataStateType.Error: { + case StateType.Error: { return

Uh oh, an error occured!

; } - case DataStateType.Loaded: { + case StateType.Loaded: { return ( @@ -56,7 +50,9 @@ export const AccountHistory = () => { ) => ( - + @@ -82,37 +78,37 @@ export const AccountHistory = () => { } }; -const mkDescription = (action: AccountHistoryAction): string => { - switch (action.type) { +const Description = ({ children }: { children: AccountHistoryAction }) => { + switch (children.type) { case AccountHistoryItemType.Claim: { return "Rewards claimed"; } - case AccountHistoryItemType.Deposit: { - return "Tokens deposited"; + case AccountHistoryItemType.AddTokens: { + return "Tokens added"; } case AccountHistoryItemType.LockedDeposit: { - return `Locked tokens deposited, unlocking ${action.unlockDate.toLocaleString()}`; + return `Locked tokens deposited, unlocking ${children.unlockDate.toLocaleString()}`; } case AccountHistoryItemType.RewardsCredited: { return "Rewards credited"; } case AccountHistoryItemType.Slash: { - return `Staked tokens slashed from ${action.publisherName}`; + return `Staked tokens slashed from ${children.publisherName}`; } case AccountHistoryItemType.StakeCreated: { - return `Created stake position for ${getStakeDetails(action.details)}`; + return `Created stake position for ${getStakeDetails(children.details)}`; } case AccountHistoryItemType.StakeFinishedWarmup: { - return `Warmup complete for position for ${getStakeDetails(action.details)}`; + return `Warmup complete for position for ${getStakeDetails(children.details)}`; } case AccountHistoryItemType.Unlock: { return "Locked tokens unlocked"; } case AccountHistoryItemType.UnstakeCreated: { - return `Requested unstake for position for ${getStakeDetails(action.details)}`; + return `Requested unstake for position for ${getStakeDetails(children.details)}`; } case AccountHistoryItemType.UnstakeExitedCooldown: { - return `Cooldown completed for ${getStakeDetails(action.details)}`; + return `Cooldown completed for ${getStakeDetails(children.details)}`; } case AccountHistoryItemType.Withdrawal: { return "Tokens withdrawn to wallet"; @@ -130,46 +126,3 @@ const getStakeDetails = (details: StakeDetails): string => { } } }; - -const useAccountHistoryData = () => { - const apiContext = useApiContext(); - - const { data, isLoading, ...rest } = useSWR( - `${apiContext.stakeAccount.address.toBase58()}/history`, - () => loadAccountHistory(apiContext), - { - refreshInterval: REFRESH_INTERVAL, - }, - ); - const error = rest.error as unknown; - - if (error) { - return DataState.ErrorState(error); - } else if (isLoading) { - return DataState.Loading(); - } else if (data) { - return DataState.Loaded(data); - } else { - return DataState.NotLoaded(); - } -}; - -enum DataStateType { - NotLoaded, - Loading, - Loaded, - Error, -} -const DataState = { - NotLoaded: () => ({ type: DataStateType.NotLoaded as const }), - Loading: () => ({ type: DataStateType.Loading as const }), - Loaded: (data: Awaited>) => ({ - type: DataStateType.Loaded as const, - data, - }), - ErrorState: (error: unknown) => ({ - type: DataStateType.Error as const, - error, - }), -}; -type DataState = ReturnType<(typeof DataState)[keyof typeof DataState]>; diff --git a/apps/staking/src/components/AccountSummary/index.tsx b/apps/staking/src/components/AccountSummary/index.tsx index 6316275476..6e3f7855d8 100644 --- a/apps/staking/src/components/AccountSummary/index.tsx +++ b/apps/staking/src/components/AccountSummary/index.tsx @@ -47,16 +47,16 @@ export const AccountSummary = ({ -
+
-
+
Total Balance
- {total} + {total} {lastSlash && (

@@ -65,19 +65,11 @@ export const AccountSummary = ({

)}
-
- -
{locked > 0n && ( <> -
+
{locked} -
locked
+
locked included
-
+
{timestamp.toLocaleString()}{mkDescription(action)} + {action} + {amount}
@@ -101,7 +93,7 @@ export const AccountSummary = ({ {unlockSchedule.map((unlock, i) => ( - {!isSelf && ( <> - + {publisher.name} @@ -239,14 +277,14 @@ const Publisher = ({ publisher, availableToStake, isSelf }: PublisherProps) => { {publisher.qualityRanking} {availableToStake > 0 && ( { )} - {publisher.positions && ( + {(warmup !== undefined || staked !== undefined) && ( diff --git a/apps/staking/src/components/PositionFlowchart/index.tsx b/apps/staking/src/components/PositionFlowchart/index.tsx deleted file mode 100644 index b821d6c750..0000000000 --- a/apps/staking/src/components/PositionFlowchart/index.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { ArrowLongRightIcon } from "@heroicons/react/24/outline"; -import clsx from "clsx"; -import type { HTMLAttributes, ReactNode, ComponentProps } from "react"; - -import { getUpcomingEpoch, getNextFullEpoch } from "../../api"; -import { StakingTimeline } from "../StakingTimeline"; -import { Tokens } from "../Tokens"; -import { TransferButton } from "../TransferButton"; - -type Props = HTMLAttributes & { - locked?: bigint | undefined; - warmup: bigint; - staked: bigint; - cooldown: bigint; - cooldown2: bigint; - small?: boolean | undefined; -} & ( - | { - stake?: never; - stakeDescription?: never; - available?: bigint | undefined; - } - | { - available: bigint; - stake: ComponentProps["transfer"] | undefined; - stakeDescription: string; - } - ) & - ( - | { cancelWarmup?: never; cancelWarmupDescription?: never } - | { - cancelWarmup: - | ComponentProps["transfer"] - | undefined; - cancelWarmupDescription: string; - } - ) & - ( - | { unstake?: never; unstakeDescription?: never } - | { - unstake: ComponentProps["transfer"] | undefined; - unstakeDescription: string; - } - ); - -export const PositionFlowchart = ({ - className, - small, - locked, - available, - warmup, - staked, - cooldown, - cooldown2, - stake, - stakeDescription, - cancelWarmup, - cancelWarmupDescription, - unstake, - unstakeDescription, - ...props -}: Props) => ( -
- {locked !== undefined && ( - - {locked} - - )} - {available !== undefined && ( - <> - 0n && { - actions: ( - - - - ), - })} - > - {available} - - - - )} - 0n && { - details: ( -
- Staking {getUpcomingEpoch().toLocaleString()} -
- ), - ...(cancelWarmup !== undefined && { - actions: ( - - ), - }), - })} - > - {warmup} -
- - 0n && { - actions: ( - - - - ), - })} - > - {staked} - - - - {cooldown > 0n && ( -
- {cooldown} end{" "} - {getUpcomingEpoch().toLocaleString()} -
- )} - {cooldown2 > 0n && ( -
- {cooldown2} end{" "} - {getNextFullEpoch().toLocaleString()} -
- )} - - } - > - {cooldown + cooldown2} -
-
-); - -type PositionProps = { - name: string; - small: boolean | undefined; - nameClassName?: string | undefined; - className?: string | undefined; - children: bigint; - actions?: ReactNode | ReactNode[]; - details?: ReactNode; -}; - -const Position = ({ - name, - small, - nameClassName, - details, - className, - children, - actions, -}: PositionProps) => ( -
-
-
- {name} -
-
- - {children} - -
- {details} -
- {actions &&
{actions}
} -
-); - -const Arrow = () => ( -
- -
-); diff --git a/apps/staking/src/components/ProgramSection/index.tsx b/apps/staking/src/components/ProgramSection/index.tsx index d534eff8c0..8e7d2a5b83 100644 --- a/apps/staking/src/components/ProgramSection/index.tsx +++ b/apps/staking/src/components/ProgramSection/index.tsx @@ -1,38 +1,226 @@ +import { ArrowLongDownIcon } from "@heroicons/react/24/outline"; import clsx from "clsx"; -import type { HTMLAttributes, ComponentProps } from "react"; +import type { HTMLAttributes, ReactNode, ComponentProps } from "react"; -import { PositionFlowchart } from "../PositionFlowchart"; +import { getUpcomingEpoch, getNextFullEpoch } from "../../api"; +import { StakingTimeline } from "../StakingTimeline"; +import { Tokens } from "../Tokens"; +import { TransferButton } from "../TransferButton"; -type Props = HTMLAttributes & { +type Props = HTMLAttributes & { name: string; description: string; - positions: ComponentProps; -}; + warmup: bigint; + staked: bigint; + cooldown: bigint; + cooldown2: bigint; + availableToStakeDetails?: ReactNode | ReactNode[] | undefined; +} & ( + | { + stake?: never; + stakeDescription?: never; + available?: bigint | undefined; + } + | { + available: bigint; + stake: ComponentProps["transfer"] | undefined; + stakeDescription: string; + } + ) & + ( + | { cancelWarmup?: never; cancelWarmupDescription?: never } + | { + cancelWarmup: + | ComponentProps["transfer"] + | undefined; + cancelWarmupDescription: string; + } + ) & + ( + | { unstake?: never; unstakeDescription?: never } + | { + unstake: ComponentProps["transfer"] | undefined; + unstakeDescription: string; + } + ); export const ProgramSection = ({ className, name, description, children, - positions, + warmup, + staked, + cooldown, + cooldown2, + availableToStakeDetails, + stake, + stakeDescription, + available, + cancelWarmup, + cancelWarmupDescription, + unstake, + unstakeDescription, ...props }: Props) => (
-

{name}

-

{description}

- {name} +

{description}

+
+ {available !== undefined && ( + <> + 0n && { + actions: ( + + + + ), + })} + > + {available} + + + )} - /> + 0n && { + details: ( +
+ Staking {getUpcomingEpoch().toLocaleString()} +
+ ), + ...(cancelWarmup !== undefined && { + actions: ( + + ), + }), + })} + > + {warmup} +
+ + 0n && { + actions: ( + + + + ), + })} + > + {staked} + + + + {cooldown > 0n && ( +
+ {cooldown} end{" "} + {getUpcomingEpoch().toLocaleString()} +
+ )} + {cooldown2 > 0n && ( +
+ {cooldown2} end{" "} + {getNextFullEpoch().toLocaleString()} +
+ )} + + } + > + {cooldown + cooldown2} +
+
{children}
); + +type PositionProps = { + name: string; + nameClassName?: string | undefined; + className?: string | undefined; + children: bigint; + actions?: ReactNode | ReactNode[]; + details?: ReactNode; +}; + +const Position = ({ + name, + nameClassName, + details, + className, + children, + actions, +}: PositionProps) => ( +
+
+ {name} +
+
+
+
+ {children} +
+ {details} +
+ {actions &&
{actions}
} +
+
+); + +const Arrow = () => ( +
+ +
+); diff --git a/apps/staking/src/components/Tokens/index.tsx b/apps/staking/src/components/Tokens/index.tsx index 50dd10d055..a4e62419d9 100644 --- a/apps/staking/src/components/Tokens/index.tsx +++ b/apps/staking/src/components/Tokens/index.tsx @@ -1,26 +1,52 @@ import clsx from "clsx"; -import { useMemo, type HTMLAttributes } from "react"; +import * as dnum from "dnum"; +import { type ComponentProps, useMemo } from "react"; +import { Button, TooltipTrigger } from "react-aria-components"; import Pyth from "./pyth.svg"; -import { tokensToString } from "../../tokens"; +import { DECIMALS } from "../../tokens"; +import { Tooltip } from "../Tooltip"; -type Props = Omit, "children"> & { +type Props = Omit, "children"> & { children: bigint; }; -export const Tokens = ({ children, className, ...props }: Props) => { - const value = useMemo(() => tokensToString(children), [children]); +export const Tokens = ({ children, ...props }: Props) => { + const compactValue = useMemo( + () => dnum.format([children, DECIMALS], { compact: true }), + [children], + ); + const fullValue = useMemo( + () => dnum.format([children, DECIMALS]), + [children], + ); - return ( - - - {value} - + return compactValue === fullValue ? ( + {compactValue} + ) : ( + + {compactValue} + + + {fullValue} + + ); }; + +type TokenButtonProps = Omit, "children"> & { + children: string; +}; + +const TokenButton = ({ children, className, ...props }: TokenButtonProps) => ( + +); diff --git a/apps/staking/src/components/Tooltip/index.tsx b/apps/staking/src/components/Tooltip/index.tsx new file mode 100644 index 0000000000..fd095e9896 --- /dev/null +++ b/apps/staking/src/components/Tooltip/index.tsx @@ -0,0 +1,25 @@ +import clsx from "clsx"; +import type { ComponentProps } from "react"; +import { OverlayArrow, Tooltip as TooltipImpl } from "react-aria-components"; + +type Props = Omit, "children"> & { + children: React.ReactNode; +}; + +export const Tooltip = ({ children, className, offset, ...props }: Props) => ( + + + + + + + {children} + +); diff --git a/apps/staking/src/components/TransferButton/index.tsx b/apps/staking/src/components/TransferButton/index.tsx index 8d0acd3aff..4c6229bccc 100644 --- a/apps/staking/src/components/TransferButton/index.tsx +++ b/apps/staking/src/components/TransferButton/index.tsx @@ -82,7 +82,7 @@ export const TransferButton = ({ > {(close) => ( <> - +
@@ -112,7 +112,9 @@ export const TransferButton = ({
{state.type === StateType.Error && ( -

Uh oh, an error occurred!

+

+ Uh oh, an error occurred! Please try again +

)}
{children && ( diff --git a/apps/staking/src/components/WalletButton/index.tsx b/apps/staking/src/components/WalletButton/index.tsx index f56c30a6f8..7040325d31 100644 --- a/apps/staking/src/components/WalletButton/index.tsx +++ b/apps/staking/src/components/WalletButton/index.tsx @@ -81,7 +81,7 @@ const ConnectedButton = (props: Props) => { @@ -93,7 +93,7 @@ const ConnectedButton = (props: Props) => { {stakeAccountState.type === StateType.Loaded && @@ -107,7 +107,7 @@ const ConnectedButton = (props: Props) => { @@ -201,7 +201,7 @@ const WalletMenuItemImpl = ( return ( { }, [modal]); return ( - + Connect wallet ); @@ -241,7 +241,10 @@ const ButtonComponent = ({ ...props }: ButtonComponentProps) => (
+ {unlock.date.toLocaleString()} @@ -116,12 +108,20 @@ export const AccountSummary = ({ )} +
+ +
-
+
} {...(expiringRewards !== undefined && expiringRewards.amount > 0n && { @@ -169,7 +169,7 @@ const BalanceCategory = ({ action, warning, }: BalanceCategoryProps) => ( -
+
{name} diff --git a/apps/staking/src/components/Amplitude/index.tsx b/apps/staking/src/components/Amplitude/index.tsx index 823261f790..7df41dbc5e 100644 --- a/apps/staking/src/components/Amplitude/index.tsx +++ b/apps/staking/src/components/Amplitude/index.tsx @@ -1,25 +1,13 @@ "use client"; -import * as amplitude from "@amplitude/analytics-browser"; -import { autocapturePlugin } from "@amplitude/plugin-autocapture-browser"; -import { useEffect, useRef } from "react"; +import { useAmplitude } from "../../hooks/use-amplitude"; type Props = { apiKey: string | undefined; }; export const Amplitude = ({ apiKey }: Props) => { - const amplitudeInitialized = useRef(false); - - useEffect(() => { - if (!amplitudeInitialized.current && apiKey) { - amplitude.add(autocapturePlugin()); - amplitude.init(apiKey, { - defaultTracking: true, - }); - amplitudeInitialized.current = true; - } - }, [apiKey]); + useAmplitude(apiKey); // eslint-disable-next-line unicorn/no-null return null; diff --git a/apps/staking/src/components/Button/index.tsx b/apps/staking/src/components/Button/index.tsx index a98cd3a7b3..52f3852727 100644 --- a/apps/staking/src/components/Button/index.tsx +++ b/apps/staking/src/components/Button/index.tsx @@ -29,5 +29,5 @@ const ButtonBase = ({ export const Button = Styled( ButtonBase, - "border border-pythpurple-600 bg-pythpurple-600/50 data-[small]:text-sm data-[small]:px-6 data-[small]:py-1 data-[secondary]:bg-pythpurple-600/20 px-8 py-2 data-[nopad]:px-0 data-[nopad]:py-0 disabled:cursor-not-allowed disabled:bg-neutral-50/10 disabled:border-neutral-50/10 disabled:text-white/60 disabled:data-[loading]:cursor-wait hover:bg-pythpurple-600/60 data-[secondary]:hover:bg-pythpurple-600/60 data-[secondary]:disabled:bg-neutral-50/10 focus-visible:ring-1 focus-visible:ring-pythpurple-400 focus:outline-none justify-center", + "border border-pythpurple-600 bg-pythpurple-600/50 data-[small]:text-sm data-[small]:px-6 data-[small]:py-1 data-[secondary]:bg-pythpurple-600/20 px-2 sm:px-4 md:px-8 py-2 data-[nopad]:px-0 data-[nopad]:py-0 disabled:cursor-not-allowed disabled:bg-neutral-50/10 disabled:border-neutral-50/10 disabled:text-white/60 disabled:data-[loading]:cursor-wait hover:bg-pythpurple-600/60 data-[secondary]:hover:bg-pythpurple-600/60 data-[secondary]:disabled:bg-neutral-50/10 focus-visible:ring-1 focus-visible:ring-pythpurple-400 focus:outline-none", ); diff --git a/apps/staking/src/components/Dashboard/index.tsx b/apps/staking/src/components/Dashboard/index.tsx index f5fb200032..6502729583 100644 --- a/apps/staking/src/components/Dashboard/index.tsx +++ b/apps/staking/src/components/Dashboard/index.tsx @@ -118,10 +118,15 @@ export const Dashboard = ({ expiringRewards={expiringRewards} /> - + Overview Governance - Oracle Integrity Staking + + Integrity Staking + + Oracle Integrity Staking (OIS) + + @@ -159,7 +164,7 @@ export const Dashboard = ({ const DashboardTab = Styled( Tab, - "grow border-b border-neutral-600/50 px-4 py-2 focus-visible:outline-none data-[selected]:cursor-default data-[selected]:border-pythpurple-400 data-[selected]:data-[hover]:bg-transparent data-[hover]:text-pythpurple-400 data-[selected]:text-pythpurple-400 data-[focus]:outline-none data-[focus]:ring-1 data-[focus]:ring-pythpurple-400", + "grow basis-0 border-b border-neutral-600/50 px-4 py-2 focus-visible:outline-none data-[selected]:cursor-default data-[selected]:border-pythpurple-400 data-[selected]:data-[hover]:bg-transparent data-[hover]:text-pythpurple-400 data-[selected]:text-pythpurple-400 data-[focus]:outline-none data-[focus]:ring-1 data-[focus]:ring-pythpurple-400", ); const DashboardTabPanel = Styled( diff --git a/apps/staking/src/components/Error/index.tsx b/apps/staking/src/components/Error/index.tsx index 14841a8e70..7644dae7b2 100644 --- a/apps/staking/src/components/Error/index.tsx +++ b/apps/staking/src/components/Error/index.tsx @@ -1,13 +1,33 @@ +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; +import { useEffect } from "react"; + +import { useLogger } from "../../hooks/use-logger"; +import { Button } from "../Button"; + type Props = { error: Error & { digest?: string }; reset?: () => void; }; -export const Error = ({ error, reset }: Props) => ( -
-

Uh oh!

-

Something went wrong

-

Error Code: {error.digest}

- {reset && } -
-); +export const Error = ({ error, reset }: Props) => { + const logger = useLogger(); + + useEffect(() => { + logger.error(error); + }, [error, logger]); + + return ( +
+
+ +

Uh oh!

+
+

Something went wrong

+
Error Details:
+ + {error.digest ?? error.message} + + {reset && } +
+ ); +}; diff --git a/apps/staking/src/components/Footer/index.tsx b/apps/staking/src/components/Footer/index.tsx index 585734867c..1fc275dcfa 100644 --- a/apps/staking/src/components/Footer/index.tsx +++ b/apps/staking/src/components/Footer/index.tsx @@ -42,7 +42,7 @@ export const Footer = ({ {...props} >
- +
© 2024 Pyth Data Association
{SOCIAL_LINKS.map(({ name, icon: Icon, href }) => ( @@ -50,7 +50,7 @@ export const Footer = ({ target="_blank" href={href} key={name} - className="grid h-full place-content-center px-3 hover:text-pythpurple-400" + className="grid h-full place-content-center px-2 hover:text-pythpurple-400 sm:px-3" rel="noreferrer" > diff --git a/apps/staking/src/components/Governance/index.tsx b/apps/staking/src/components/Governance/index.tsx index d754112077..34c5a8dbf9 100644 --- a/apps/staking/src/components/Governance/index.tsx +++ b/apps/staking/src/components/Governance/index.tsx @@ -23,19 +23,16 @@ export const Governance = ({ ); diff --git a/apps/staking/src/components/Header/index.tsx b/apps/staking/src/components/Header/index.tsx index 5076fbdf66..381c3c2030 100644 --- a/apps/staking/src/components/Header/index.tsx +++ b/apps/staking/src/components/Header/index.tsx @@ -16,9 +16,9 @@ export const Header = ({ {...props} >
- - - + + +
diff --git a/apps/staking/src/components/Home/index.tsx b/apps/staking/src/components/Home/index.tsx index 8dd1dd5760..3c9c456a08 100644 --- a/apps/staking/src/components/Home/index.tsx +++ b/apps/staking/src/components/Home/index.tsx @@ -3,53 +3,53 @@ import { useWalletModal } from "@solana/wallet-adapter-react-ui"; import { useCallback } from "react"; import { useIsSSR } from "react-aria"; -import useSWR from "swr"; -import { loadData } from "../../api"; -import { useApiContext } from "../../hooks/use-api-context"; -import { useLogger } from "../../hooks/use-logger"; -import { StateType, useStakeAccount } from "../../hooks/use-stake-account"; +import { + StateType as DashboardDataStateType, + useDashboardData, +} from "../../hooks/use-dashboard-data"; +import { + StateType as StakeAccountStateType, + useStakeAccount, +} from "../../hooks/use-stake-account"; import { Button } from "../Button"; import { Dashboard } from "../Dashboard"; -import { LoadingSpinner } from "../LoadingSpinner"; - -const ONE_SECOND_IN_MS = 1000; -const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS; -const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS; +import { Error as ErrorPage } from "../Error"; +import { Loading } from "../Loading"; export const Home = () => { const isSSR = useIsSSR(); - return ( -
- {isSSR ? : } -
- ); + return isSSR ? : ; }; const MountedHome = () => { const stakeAccountState = useStakeAccount(); switch (stakeAccountState.type) { - case StateType.Initialized: - case StateType.Loading: { - return ; + case StakeAccountStateType.Initialized: + case StakeAccountStateType.Loading: { + return ; } - case StateType.NoAccounts: { - return

No stake account found for your wallet!

; + case StakeAccountStateType.NoAccounts: { + return ( +
+

No stake account found for your wallet!

+
+ ); } - case StateType.NoWallet: { + case StakeAccountStateType.NoWallet: { return ; } - case StateType.Error: { + case StakeAccountStateType.Error: { return ( -

- Uh oh, an error occurred while loading stake accounts. Please refresh - and try again -

+ ); } - case StateType.Loaded: { + case StakeAccountStateType.Loaded: { return ; } } @@ -62,7 +62,7 @@ const NoWalletHome = () => { }, [modal]); return ( - <> +

Staking & Delegating

@@ -74,68 +74,29 @@ const NoWalletHome = () => {
- +
); }; const StakeAccountLoadedHome = () => { const data = useDashboardData(); - const logger = useLogger(); switch (data.type) { - case DataStateType.NotLoaded: - case DataStateType.Loading: { - return ; - } - case DataStateType.Error: { - logger.error(data.error); - return

Uh oh, an error occured!

; + case DashboardDataStateType.NotLoaded: + case DashboardDataStateType.Loading: { + return ; } - case DataStateType.Loaded: { - return ; - } - } -}; - -const useDashboardData = () => { - const apiContext = useApiContext(); - const { data, isLoading, ...rest } = useSWR( - apiContext.stakeAccount.address.toBase58(), - () => loadData(apiContext), - { - refreshInterval: REFRESH_INTERVAL, - }, - ); - const error = rest.error as unknown; + case DashboardDataStateType.Error: { + return ; + } - if (error) { - return DataState.ErrorState(error); - } else if (isLoading) { - return DataState.Loading(); - } else if (data) { - return DataState.Loaded(data); - } else { - return DataState.NotLoaded(); + case DashboardDataStateType.Loaded: { + return ( +
+ +
+ ); + } } }; - -enum DataStateType { - NotLoaded, - Loading, - Loaded, - Error, -} -const DataState = { - NotLoaded: () => ({ type: DataStateType.NotLoaded as const }), - Loading: () => ({ type: DataStateType.Loading as const }), - Loaded: (data: Awaited>) => ({ - type: DataStateType.Loaded as const, - data, - }), - ErrorState: (error: unknown) => ({ - type: DataStateType.Error as const, - error, - }), -}; -type DataState = ReturnType<(typeof DataState)[keyof typeof DataState]>; diff --git a/apps/staking/src/components/Loading/index.tsx b/apps/staking/src/components/Loading/index.tsx new file mode 100644 index 0000000000..edff2e5655 --- /dev/null +++ b/apps/staking/src/components/Loading/index.tsx @@ -0,0 +1,7 @@ +export const Loading = () => ( +
+
+
+
+
+); diff --git a/apps/staking/src/components/LoadingSpinner/index.tsx b/apps/staking/src/components/LoadingSpinner/index.tsx deleted file mode 100644 index 032c5242ad..0000000000 --- a/apps/staking/src/components/LoadingSpinner/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ArrowPathIcon } from "@heroicons/react/24/outline"; - -import { Styled } from "../Styled"; - -export const LoadingSpinner = Styled(ArrowPathIcon, "size-6 animate-spin"); diff --git a/apps/staking/src/components/MaxWidth/index.tsx b/apps/staking/src/components/MaxWidth/index.tsx index 22fb3f89f7..c9e5a0ba9a 100644 --- a/apps/staking/src/components/MaxWidth/index.tsx +++ b/apps/staking/src/components/MaxWidth/index.tsx @@ -1,3 +1,3 @@ import { Styled } from "../Styled"; -export const MaxWidth = Styled("div", "px-12"); +export const MaxWidth = Styled("div", "w-full px-6 sm:px-12 overflow-hidden"); diff --git a/apps/staking/src/components/Modal/index.tsx b/apps/staking/src/components/Modal/index.tsx index 87acb808c0..88af3fe513 100644 --- a/apps/staking/src/components/Modal/index.tsx +++ b/apps/staking/src/components/Modal/index.tsx @@ -118,7 +118,7 @@ export const RawModal = ({
{title} diff --git a/apps/staking/src/components/NotFound/index.tsx b/apps/staking/src/components/NotFound/index.tsx index 370fa3a33d..fca29b05ff 100644 --- a/apps/staking/src/components/NotFound/index.tsx +++ b/apps/staking/src/components/NotFound/index.tsx @@ -1,15 +1,17 @@ import Link from "next/link"; +import { Button } from "../Button"; + export const NotFound = () => (
-

+

Not Found

{"The page you're looking for isn't here"}

- +
); diff --git a/apps/staking/src/components/OracleIntegrityStaking/index.tsx b/apps/staking/src/components/OracleIntegrityStaking/index.tsx index 5c565407a5..b33a53861d 100644 --- a/apps/staking/src/components/OracleIntegrityStaking/index.tsx +++ b/apps/staking/src/components/OracleIntegrityStaking/index.tsx @@ -8,7 +8,6 @@ import { unstakeIntegrityStaking, calculateApy, } from "../../api"; -import { PositionFlowchart } from "../PositionFlowchart"; import { ProgramSection } from "../ProgramSection"; import { SparkChart } from "../SparkChart"; import { StakingTimeline } from "../StakingTimeline"; @@ -47,80 +46,96 @@ export const OracleIntegrityStaking = ({ return ( 0n && { + availableToStakeDetails: ( +
+ {locked} are locked and cannot be staked in OIS +
+ ), + })} > {self && ( -
- - - + + +
+
+
+

You ({self.name}) -

+ + + Pool + Last epoch APY + Historical APY + Number of feeds + Quality ranking + {availableToStake > 0n && } + + + + + +
+
+
+ )} +
+
+

+ {self ? "Other Publishers" : "Publishers"} +

+ + + + + Publisher + + Self stakePool - APY + Last epoch APYHistorical APYNumber of feeds - Quality ranking - {availableToStake > 0n && } + + Quality ranking + + {availableToStake > 0n && ( + + )} - - + + {otherPublishers.map((publisher) => ( + + ))}
- )} -
- - - - - - Publisher - - Self stake - Pool - APY - Historical APY - Number of feeds - - Quality ranking - - {availableToStake > 0n && ( - - )} - - - - {otherPublishers.map((publisher) => ( - - ))} - -
- {self ? "Other Publishers" : "Publishers"} -
); @@ -133,6 +148,7 @@ const PublisherTableHeader = Styled( type PublisherProps = { availableToStake: bigint; + totalStaked: bigint; isSelf?: boolean; publisher: { name: string; @@ -155,7 +171,29 @@ type PublisherProps = { }; }; -const Publisher = ({ publisher, availableToStake, isSelf }: PublisherProps) => { +const Publisher = ({ + publisher, + availableToStake, + totalStaked, + isSelf, +}: PublisherProps) => { + const warmup = useMemo( + () => + publisher.positions?.warmup !== undefined && + publisher.positions.warmup > 0n + ? publisher.positions.warmup + : undefined, + [publisher.positions?.warmup], + ); + const staked = useMemo( + () => + publisher.positions?.staked !== undefined && + publisher.positions.staked > 0n + ? publisher.positions.staked + : undefined, + [publisher.positions?.staked], + ); + const cancelWarmup = useTransferActionForPublisher( cancelWarmupIntegrityStaking, publisher.publicKey, @@ -174,7 +212,7 @@ const Publisher = ({ publisher, availableToStake, isSelf }: PublisherProps) => {
-
- +
+ + + + {warmup !== undefined && ( + + + + + + )} + {staked !== undefined && ( + + + + + + )} + +
+ Your Positions +
Warmup + {warmup} + + +
Staked +
+ {staked} +
+ ({Number((100n * staked) / totalStaked)}% of your + staked tokens) +
+
+
+ + + +