Skip to content

Commit 106b09b

Browse files
authored
Merge pull request #1918 from cprussin/more-feedback
feat(staking): implement some feedback items
2 parents 122d340 + 7aa0da7 commit 106b09b

File tree

9 files changed

+346
-203
lines changed

9 files changed

+346
-203
lines changed

apps/staking/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@next/third-parties": "^14.2.5",
2929
"@pythnetwork/hermes-client": "workspace:*",
3030
"@pythnetwork/staking-sdk": "workspace:*",
31+
"@react-hookz/web": "^24.0.4",
3132
"@solana/wallet-adapter-base": "^0.9.20",
3233
"@solana/wallet-adapter-react": "^0.15.28",
3334
"@solana/wallet-adapter-react-ui": "^0.9.27",

apps/staking/src/components/AccountHistory/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ import type { States, StateType as ApiStateType } from "../../hooks/use-api";
1010
import { StateType, useData } from "../../hooks/use-data";
1111
import { Tokens } from "../Tokens";
1212

13+
const ONE_SECOND_IN_MS = 1000;
14+
const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
15+
const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;
16+
1317
type Props = { api: States[ApiStateType.Loaded] };
1418

1519
export const AccountHistory = ({ api }: Props) => {
16-
const history = useData(api.accountHistoryCacheKey, api.loadAccountHistory);
20+
const history = useData(api.accountHistoryCacheKey, api.loadAccountHistory, {
21+
refreshInterval: REFRESH_INTERVAL,
22+
});
1723

1824
switch (history.type) {
1925
case StateType.NotLoaded:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const CurrentStakeAccount = ({
1515

1616
return api.type === ApiStateType.Loaded ? (
1717
<div className={clsx("grid place-content-center", className)} {...props}>
18-
<div className="flex flex-col items-end text-xs md:flex-row md:items-baseline md:text-sm">
18+
<div className="flex flex-col items-end text-xs md:flex-row md:items-center md:text-sm">
1919
<div className="font-semibold">Stake account:</div>
2020
<CopyButton
2121
text={api.account.toBase58()}

apps/staking/src/components/Home/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import { Error as ErrorPage } from "../Error";
1616
import { Loading } from "../Loading";
1717
import { NoWalletHome } from "../NoWalletHome";
1818

19+
const ONE_SECOND_IN_MS = 1000;
20+
const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
21+
const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;
22+
1923
export const Home = () => {
2024
const isSSR = useIsSSR();
2125

@@ -30,6 +34,8 @@ const MountedHome = () => {
3034
case ApiStateType.LoadingStakeAccounts: {
3135
return <Loading />;
3236
}
37+
case ApiStateType.WalletDisconnecting:
38+
case ApiStateType.WalletConnecting:
3339
case ApiStateType.NoWallet: {
3440
return <NoWalletHome />;
3541
}
@@ -48,7 +54,9 @@ type StakeAccountLoadedHomeProps = {
4854
};
4955

5056
const StakeAccountLoadedHome = ({ api }: StakeAccountLoadedHomeProps) => {
51-
const data = useData(api.dashboardDataCacheKey, api.loadData);
57+
const data = useData(api.dashboardDataCacheKey, api.loadData, {
58+
refreshInterval: REFRESH_INTERVAL,
59+
});
5260

5361
switch (data.type) {
5462
case DashboardDataStateType.NotLoaded:

apps/staking/src/components/OracleIntegrityStaking/index.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ const OptOutButton = ({ api, self }: OptOutButtonProps) => {
343343

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

@@ -375,13 +375,9 @@ const OptOutButton = ({ api, self }: OptOutButtonProps) => {
375375
</p>
376376
<p className="opacity-90">
377377
Opting out of rewards will prevent you from earning the
378-
publisher yield rate. You will still be able to participate in
379-
OIS after opting out of rewards, but{" "}
380-
<PublisherName className="font-semibold">
381-
{self}
382-
</PublisherName>{" "}
383-
will no longer be able to receive delegated stake, and you
384-
will no longer receive the self-staking yield.
378+
publisher yield rate and delegation fees from your delegators.
379+
You will still be able to participate in OIS after opting out
380+
of rewards.
385381
</p>
386382
</div>
387383
{state.type === UseAsyncStateType.Error && (
@@ -879,7 +875,7 @@ const Publisher = ({
879875
poolUtilization:
880876
publisher.poolUtilization + publisher.poolUtilizationDelta,
881877
yieldRate,
882-
})}
878+
}).toFixed(2)}
883879
%
884880
</div>
885881
</PublisherTableCell>
@@ -1047,7 +1043,10 @@ const StakeToPublisherButton = ({
10471043
publisher={publisher}
10481044
yieldRate={yieldRate}
10491045
>
1050-
{amount.type === AmountType.Valid ? amount.amount : 0n}
1046+
{amount.type === AmountType.Valid ||
1047+
amount.type === AmountType.AboveMax
1048+
? amount.amount
1049+
: 0n}
10511050
</NewApy>
10521051
</div>
10531052
<StakingTimeline currentEpoch={currentEpoch} />

apps/staking/src/components/WalletButton/index.tsx

Lines changed: 118 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from "@heroicons/react/24/outline";
1313
import { useWallet } from "@solana/wallet-adapter-react";
1414
import { useWalletModal } from "@solana/wallet-adapter-react-ui";
15+
import type { PublicKey } from "@solana/web3.js";
1516
import clsx from "clsx";
1617
import { useSelectedLayoutSegment } from "next/navigation";
1718
import {
@@ -21,6 +22,8 @@ import {
2122
type ReactNode,
2223
useCallback,
2324
useState,
25+
useMemo,
26+
type ReactElement,
2427
} from "react";
2528
import {
2629
Menu,
@@ -30,6 +33,8 @@ import {
3033
Separator,
3134
Section,
3235
SubmenuTrigger,
36+
Header,
37+
Collection,
3338
} from "react-aria-components";
3439

3540
import {
@@ -41,13 +46,18 @@ import {
4146
type States,
4247
useApi,
4348
} from "../../hooks/use-api";
49+
import { StateType as DataStateType, useData } from "../../hooks/use-data";
4450
import { useLogger } from "../../hooks/use-logger";
4551
import { usePrimaryDomain } from "../../hooks/use-primary-domain";
4652
import { AccountHistory } from "../AccountHistory";
4753
import { Button } from "../Button";
4854
import { ModalDialog } from "../ModalDialog";
4955
import { TruncatedKey } from "../TruncatedKey";
5056

57+
const ONE_SECOND_IN_MS = 1000;
58+
const ONE_MINUTE_IN_MS = 60 * ONE_SECOND_IN_MS;
59+
const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS;
60+
5161
type Props = Omit<ComponentProps<typeof Button>, "onClick" | "children">;
5262

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

6575
switch (api.type) {
76+
case ApiStateType.WalletDisconnecting:
77+
case ApiStateType.WalletConnecting: {
78+
return (
79+
<ButtonComponent isLoading={true} {...props}>
80+
Loading...
81+
</ButtonComponent>
82+
);
83+
}
84+
6685
case ApiStateType.NotLoaded:
6786
case ApiStateType.NoWallet: {
6887
return <DisconnectedButton {...props} />;
@@ -125,40 +144,15 @@ const ConnectedButton = ({
125144
{api.type === ApiStateType.Loaded && (
126145
<>
127146
<Section className="flex w-full flex-col">
128-
<SubmenuTrigger>
147+
<StakeAccountSelector api={api}>
129148
<WalletMenuItem
130149
icon={BanknotesIcon}
131150
textValue="Select stake account"
132151
>
133152
<span>Select stake account</span>
134153
<ChevronRightIcon className="size-4" />
135154
</WalletMenuItem>
136-
<StyledMenu
137-
items={api.allAccounts.map((account) => ({
138-
account,
139-
id: account.toBase58(),
140-
}))}
141-
>
142-
{(item) => (
143-
<WalletMenuItem
144-
onAction={() => {
145-
api.selectAccount(item.account);
146-
}}
147-
className={clsx({
148-
"font-semibold": item.account === api.account,
149-
})}
150-
isDisabled={item.account === api.account}
151-
>
152-
<CheckIcon
153-
className={clsx("size-4 text-pythpurple-600", {
154-
invisible: item.account !== api.account,
155-
})}
156-
/>
157-
<TruncatedKey>{item.account}</TruncatedKey>
158-
</WalletMenuItem>
159-
)}
160-
</StyledMenu>
161-
</SubmenuTrigger>
155+
</StakeAccountSelector>
162156
<WalletMenuItem
163157
onAction={openAccountHistory}
164158
icon={TableCellsIcon}
@@ -193,14 +187,109 @@ const ConnectedButton = ({
193187
);
194188
};
195189

190+
type StakeAccountSelectorProps = {
191+
api: States[ApiStateType.Loaded];
192+
children: ReactElement;
193+
};
194+
195+
const StakeAccountSelector = ({ children, api }: StakeAccountSelectorProps) => {
196+
const data = useData(api.dashboardDataCacheKey, api.loadData, {
197+
refreshInterval: REFRESH_INTERVAL,
198+
});
199+
const accounts = useMemo(() => {
200+
if (data.type === DataStateType.Loaded) {
201+
const main = api.allAccounts.find((account) =>
202+
data.data.integrityStakingPublishers.some((publisher) =>
203+
publisher.stakeAccount?.equals(account),
204+
),
205+
);
206+
const other = api.allAccounts
207+
.filter((account) => account !== main)
208+
.map((account) => ({
209+
account,
210+
id: account.toBase58(),
211+
}));
212+
return { main, other };
213+
} else {
214+
return;
215+
}
216+
}, [data, api]);
217+
218+
if (accounts === undefined) {
219+
// eslint-disable-next-line unicorn/no-null
220+
return null;
221+
} else if (accounts.main === undefined) {
222+
return accounts.other.length > 1 ? (
223+
<SubmenuTrigger>
224+
{children}
225+
<StyledMenu items={accounts.other}>
226+
{({ account }) => <AccountMenuItem account={account} api={api} />}
227+
</StyledMenu>
228+
</SubmenuTrigger>
229+
) : // eslint-disable-next-line unicorn/no-null
230+
null;
231+
} else {
232+
return (
233+
<SubmenuTrigger>
234+
{children}
235+
<StyledMenu>
236+
<Section className="flex w-full flex-col">
237+
<Header className="mx-4 text-sm font-semibold">Main Account</Header>
238+
<AccountMenuItem account={accounts.main} api={api} />
239+
</Section>
240+
{accounts.other.length > 0 && (
241+
<>
242+
<Separator className="mx-2 my-1 h-px bg-black/20" />
243+
<Section className="flex w-full flex-col">
244+
<Header className="mx-4 text-sm font-semibold">
245+
Other Accounts
246+
</Header>
247+
<Collection items={accounts.other}>
248+
{({ account }) => (
249+
<AccountMenuItem account={account} api={api} />
250+
)}
251+
</Collection>
252+
</Section>
253+
</>
254+
)}
255+
</StyledMenu>
256+
</SubmenuTrigger>
257+
);
258+
}
259+
};
260+
261+
type AccountMenuItemProps = {
262+
api: States[ApiStateType.Loaded];
263+
account: PublicKey;
264+
};
265+
266+
const AccountMenuItem = ({ account, api }: AccountMenuItemProps) => (
267+
<WalletMenuItem
268+
onAction={() => {
269+
api.selectAccount(account);
270+
}}
271+
className={clsx({
272+
"pr-8 font-semibold": account === api.account,
273+
})}
274+
isDisabled={account === api.account}
275+
>
276+
<CheckIcon
277+
className={clsx("size-4 text-pythpurple-600", {
278+
invisible: account !== api.account,
279+
})}
280+
/>
281+
<TruncatedKey>{account}</TruncatedKey>
282+
</WalletMenuItem>
283+
);
284+
196285
const StyledMenu = <T extends object>({
197286
className,
198287
...props
199288
}: ComponentProps<typeof Menu<T>>) => (
200-
<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">
289+
<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">
201290
<Menu
202291
className={clsx(
203-
"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",
292+
"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",
204293
className,
205294
)}
206295
{...props}

0 commit comments

Comments
 (0)