Skip to content

Commit

Permalink
feat(extension): #252: asset list (#259)
Browse files Browse the repository at this point in the history
* feat(extension): #252: add assets table to the main popup screen

* chore: changeset

* fix: #252: fix review comments

* fix: possible height fix
  • Loading branch information
VanishMax authored Jan 10, 2025
1 parent 1800f1f commit bc5fabe
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 17 deletions.
6 changes: 6 additions & 0 deletions .changeset/strong-carrots-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'chrome-extension': minor
'@repo/ui': minor
---

Add AssetsTable to the home screen of the extension
88 changes: 88 additions & 0 deletions apps/extension/src/routes/popup/home/assets-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@repo/ui/components/ui/table';
import { ValueViewComponent } from '@repo/ui/components/ui/value';
import { ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { getDisplayDenomFromView, getEquivalentValues } from '@penumbra-zone/getters/value-view';
import { getMetadataFromBalancesResponse } from '@penumbra-zone/getters/balances-response';
import { asValueView } from '@penumbra-zone/getters/equivalent-value';
import { useQuery } from '@tanstack/react-query';
import { viewClient } from '../../../clients';

const EquivalentValues = ({ valueView }: { valueView?: ValueView }) => {
const equivalentValuesAsValueViews = (getEquivalentValues.optional(valueView) ?? []).map(
asValueView,
);

return (
<div className='flex flex-wrap gap-2'>
{equivalentValuesAsValueViews.map(equivalentValueAsValueView => (
<ValueViewComponent
key={getDisplayDenomFromView(equivalentValueAsValueView)}
view={equivalentValueAsValueView}
variant='equivalent'
/>
))}
</div>
);
};

export interface AssetsTableProps {
account: number;
}

export const AssetsTable = ({ account }: AssetsTableProps) => {
const {
data: balances,
isLoading,
error,
} = useQuery({
queryKey: ['balances', account],
staleTime: Infinity,
queryFn: async () => {
try {
const balances = await Array.fromAsync(viewClient.balances({ accountFilter: { account } }));
balances.sort((a, b) => {
const aScore = getMetadataFromBalancesResponse.optional(a)?.priorityScore ?? 0n;
const bScore = getMetadataFromBalancesResponse.optional(b)?.priorityScore ?? 0n;
return Number(bScore - aScore);
});
return balances;
} catch (_) {
return [];
}
},
});

if (isLoading || error || !balances?.length) {
return null;
}

return (
<Table>
<TableHeader className='group'>
<TableRow>
<TableHead>Balance</TableHead>
<TableHead>Value</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{balances.map((assetBalance, index) => (
<TableRow className='group' key={index}>
<TableCell>
<ValueViewComponent view={assetBalance.balanceView} />
</TableCell>
<TableCell>
<EquivalentValues valueView={assetBalance.balanceView} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};
38 changes: 26 additions & 12 deletions apps/extension/src/routes/popup/home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Address, FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import { SelectAccount } from '@repo/ui/components/ui/select';
import { getAddressByIndex, getEphemeralByIndex } from '@penumbra-zone/wasm/keys';
import { Wallet } from '@penumbra-zone/types/wallet';
import { IndexHeader } from './index-header';
import { useStore } from '../../../state';
import { BlockSync } from './block-sync';
import { localExtStorage } from '../../../storage/local';
import { getActiveWallet } from '../../../state/wallets';
import { needsLogin, needsOnboard } from '../popup-needs';
import { Address, FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import { getAddressByIndex, getEphemeralByIndex } from '@penumbra-zone/wasm/keys';
import { Wallet } from '@penumbra-zone/types/wallet';
import { ValidateAddress } from './validate-address';
import { FrontendLink } from './frontend-link';
import { AssetsTable } from './assets-table';
import { useState } from 'react';

export interface PopupLoaderData {
fullSyncHeight?: number;
Expand Down Expand Up @@ -45,23 +47,35 @@ const getAddrByIndex =

export const PopupIndex = () => {
const activeWallet = useStore(getActiveWallet);
const [index, setIndex] = useState<number>(0);

return (
<>
<BlockSync />
<div className='fixed top-0 left-0 w-screen h-screen bg-logoImg bg-[left_-180px] bg-no-repeat pointer-events-none' />
<div className='fixed top-0 left-0 w-screen h-screen bg-logo pointer-events-none' />

<div className='flex h-full grow flex-col items-stretch gap-[15px] bg-logo bg-left-bottom px-[15px] pb-[15px]'>
<IndexHeader />
<div className='z-[1] flex flex-col h-full'>
<BlockSync />

<div className='flex flex-col gap-4'>
{activeWallet && <SelectAccount getAddrByIndex={getAddrByIndex(activeWallet)} />}
</div>
<div className='flex h-full grow flex-col items-stretch gap-[15px] px-[15px] pb-[15px]'>
<IndexHeader />

<ValidateAddress />
<div className='flex flex-col gap-4'>
{activeWallet && (
<SelectAccount
index={index}
setIndex={setIndex}
getAddrByIndex={getAddrByIndex(activeWallet)}
/>
)}
</div>

<div className='shrink-0 grow' />
<ValidateAddress />

<FrontendLink />
<FrontendLink />

<AssetsTable account={index} />
</div>
</div>
</>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/extension/src/routes/popup/popup-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const PopupLayout = () => {
usePopupReady();

return (
<div className='flex grow flex-col bg-card-radial'>
<div className='relative flex grow flex-col bg-card-radial'>
<Outlet />
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/tailwind-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ export default {
linear-gradient(
color-mix(in srgb, var(--charcoal) 80%, transparent),
color-mix(in srgb, var(--charcoal) 80%, transparent)
),
url('penumbra-logo.svg')
)
`,
logoImg: `url('penumbra-logo.svg')`,
},
},
},
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/components/ui/select/select-account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import { Box } from '../box';

export interface SelectAccountProps {
getAddrByIndex: (index: number, ephemeral: boolean) => Promise<Address> | Address;
index: number;
setIndex: (index: number) => void;
}

/**
* Renders an account address, along with a switcher to choose a different
* account index. Also allows the user to view a one-time IBC deposit address.
*/
export const SelectAccount = ({ getAddrByIndex }: SelectAccountProps) => {
const [index, setIndex] = useState<number>(0);
export const SelectAccount = ({ getAddrByIndex, index, setIndex }: SelectAccountProps) => {
const [ephemeral, setEphemeral] = useState<boolean>(false);
const [address, setAddress] = useState<Address>();

Expand Down
81 changes: 81 additions & 0 deletions packages/ui/components/ui/table/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from 'react';
import { cn } from '../../../lib/utils';

const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
),
);
Table.displayName = 'Table';

const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
));
TableHeader.displayName = 'TableHeader';

const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
));
TableBody.displayName = 'TableBody';

const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn('bg-primary font-medium text-primary-foreground', className)}
{...props}
/>
));
TableFooter.displayName = 'TableFooter';

const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr ref={ref} className={cn('border-b border-border-secondary', className)} {...props} />
),
);
TableRow.displayName = 'TableRow';

const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'py-4 text-left align-top text-lg leading-[26px] font-headline font-semibold [&:has([role=checkbox])]:pr-0',
className,
)}
{...props}
/>
));
TableHead.displayName = 'TableHead';

const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn('py-4 align-middle text-base [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
));
TableCell.displayName = 'TableCell';

const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
));
TableCaption.displayName = 'TableCaption';

export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };

0 comments on commit bc5fabe

Please sign in to comment.