From 8c2150b059db754c8daecf530234d140ac97bd5e Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Thu, 22 Feb 2024 09:06:22 -0800 Subject: [PATCH] Show a user's unstaked tokens on the validators page (#590) * Move into * Rename type/helper * Show unstaked tokens on Validators page --- .../src/components/dashboard/assets-table.tsx | 9 +- apps/webapp/src/components/root-router.tsx | 3 +- .../components/staking/accounts/account.tsx | 42 +++++++++ .../src/components/staking/accounts/index.tsx | 15 ++++ apps/webapp/src/components/staking/layout.tsx | 26 ++++-- .../components/staking/validators-table.tsx | 88 +++++++++++-------- .../src/fetchers/balances/by-account.ts | 9 +- 7 files changed, 136 insertions(+), 56 deletions(-) create mode 100644 apps/webapp/src/components/staking/accounts/account.tsx create mode 100644 apps/webapp/src/components/staking/accounts/index.tsx diff --git a/apps/webapp/src/components/dashboard/assets-table.tsx b/apps/webapp/src/components/dashboard/assets-table.tsx index 5ba8e1b32c..e2634ae875 100644 --- a/apps/webapp/src/components/dashboard/assets-table.tsx +++ b/apps/webapp/src/components/dashboard/assets-table.tsx @@ -2,20 +2,17 @@ import { LoaderFunction, useLoaderData } from 'react-router-dom'; import { throwIfExtNotInstalled } from '../../utils/is-connected.ts'; import { AddressIcon } from '@penumbra-zone/ui/components/ui/address-icon'; import { AddressComponent } from '@penumbra-zone/ui/components/ui/address-component'; -import { - AccountGroupedBalances, - getBalancesByAccount, -} from '../../fetchers/balances/by-account.ts'; +import { BalancesByAccount, getBalancesByAccount } from '../../fetchers/balances/by-account.ts'; import { Table, TableBody, TableCell, TableRow } from '@penumbra-zone/ui'; import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value.tsx'; -export const AssetsLoader: LoaderFunction = async (): Promise => { +export const AssetsLoader: LoaderFunction = async (): Promise => { throwIfExtNotInstalled(); return await getBalancesByAccount(); }; export default function AssetsTable() { - const data = useLoaderData() as AccountGroupedBalances[]; + const data = useLoaderData() as BalancesByAccount[]; if (data.length === 0) { return ( diff --git a/apps/webapp/src/components/root-router.tsx b/apps/webapp/src/components/root-router.tsx index 94f976ca65..5fadd0ffef 100644 --- a/apps/webapp/src/components/root-router.tsx +++ b/apps/webapp/src/components/root-router.tsx @@ -12,7 +12,7 @@ import { Receive } from './send/receive.tsx'; import { ErrorBoundary } from './shared/error-boundary.tsx'; import { SwapLayout } from './swap/layout.tsx'; import { SwapLoader } from './swap/swap-loader.tsx'; -import { StakingLayout } from './staking/layout.tsx'; +import { StakingLayout, StakingLoader } from './staking/layout.tsx'; export const rootRouter = createHashRouter([ { @@ -73,6 +73,7 @@ export const rootRouter = createHashRouter([ }, { path: PagePath.STAKING, + loader: StakingLoader, element: , }, ], diff --git a/apps/webapp/src/components/staking/accounts/account.tsx b/apps/webapp/src/components/staking/accounts/account.tsx new file mode 100644 index 0000000000..464687080c --- /dev/null +++ b/apps/webapp/src/components/staking/accounts/account.tsx @@ -0,0 +1,42 @@ +import { getDisplayDenomFromView } from '@penumbra-zone/types'; +import { + Card, + CardContent, + CardHeader, + CardTitle, + Table, + TableBody, + TableCell, + TableRow, +} from '@penumbra-zone/ui'; +import { BalancesByAccount } from '../../../fetchers/balances/by-account'; +import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; +import { useMemo } from 'react'; + +export const Account = ({ account }: { account: BalancesByAccount }) => { + const unstakedBalance = useMemo( + () => account.balances.find(balance => getDisplayDenomFromView(balance.value) === 'penumbra'), + [account.balances], + ); + + if (!unstakedBalance) return null; + + return ( + + + Account #{account.index.account} + + + + + + + + + + +
+
+
+ ); +}; diff --git a/apps/webapp/src/components/staking/accounts/index.tsx b/apps/webapp/src/components/staking/accounts/index.tsx new file mode 100644 index 0000000000..696ad6b381 --- /dev/null +++ b/apps/webapp/src/components/staking/accounts/index.tsx @@ -0,0 +1,15 @@ +import { useLoaderData } from 'react-router-dom'; +import { BalancesByAccount } from '../../../fetchers/balances/by-account'; +import { Account } from './account'; + +export const Accounts = () => { + const balancesByAccount = useLoaderData() as BalancesByAccount[]; + + return ( + <> + {balancesByAccount.map(account => ( + + ))} + + ); +}; diff --git a/apps/webapp/src/components/staking/layout.tsx b/apps/webapp/src/components/staking/layout.tsx index f71fa3bd7f..6ae6aaf810 100644 --- a/apps/webapp/src/components/staking/layout.tsx +++ b/apps/webapp/src/components/staking/layout.tsx @@ -1,17 +1,27 @@ -import { Card, CardContent } from '@penumbra-zone/ui'; +import { LoaderFunction } from 'react-router-dom'; +import { BalancesByAccount, getBalancesByAccount } from '../../fetchers/balances/by-account'; +import { throwIfExtNotInstalled } from '../../utils/is-connected'; import { EduInfoCard } from '../shared/edu-panels/edu-info-card'; import { EduPanel } from '../shared/edu-panels/content'; import { ValidatorsTable } from './validators-table'; +import { Accounts } from './accounts'; +import { cn } from '@penumbra-zone/ui/lib/utils'; + +export const StakingLoader: LoaderFunction = async (): Promise => { + throwIfExtNotInstalled(); + const balancesByAccount = await getBalancesByAccount(); + return balancesByAccount; +}; + +const GAPS = 'gap-6 md:gap-4 xl:gap-5'; export const StakingLayout = () => { return ( -
-
- - - - - +
+
+ + +
diff --git a/apps/webapp/src/components/staking/validators-table.tsx b/apps/webapp/src/components/staking/validators-table.tsx index cd2b7a1a62..38c3eb37cd 100644 --- a/apps/webapp/src/components/staking/validators-table.tsx +++ b/apps/webapp/src/components/staking/validators-table.tsx @@ -1,4 +1,15 @@ -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@penumbra-zone/ui'; +import { + Card, + CardContent, + CardHeader, + CardTitle, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@penumbra-zone/ui'; import { getValidator } from '@penumbra-zone/types'; import { useValidatorInfos } from './use-validator-infos'; import { Oval } from 'react-loader-spinner'; @@ -14,41 +25,48 @@ export const ValidatorsTable = () => { const showValidators = !showError && !showLoading; return ( - - - - {HEADERS.map(header => ( - {header} - ))} - - - - {showError && ( - - - There was an error loading validators. Please reload the page. - - - )} + + + Active validators + + +
+ + + {HEADERS.map(header => ( + {header} + ))} + + + + {showError && ( + + + There was an error loading validators. Please reload the page. + + + )} - {showLoading && ( - - - - - - )} + {showLoading && ( + + + + + + )} - {showValidators && - validatorInfos.map(validatorInfo => ( - - ))} - -
+ {showValidators && + validatorInfos.map(validatorInfo => ( + + ))} + + + + ); }; diff --git a/apps/webapp/src/fetchers/balances/by-account.ts b/apps/webapp/src/fetchers/balances/by-account.ts index d25fc5c8f0..a6eca539f9 100644 --- a/apps/webapp/src/fetchers/balances/by-account.ts +++ b/apps/webapp/src/fetchers/balances/by-account.ts @@ -4,16 +4,13 @@ import { } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; import { AssetBalance, getAssetBalances } from './index.ts'; -export interface AccountGroupedBalances { +export interface BalancesByAccount { index: AddressIndex; address: Address; balances: AssetBalance[]; } -const groupByAccount = ( - acc: AccountGroupedBalances[], - curr: AssetBalance, -): AccountGroupedBalances[] => { +const groupByAccount = (acc: BalancesByAccount[], curr: AssetBalance): BalancesByAccount[] => { if (curr.address.addressView.case !== 'decoded') throw new Error('address is not decoded'); if (!curr.address.addressView.value.address) throw new Error('no address in address view'); if (!curr.address.addressView.value.index) throw new Error('no index in address view'); @@ -34,7 +31,7 @@ const groupByAccount = ( return acc; }; -export const getBalancesByAccount = async (): Promise => { +export const getBalancesByAccount = async (): Promise => { const balances = await getAssetBalances(); return balances.reduce(groupByAccount, []); };