Skip to content

Commit

Permalink
Show a user's unstaked tokens on the validators page (#590)
Browse files Browse the repository at this point in the history
* Move <Card> into <ValidatorsTable />

* Rename type/helper

* Show unstaked tokens on Validators page
  • Loading branch information
jessepinho authored Feb 22, 2024
1 parent 3ac4b84 commit 8c2150b
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 56 deletions.
9 changes: 3 additions & 6 deletions apps/webapp/src/components/dashboard/assets-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccountGroupedBalances[]> => {
export const AssetsLoader: LoaderFunction = async (): Promise<BalancesByAccount[]> => {
throwIfExtNotInstalled();
return await getBalancesByAccount();
};

export default function AssetsTable() {
const data = useLoaderData() as AccountGroupedBalances[];
const data = useLoaderData() as BalancesByAccount[];

if (data.length === 0) {
return (
Expand Down
3 changes: 2 additions & 1 deletion apps/webapp/src/components/root-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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([
{
Expand Down Expand Up @@ -73,6 +73,7 @@ export const rootRouter = createHashRouter([
},
{
path: PagePath.STAKING,
loader: StakingLoader,
element: <StakingLayout />,
},
],
Expand Down
42 changes: 42 additions & 0 deletions apps/webapp/src/components/staking/accounts/account.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card gradient>
<CardHeader>
<CardTitle>Account #{account.index.account}</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableBody>
<TableRow>
<TableCell>
<ValueViewComponent view={unstakedBalance.value} />
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
);
};
15 changes: 15 additions & 0 deletions apps/webapp/src/components/staking/accounts/index.tsx
Original file line number Diff line number Diff line change
@@ -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 => (
<Account account={account} key={account.index.account} />
))}
</>
);
};
26 changes: 18 additions & 8 deletions apps/webapp/src/components/staking/layout.tsx
Original file line number Diff line number Diff line change
@@ -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<BalancesByAccount[]> => {
throwIfExtNotInstalled();
const balancesByAccount = await getBalancesByAccount();
return balancesByAccount;
};

const GAPS = 'gap-6 md:gap-4 xl:gap-5';

export const StakingLayout = () => {
return (
<div className='grid gap-6 md:grid-cols-3 md:gap-4 xl:gap-5'>
<div className='col-span-2'>
<Card gradient>
<CardContent>
<ValidatorsTable />
</CardContent>
</Card>
<div className={cn('grid md:grid-cols-3', GAPS)}>
<div className={cn('col-span-2 flex flex-col', GAPS)}>
<Accounts />

<ValidatorsTable />
</div>

<div>
Expand Down
88 changes: 53 additions & 35 deletions apps/webapp/src/components/staking/validators-table.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,41 +25,48 @@ export const ValidatorsTable = () => {
const showValidators = !showError && !showLoading;

return (
<Table className='w-full'>
<TableHeader>
<TableRow>
{HEADERS.map(header => (
<TableHead key={header}>{header}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{showError && (
<TableRow>
<TableCell colSpan={HEADERS.length}>
There was an error loading validators. Please reload the page.
</TableCell>
</TableRow>
)}
<Card>
<CardHeader>
<CardTitle>Active validators</CardTitle>
</CardHeader>
<CardContent>
<Table className='w-full'>
<TableHeader>
<TableRow>
{HEADERS.map(header => (
<TableHead key={header}>{header}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{showError && (
<TableRow>
<TableCell colSpan={HEADERS.length}>
There was an error loading validators. Please reload the page.
</TableCell>
</TableRow>
)}

{showLoading && (
<TableRow>
<TableCell colSpan={HEADERS.length} className='flex gap-4'>
<Oval width={16} height={16} color='white' secondaryColor='white' />
</TableCell>
</TableRow>
)}
{showLoading && (
<TableRow>
<TableCell colSpan={HEADERS.length} className='flex gap-4'>
<Oval width={16} height={16} color='white' secondaryColor='white' />
</TableCell>
</TableRow>
)}

{showValidators &&
validatorInfos.map(validatorInfo => (
<ValidatorInfoRow
key={getValidator(validatorInfo).name}
loading={loading}
validatorInfo={validatorInfo}
votingPowerByValidatorInfo={votingPowerByValidatorInfo}
/>
))}
</TableBody>
</Table>
{showValidators &&
validatorInfos.map(validatorInfo => (
<ValidatorInfoRow
key={getValidator(validatorInfo).name}
loading={loading}
validatorInfo={validatorInfo}
votingPowerByValidatorInfo={votingPowerByValidatorInfo}
/>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
};
9 changes: 3 additions & 6 deletions apps/webapp/src/fetchers/balances/by-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -34,7 +31,7 @@ const groupByAccount = (
return acc;
};

export const getBalancesByAccount = async (): Promise<AccountGroupedBalances[]> => {
export const getBalancesByAccount = async (): Promise<BalancesByAccount[]> => {
const balances = await getAssetBalances();
return balances.reduce(groupByAccount, []);
};

0 comments on commit 8c2150b

Please sign in to comment.