Skip to content

Commit

Permalink
Merge pull request #1918 from cprussin/more-feedback
Browse files Browse the repository at this point in the history
feat(staking): implement some feedback items
  • Loading branch information
cprussin authored Sep 13, 2024
2 parents 122d340 + 7aa0da7 commit 106b09b
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 203 deletions.
1 change: 1 addition & 0 deletions apps/staking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@next/third-parties": "^14.2.5",
"@pythnetwork/hermes-client": "workspace:*",
"@pythnetwork/staking-sdk": "workspace:*",
"@react-hookz/web": "^24.0.4",
"@solana/wallet-adapter-base": "^0.9.20",
"@solana/wallet-adapter-react": "^0.15.28",
"@solana/wallet-adapter-react-ui": "^0.9.27",
Expand Down
8 changes: 7 additions & 1 deletion apps/staking/src/components/AccountHistory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ import type { States, StateType as ApiStateType } from "../../hooks/use-api";
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;

type Props = { api: States[ApiStateType.Loaded] };

export const AccountHistory = ({ api }: Props) => {
const history = useData(api.accountHistoryCacheKey, api.loadAccountHistory);
const history = useData(api.accountHistoryCacheKey, api.loadAccountHistory, {
refreshInterval: REFRESH_INTERVAL,
});

switch (history.type) {
case StateType.NotLoaded:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const CurrentStakeAccount = ({

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="flex flex-col items-end text-xs md:flex-row md:items-center md:text-sm">
<div className="font-semibold">Stake account:</div>
<CopyButton
text={api.account.toBase58()}
Expand Down
10 changes: 9 additions & 1 deletion apps/staking/src/components/Home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { Error as ErrorPage } from "../Error";
import { Loading } from "../Loading";
import { NoWalletHome } from "../NoWalletHome";

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 Home = () => {
const isSSR = useIsSSR();

Expand All @@ -30,6 +34,8 @@ const MountedHome = () => {
case ApiStateType.LoadingStakeAccounts: {
return <Loading />;
}
case ApiStateType.WalletDisconnecting:
case ApiStateType.WalletConnecting:
case ApiStateType.NoWallet: {
return <NoWalletHome />;
}
Expand All @@ -48,7 +54,9 @@ type StakeAccountLoadedHomeProps = {
};

const StakeAccountLoadedHome = ({ api }: StakeAccountLoadedHomeProps) => {
const data = useData(api.dashboardDataCacheKey, api.loadData);
const data = useData(api.dashboardDataCacheKey, api.loadData, {
refreshInterval: REFRESH_INTERVAL,
});

switch (data.type) {
case DashboardDataStateType.NotLoaded:
Expand Down
19 changes: 9 additions & 10 deletions apps/staking/src/components/OracleIntegrityStaking/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ const OptOutButton = ({ api, self }: OptOutButtonProps) => {

const doOptOut = useCallback(() => {
execute().catch(() => {
/* TODO figure out a better UI treatment for when claim fails */
/* no-op since this is already handled in the UI using `state` and is logged in useTransfer */
});
}, [execute]);

Expand Down Expand Up @@ -375,13 +375,9 @@ const OptOutButton = ({ api, self }: OptOutButtonProps) => {
</p>
<p className="opacity-90">
Opting out of rewards will prevent you from earning the
publisher yield rate. You will still be able to participate in
OIS after opting out of rewards, but{" "}
<PublisherName className="font-semibold">
{self}
</PublisherName>{" "}
will no longer be able to receive delegated stake, and you
will no longer receive the self-staking yield.
publisher yield rate and delegation fees from your delegators.
You will still be able to participate in OIS after opting out
of rewards.
</p>
</div>
{state.type === UseAsyncStateType.Error && (
Expand Down Expand Up @@ -879,7 +875,7 @@ const Publisher = ({
poolUtilization:
publisher.poolUtilization + publisher.poolUtilizationDelta,
yieldRate,
})}
}).toFixed(2)}
%
</div>
</PublisherTableCell>
Expand Down Expand Up @@ -1047,7 +1043,10 @@ const StakeToPublisherButton = ({
publisher={publisher}
yieldRate={yieldRate}
>
{amount.type === AmountType.Valid ? amount.amount : 0n}
{amount.type === AmountType.Valid ||
amount.type === AmountType.AboveMax
? amount.amount
: 0n}
</NewApy>
</div>
<StakingTimeline currentEpoch={currentEpoch} />
Expand Down
147 changes: 118 additions & 29 deletions apps/staking/src/components/WalletButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@heroicons/react/24/outline";
import { useWallet } from "@solana/wallet-adapter-react";
import { useWalletModal } from "@solana/wallet-adapter-react-ui";
import type { PublicKey } from "@solana/web3.js";
import clsx from "clsx";
import { useSelectedLayoutSegment } from "next/navigation";
import {
Expand All @@ -21,6 +22,8 @@ import {
type ReactNode,
useCallback,
useState,
useMemo,
type ReactElement,
} from "react";
import {
Menu,
Expand All @@ -30,6 +33,8 @@ import {
Separator,
Section,
SubmenuTrigger,
Header,
Collection,
} from "react-aria-components";

import {
Expand All @@ -41,13 +46,18 @@ import {
type States,
useApi,
} from "../../hooks/use-api";
import { StateType as DataStateType, useData } from "../../hooks/use-data";
import { useLogger } from "../../hooks/use-logger";
import { usePrimaryDomain } from "../../hooks/use-primary-domain";
import { AccountHistory } from "../AccountHistory";
import { Button } from "../Button";
import { ModalDialog } from "../ModalDialog";
import { TruncatedKey } from "../TruncatedKey";

const ONE_SECOND_IN_MS = 1000;
const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;

type Props = Omit<ComponentProps<typeof Button>, "onClick" | "children">;

export const WalletButton = (props: Props) => {
Expand All @@ -63,6 +73,15 @@ const WalletButtonImpl = (props: Props) => {
const api = useApi();

switch (api.type) {
case ApiStateType.WalletDisconnecting:
case ApiStateType.WalletConnecting: {
return (
<ButtonComponent isLoading={true} {...props}>
Loading...
</ButtonComponent>
);
}

case ApiStateType.NotLoaded:
case ApiStateType.NoWallet: {
return <DisconnectedButton {...props} />;
Expand Down Expand Up @@ -125,40 +144,15 @@ const ConnectedButton = ({
{api.type === ApiStateType.Loaded && (
<>
<Section className="flex w-full flex-col">
<SubmenuTrigger>
<StakeAccountSelector api={api}>
<WalletMenuItem
icon={BanknotesIcon}
textValue="Select stake account"
>
<span>Select stake account</span>
<ChevronRightIcon className="size-4" />
</WalletMenuItem>
<StyledMenu
items={api.allAccounts.map((account) => ({
account,
id: account.toBase58(),
}))}
>
{(item) => (
<WalletMenuItem
onAction={() => {
api.selectAccount(item.account);
}}
className={clsx({
"font-semibold": item.account === api.account,
})}
isDisabled={item.account === api.account}
>
<CheckIcon
className={clsx("size-4 text-pythpurple-600", {
invisible: item.account !== api.account,
})}
/>
<TruncatedKey>{item.account}</TruncatedKey>
</WalletMenuItem>
)}
</StyledMenu>
</SubmenuTrigger>
</StakeAccountSelector>
<WalletMenuItem
onAction={openAccountHistory}
icon={TableCellsIcon}
Expand Down Expand Up @@ -193,14 +187,109 @@ const ConnectedButton = ({
);
};

type StakeAccountSelectorProps = {
api: States[ApiStateType.Loaded];
children: ReactElement;
};

const StakeAccountSelector = ({ children, api }: StakeAccountSelectorProps) => {
const data = useData(api.dashboardDataCacheKey, api.loadData, {
refreshInterval: REFRESH_INTERVAL,
});
const accounts = useMemo(() => {
if (data.type === DataStateType.Loaded) {
const main = api.allAccounts.find((account) =>
data.data.integrityStakingPublishers.some((publisher) =>
publisher.stakeAccount?.equals(account),
),
);
const other = api.allAccounts
.filter((account) => account !== main)
.map((account) => ({
account,
id: account.toBase58(),
}));
return { main, other };
} else {
return;
}
}, [data, api]);

if (accounts === undefined) {
// eslint-disable-next-line unicorn/no-null
return null;
} else if (accounts.main === undefined) {
return accounts.other.length > 1 ? (
<SubmenuTrigger>
{children}
<StyledMenu items={accounts.other}>
{({ account }) => <AccountMenuItem account={account} api={api} />}
</StyledMenu>
</SubmenuTrigger>
) : // eslint-disable-next-line unicorn/no-null
null;
} else {
return (
<SubmenuTrigger>
{children}
<StyledMenu>
<Section className="flex w-full flex-col">
<Header className="mx-4 text-sm font-semibold">Main Account</Header>
<AccountMenuItem account={accounts.main} api={api} />
</Section>
{accounts.other.length > 0 && (
<>
<Separator className="mx-2 my-1 h-px bg-black/20" />
<Section className="flex w-full flex-col">
<Header className="mx-4 text-sm font-semibold">
Other Accounts
</Header>
<Collection items={accounts.other}>
{({ account }) => (
<AccountMenuItem account={account} api={api} />
)}
</Collection>
</Section>
</>
)}
</StyledMenu>
</SubmenuTrigger>
);
}
};

type AccountMenuItemProps = {
api: States[ApiStateType.Loaded];
account: PublicKey;
};

const AccountMenuItem = ({ account, api }: AccountMenuItemProps) => (
<WalletMenuItem
onAction={() => {
api.selectAccount(account);
}}
className={clsx({
"pr-8 font-semibold": account === api.account,
})}
isDisabled={account === api.account}
>
<CheckIcon
className={clsx("size-4 text-pythpurple-600", {
invisible: account !== api.account,
})}
/>
<TruncatedKey>{account}</TruncatedKey>
</WalletMenuItem>
);

const StyledMenu = <T extends object>({
className,
...props
}: ComponentProps<typeof Menu<T>>) => (
<Popover className="data-[entering]:animate-in data-[exiting]:animate-out data-[entering]:fade-in data-[exiting]:fade-out focus:outline-none focus-visible:outline-none focus-visible:ring-0">
<Popover className="data-[entering]:animate-in data-[exiting]:animate-out data-[entering]:fade-in data-[exiting]:fade-out focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0">
<Menu
className={clsx(
"flex origin-top-right flex-col border border-neutral-400 bg-pythpurple-100 py-2 text-sm text-pythpurple-950 shadow shadow-neutral-400 focus:outline-none focus-visible:outline-none focus-visible:ring-0",
"flex origin-top-right flex-col border border-neutral-400 bg-pythpurple-100 py-2 text-sm text-pythpurple-950 shadow shadow-neutral-400 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0",
className,
)}
{...props}
Expand Down
Loading

0 comments on commit 106b09b

Please sign in to comment.