Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor balances.ts to use protobuf types #453

Merged
merged 2 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/extension/src/routes/popup/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IndexHeader } from './index-header';
import { useStore } from '../../../state';
import { BlockSync } from './block-sync';
import { localExtStorage, sessionExtStorage } from '@penumbra-zone/storage';
import { accountAddrSelector } from '../../../state/wallets';
import { addrByIndexSelector } from '../../../state/wallets';

export interface PopupLoaderData {
lastBlockSynced: number;
Expand All @@ -32,14 +32,14 @@ export const popupIndexLoader = async (): Promise<Response | PopupLoaderData> =>
};

export const PopupIndex = () => {
const getAccount = useStore(accountAddrSelector);
const getAccount = useStore(addrByIndexSelector);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const getAccount = useStore(addrByIndexSelector);
const getAddrByIndex = useStore(addrByIndexSelector);


return (
<div className='relative flex h-full flex-col items-stretch justify-start bg-left-bottom px-[30px]'>
<div className='absolute bottom-[50px] left-[-10px] -z-10 h-[715px] w-[900px] overflow-hidden bg-logo opacity-10' />
<IndexHeader />
<div className='my-32'>
<SelectAccount getAccount={getAccount} />
<SelectAccount getAddrByIndex={getAccount} />
</div>
<BlockSync />
</div>
Expand Down
23 changes: 10 additions & 13 deletions apps/extension/src/state/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
} from '@penumbra-zone/wasm-ts';
import { Key } from '@penumbra-zone/crypto-web';
import { ExtensionStorage, LocalStorageState } from '@penumbra-zone/storage';
import { Wallet, WalletCreate, bech32Address } from '@penumbra-zone/types';
import { Wallet, WalletCreate } from '@penumbra-zone/types';
import { Address } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1alpha1/keys_pb';

export interface WalletsSlice {
all: Wallet[];
Expand Down Expand Up @@ -74,17 +75,13 @@ export const createWalletsSlice =
export const walletsSelector = (state: AllSlices) => state.wallets;
export const getActiveWallet = (state: AllSlices) => state.wallets.all[0];

export const accountAddrSelector = (state: AllSlices) => (index: number, ephemeral: boolean) => {
const active = getActiveWallet(state);
if (!active) return;
export const addrByIndexSelector =
(state: AllSlices) =>
(index: number, ephemeral: boolean): Address => {
const active = getActiveWallet(state);
if (!active) throw new Error('No active wallet');

const addr = ephemeral
? getEphemeralByIndex(active.fullViewingKey, index)
: getAddressByIndex(active.fullViewingKey, index);
const bech32Addr = bech32Address(addr);

return {
address: bech32Addr,
index,
return ephemeral
? getEphemeralByIndex(active.fullViewingKey, index)
: getAddressByIndex(active.fullViewingKey, index);
};
};
2 changes: 2 additions & 0 deletions apps/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
"@radix-ui/react-icons": "^1.3.0",
"@tanstack/react-query": "^5.18.1",
"bignumber.js": "^9.1.2",
"immer": "^10.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.22.0",
"tailwindcss": "^3.4.1",
"zod": "^3.22.4",
"zustand": "^4.5.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/src/clients/penumbra-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ declare global {
export const getPenumbraPort = (serviceTypeName: string) => {
const { port1: port, port2: transferPort } = new MessageChannel();
if (!(penumbra in window)) throw Error('No Penumbra global (extension not installed)');
const initPort = window[penumbra].services?.[serviceTypeName];
const initPort = window[penumbra]?.services?.[serviceTypeName];
if (!initPort) throw Error(`No init port for service ${serviceTypeName}`);
initPort.postMessage(
{
Expand Down
112 changes: 53 additions & 59 deletions apps/webapp/src/components/dashboard/assets-table.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@penumbra-zone/ui';
import { displayUsd, fromBaseUnitAmountAndMetadata } from '@penumbra-zone/types';
import { LoaderFunction, useLoaderData } from 'react-router-dom';
import { throwIfExtNotInstalled } from '../../fetchers/is-connected.ts';
import { AccountBalance, getBalancesByAccount } from '../../fetchers/balances.ts';
import { AssetIcon } from '../shared/asset-icon.tsx';
import { AddressIcon } from '@penumbra-zone/ui/components/ui/address-icon';
import { Address } from '@penumbra-zone/ui/components/ui/address.tsx';
import { AddressComponent } from '@penumbra-zone/ui/components/ui/address-component';
import {
AccountGroupedBalances,
getBalancesByAccount,
} from '../../fetchers/balances/by-account.ts';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@penumbra-zone/ui';
import { AssetIcon } from '../shared/asset-icon.tsx';
import { displayUsd, hasDenomMetadata } from '@penumbra-zone/types';
import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value.tsx';

export const AssetsLoader: LoaderFunction = async (): Promise<AccountBalance[]> => {
export const AssetsLoader: LoaderFunction = async (): Promise<AccountGroupedBalances[]> => {
throwIfExtNotInstalled();
return await getBalancesByAccount();
};

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

if (data.length === 0) {
return (
Expand All @@ -31,73 +35,63 @@ export default function AssetsTable() {

return (
<div className='flex flex-col gap-6'>
{data.map(a => {
return (
<div key={a.index} className='flex flex-col gap-4'>
<div className='flex flex-col items-center justify-center'>
<div className='flex flex-col items-center justify-center gap-2 md:flex-row'>
<div className='flex items-center gap-2'>
<AddressIcon address={a.address} size={20} />
<h2 className='font-bold md:text-base xl:text-xl'>Account #{a.index}</h2>{' '}
</div>

<Address address={a.address} />
{data.map((a, index) => (
<div key={index} className='flex flex-col gap-4'>
<div className='flex flex-col items-center justify-center'>
<div className='flex flex-col items-center justify-center gap-2 md:flex-row'>
<div className='flex items-center gap-2'>
<AddressIcon address={a.address} size={20} />
<h2 className='font-bold md:text-base xl:text-xl'>Account #{a.index.account}</h2>
</div>
<AddressComponent address={a.address} />
</div>
<div className='flex flex-col gap-[34px] md:hidden'>
{a.balances.map((asset, i) => (
<div key={i} className='flex items-center justify-between border-b pb-3'>
<div className='flex items-center gap-2'>
<AssetIcon name={asset.metadata.display} />
<p className='font-mono text-base font-bold'>{asset.metadata.display}</p>
</div>
<p className='font-mono text-base font-bold'>
{fromBaseUnitAmountAndMetadata(asset.amount, asset.metadata).toFormat()}
</p>
<p className='font-mono text-base font-bold'>
{asset.usdcValue == 0 ? '$–' : `$${displayUsd(asset.usdcValue)}`}
</p>
</div>
))}
</div>
<Table className='hidden md:table'>
<TableHeader>
<TableRow>
<TableHead className='w-1/3 text-center'>Asset</TableHead>
<TableHead className='w-1/3 text-center'>Balance</TableHead>
<TableHead className='w-1/3 text-center'>Value</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{a.balances.map((asset, i) => (
<TableRow key={i}>
</div>

<Table className='md:table'>
<TableHeader>
<TableRow>
<TableHead className='w-1/3 text-center'>Asset</TableHead>
<TableHead className='w-1/3 text-center'>Balance</TableHead>
<TableHead className='w-1/3 text-center'>Value</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{a.balances.map((assetBalance, index) => {
return (
<TableRow key={index}>
<TableCell className='w-1/3'>
<div className='flex items-center gap-2'>
<AssetIcon name={asset.metadata.display} />
<p className='font-mono text-base font-bold'>{asset.metadata.display}</p>
{hasDenomMetadata(assetBalance.value) && (
<>
<AssetIcon metadata={assetBalance.value.valueView.value.metadata} />
<p className='font-mono text-base font-bold'>
{assetBalance.value.valueView.value.metadata.display}
</p>
</>
)}
</div>
</TableCell>
<TableCell className='w-1/3 text-center font-mono'>
<div className='flex flex-col'>
<p className='text-base font-bold'>
{fromBaseUnitAmountAndMetadata(asset.amount, asset.metadata).toFormat()}
</p>
</div>
<p className='flex flex-col items-center text-base font-bold'>
<ValueViewComponent view={assetBalance.value} showDenom={false} />
</p>
</TableCell>
<TableCell className='w-1/3 text-center font-mono'>
<div className='flex flex-col'>
<p className='text-base font-bold'>
{asset.usdcValue == 0 ? '$–' : `$${displayUsd(asset.usdcValue)}`}
{assetBalance.usdcValue == 0
? '$–'
: `$${displayUsd(assetBalance.usdcValue)}`}
</p>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
})}
);
})}
</TableBody>
</Table>
</div>
))}
</div>
);
}
20 changes: 8 additions & 12 deletions apps/webapp/src/components/send/ibc/ibc-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,24 @@ import InputToken from '../../shared/input-token';
import { sendValidationErrors } from '../../../state/send';
import { useMemo } from 'react';
import { LoaderFunction, useLoaderData } from 'react-router-dom';
import { AccountBalance, getBalancesByAccount } from '../../../fetchers/balances';
import { AssetBalance, getAssetBalances } from '../../../fetchers/balances';
import { penumbraAddrValidation } from '../helpers';

export const IbcAssetBalanceLoader: LoaderFunction = async (): Promise<AccountBalance[]> => {
const balancesByAccount = await getBalancesByAccount();
export const IbcAssetBalanceLoader: LoaderFunction = async (): Promise<AssetBalance[]> => {
const balancesByAccount = await getAssetBalances();

if (balancesByAccount[0]) {
// set initial account if accounts exist and asset if account has asset list
useStore.setState(state => {
state.ibc.selection = {
address: balancesByAccount[0]?.address,
accountIndex: balancesByAccount[0]?.index,
asset: balancesByAccount[0]?.balances[0],
};
state.ibc.selection = balancesByAccount[0];
});
}

return balancesByAccount;
};

export default function IbcForm() {
const accountBalances = useLoaderData() as AccountBalance[];
const accountBalances = useLoaderData() as AssetBalance[];
const { toast } = useToast();
const {
sendIbcWithdraw,
Expand All @@ -42,8 +38,8 @@ export default function IbcForm() {
} = useStore(ibcSelector);

const validationErrors = useMemo(() => {
return sendValidationErrors(selection?.asset, amount, destinationChainAddress);
}, [selection?.asset, amount, destinationChainAddress]);
return sendValidationErrors(selection, amount, destinationChainAddress);
}, [selection, amount, destinationChainAddress]);

return (
<form
Expand Down Expand Up @@ -95,7 +91,7 @@ export default function IbcForm() {
!Number(amount) ||
!destinationChainAddress ||
!!Object.values(validationErrors).find(Boolean) ||
!selection?.asset
!selection
}
>
Send
Expand Down
4 changes: 2 additions & 2 deletions apps/webapp/src/components/send/receive.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { SelectAccount } from '@penumbra-zone/ui';
import { getAccountAddr } from '../../fetchers/address';
import { getAddrByIndex } from '../../fetchers/address';

export const Receive = () => {
return (
<div className='pb-3 md:pb-5'>
<SelectAccount getAccount={getAccountAddr} />
<SelectAccount getAddrByIndex={getAddrByIndex} />
</div>
);
};
20 changes: 8 additions & 12 deletions apps/webapp/src/components/send/send-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,30 @@ import { sendSelector, sendValidationErrors } from '../../../state/send.ts';
import { useToast } from '@penumbra-zone/ui/components/ui/use-toast';
import { InputBlock } from '../../shared/input-block';
import { LoaderFunction, useLoaderData } from 'react-router-dom';
import { AccountBalance, getBalancesByAccount } from '../../../fetchers/balances';
import { AssetBalance, getAssetBalances } from '../../../fetchers/balances';
import { useMemo } from 'react';
import { penumbraAddrValidation } from '../helpers';
import { throwIfExtNotInstalled } from '../../../fetchers/is-connected';
import InputToken from '../../shared/input-token.tsx';
import { useRefreshFee } from './use-refresh-fee.ts';
import { GasFee } from '../../shared/gas-fee.tsx';

export const SendAssetBalanceLoader: LoaderFunction = async (): Promise<AccountBalance[]> => {
export const SendAssetBalanceLoader: LoaderFunction = async (): Promise<AssetBalance[]> => {
throwIfExtNotInstalled();
const balancesByAccount = await getBalancesByAccount();
const balancesByAccount = await getAssetBalances();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const balancesByAccount = await getAssetBalances();
const assetBalances = await getAssetBalances();

(Took me a while to figure out how this was being used due to the old name.)


if (balancesByAccount[0]) {
// set initial account if accounts exist and asset if account has asset list
useStore.setState(state => {
state.send.selection = {
address: balancesByAccount[0]?.address,
accountIndex: balancesByAccount[0]?.index,
asset: balancesByAccount[0]?.balances[0],
};
state.send.selection = balancesByAccount[0];
});
}

return balancesByAccount;
};

export const SendForm = () => {
const accountBalances = useLoaderData() as AccountBalance[];
const accountBalances = useLoaderData() as AssetBalance[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const accountBalances = useLoaderData() as AssetBalance[];
const assetBalances = useLoaderData() as AssetBalance[];

const { toast } = useToast();
const {
selection,
Expand All @@ -52,8 +48,8 @@ export const SendForm = () => {
useRefreshFee();

const validationErrors = useMemo(() => {
return sendValidationErrors(selection?.asset, amount, recipient);
}, [selection?.asset, amount, recipient]);
return sendValidationErrors(selection, amount, recipient);
}, [selection, amount, recipient]);

return (
<form
Expand Down Expand Up @@ -128,7 +124,7 @@ export const SendForm = () => {
!recipient ||
!!Object.values(validationErrors).find(Boolean) ||
txInProgress ||
!selection?.asset
!selection
}
>
Send
Expand Down
14 changes: 10 additions & 4 deletions apps/webapp/src/components/shared/asset-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { localAssets } from '@penumbra-zone/constants';
import { Identicon } from '@penumbra-zone/ui';
import { useMemo } from 'react';
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1alpha1/asset_pb';

export const AssetIcon = ({ name }: { name: string }) => {
export const AssetIcon = ({ metadata }: { metadata: Metadata }) => {
const icon = useMemo(() => {
const assetImage = localAssets.find(i => i.display === name)?.images[0];
const assetImage = localAssets.find(d => d.display === metadata.display)?.images[0];
// Image default is "" and thus cannot do nullish-coalescing
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
return assetImage?.png || assetImage?.svg;
}, [name]);
}, [metadata]);

return (
<>
{icon ? (
<img className='size-6 rounded-full' src={icon} alt='Asset icon' />
) : (
<Identicon name={name} size={24} className='rounded-full' type='solid' />
<Identicon
uniqueIdentifier={metadata.display}
size={24}
className='rounded-full'
type='solid'
/>
)}
</>
);
Expand Down
Loading
Loading