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 selections/balances to use DenomMetadata, and to render a ValueViewComponent when possible #440

Merged
merged 6 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 15 additions & 7 deletions apps/webapp/src/components/dashboard/assets-table.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@penumbra-zone/ui';
import { displayUsd, fromBaseUnitAmount } from '@penumbra-zone/types';
import { displayUsd, fromBaseUnitAmountAndDenomMetadata } 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';
Expand Down Expand Up @@ -48,11 +48,14 @@ export default function AssetsTable() {
{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.denom.display} />
<p className='font-mono text-base font-bold'>{asset.denom.display}</p>
<AssetIcon name={asset.denomMetadata.display} />
<p className='font-mono text-base font-bold'>{asset.denomMetadata.display}</p>
</div>
<p className='font-mono text-base font-bold'>
{fromBaseUnitAmount(asset.amount, asset.denom.exponent).toFormat()}
{fromBaseUnitAmountAndDenomMetadata(
asset.amount,
asset.denomMetadata,
).toFormat()}
Comment on lines +55 to +58
Copy link
Collaborator

Choose a reason for hiding this comment

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

These are places we'd want to use ValueViewComponent as well. However, I'll pick up this work in: #427. This would require more refactoring that is currently in progress in balances.ts.

</p>
<p className='font-mono text-base font-bold'>
{asset.usdcValue == 0 ? '$–' : `$${displayUsd(asset.usdcValue)}`}
Expand All @@ -73,14 +76,19 @@ export default function AssetsTable() {
<TableRow key={i}>
<TableCell className='w-1/3'>
<div className='flex items-center gap-2'>
<AssetIcon name={asset.denom.display} />
<p className='font-mono text-base font-bold'>{asset.denom.display}</p>
<AssetIcon name={asset.denomMetadata.display} />
<p className='font-mono text-base font-bold'>
{asset.denomMetadata.display}
</p>
</div>
</TableCell>
<TableCell className='w-1/3 text-center font-mono'>
<div className='flex flex-col'>
<p className='text-base font-bold'>
{fromBaseUnitAmount(asset.amount, asset.denom.exponent).toFormat()}
{fromBaseUnitAmountAndDenomMetadata(
asset.amount,
asset.denomMetadata,
).toFormat()}
</p>
</div>
</TableCell>
Expand Down
29 changes: 22 additions & 7 deletions apps/webapp/src/components/shared/input-token.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Input, InputProps } from '@penumbra-zone/ui';
import { cn } from '@penumbra-zone/ui/lib/utils';
import { fromBaseUnitAmount, joinLoHiAmount } from '@penumbra-zone/types';
import { joinLoHiAmount } from '@penumbra-zone/types';
import SelectTokenModal from './select-token-modal';
import { Validation, validationResult } from './validation-result';
import { AccountBalance, AssetBalance } from '../../fetchers/balances';
import { Selection } from '../../state/types';
import { Fee } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/fee/v1alpha1/fee_pb';
import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value';
import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1alpha1/asset_pb';

const PENUMBRA_FEE_DENOMINATOR = 1000;

Expand All @@ -14,10 +16,23 @@ const getFeeAsString = (fee: Fee | undefined) => {
return `${(Number(joinLoHiAmount(fee.amount)) / PENUMBRA_FEE_DENOMINATOR).toString()} penumbra`;
};

const getCurrentBalance = (assetBalance: AssetBalance | undefined) =>
assetBalance
? fromBaseUnitAmount(assetBalance.amount, assetBalance.denom.exponent).toFormat()
: '0';
const getCurrentBalanceValueView = (assetBalance: AssetBalance | undefined): ValueView => {
if (assetBalance?.denomMetadata)
return new ValueView({
valueView: {
case: 'knownDenom',
value: { amount: assetBalance.amount, denom: assetBalance.denomMetadata },
},
});
else if (assetBalance?.assetId)
return new ValueView({
valueView: {
case: 'unknownDenom',
value: { amount: assetBalance.amount, assetId: assetBalance.assetId },
},
});
else return new ValueView();
};
Comment on lines +19 to +35
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is also something we'll migrate to balances.ts so components won't need to have to do this.


interface InputTokenProps extends InputProps {
label: string;
Expand Down Expand Up @@ -47,7 +62,7 @@ export default function InputToken({
}: InputTokenProps) {
const vResult = validationResult(value, validations);

const currentBalance = getCurrentBalance(selection?.asset);
const currentBalanceValueView = getCurrentBalanceValueView(selection?.asset);
const feeAsString = getFeeAsString(fee);

return (
Expand Down Expand Up @@ -93,7 +108,7 @@ export default function InputToken({
</div>
<div className='flex items-start gap-1'>
<img src='./wallet.svg' alt='Wallet' className='size-5' />
<p className='font-bold text-muted-foreground'>{currentBalance}</p>
<ValueViewComponent view={currentBalanceValueView} />
</div>
</div>
</div>
Expand Down
14 changes: 8 additions & 6 deletions apps/webapp/src/components/shared/select-token-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
DialogTrigger,
Input,
} from '@penumbra-zone/ui';
import { fromBaseUnitAmount } from '@penumbra-zone/types';
import { fromBaseUnitAmountAndDenomMetadata } from '@penumbra-zone/types';
import { cn } from '@penumbra-zone/ui/lib/utils';
import { AccountBalance } from '../../fetchers/balances';
import { AssetIcon } from './asset-icon';
Expand All @@ -31,9 +31,11 @@ export default function SelectTokenModal({
<Dialog>
<DialogTrigger disabled={!balances.length}>
<div className='flex h-9 min-w-[100px] items-center justify-center gap-2 rounded-lg bg-light-brown px-2'>
{selection?.asset?.denom.display && <AssetIcon name={selection.asset.denom.display} />}
{selection?.asset?.denomMetadata.display && (
<AssetIcon name={selection.asset.denomMetadata.display} />
)}
<p className='font-bold text-light-grey md:text-sm xl:text-base'>
{selection?.asset?.denom.display}
{selection?.asset?.denomMetadata.display}
</p>
</div>
</DialogTrigger>
Expand Down Expand Up @@ -75,11 +77,11 @@ export default function SelectTokenModal({
>
<p className='flex justify-start'>{b.index}</p>
<div className='flex justify-start gap-[6px]'>
<AssetIcon name={k.denom.display} />
<p>{k.denom.display}</p>
<AssetIcon name={k.denomMetadata.display} />
<p>{k.denomMetadata.display}</p>
</div>
<p className='flex justify-end'>
{fromBaseUnitAmount(k.amount, k.denom.exponent).toFormat()}
{fromBaseUnitAmountAndDenomMetadata(k.amount, k.denomMetadata).toFormat()}
</p>
</div>
</DialogClose>
Expand Down
4 changes: 2 additions & 2 deletions apps/webapp/src/components/swap/asset-out-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export const AssetOutBox = ({ balances }: AssetOutBoxProps) => {
value: {
amount: balanceOfDenom.amount,
denom: new DenomMetadata({
display: balanceOfDenom.denom.display,
denomUnits: [balanceOfDenom.denom],
display: balanceOfDenom.denomMetadata.display,
denomUnits: balanceOfDenom.denomMetadata.denomUnits,
}),
},
},
Expand Down
35 changes: 11 additions & 24 deletions apps/webapp/src/fetchers/balances.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

Some overlap with: #427. But no worries! I'll definitely make use of your work. I think the end state probably looks like this:

export interface AccountBalance {
  value: ValueView;
  address: AddressView;
  usdcValue: number;
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb';
import {
AssetId,
DenomUnit,
DenomMetadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1alpha1/asset_pb';
import { getAddresses, IndexAddrRecord } from './address';
import { getAllAssets } from './assets';
Expand All @@ -16,10 +16,7 @@ import { viewClient } from '../clients/grpc';
import { streamToPromise } from './stream';

export interface AssetBalance {
denom: {
display: DenomUnit['denom'];
exponent: DenomUnit['exponent'];
};
denomMetadata: DenomMetadata;
assetId: AssetId;
amount: Amount;
usdcValue: number;
Expand All @@ -35,29 +32,19 @@ type NormalizedBalance = AssetBalance & {
account: { index: number; address: string };
};

// Given an asset has many denom units, the amount should be formatted using
// the exponent of the display denom (e.g. 1,954,000,000 upenumbra = 1,954 penumbra)
export const displayDenom = (res?: AssetsResponse): { display: string; exponent: number } => {
const display = res?.denomMetadata?.display;
if (!display) return { display: 'unknown', exponent: 0 };

const match = res.denomMetadata?.denomUnits.find(d => d.denom === display);
if (!match) return { display, exponent: 0 };

return { display, exponent: match.exponent };
};
Comment on lines -40 to -48
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Eliminated this helper function entirely, as it just takes an AssetsResponse (a thin wrapper around DenomMetadata) and returns just the display denom's exponent.

Going forward, we want to use the ValueViewComponent to display balances, which requires an entire DenomMetadata object. So we don't want to just strip a few properties out of that object here.


const getDenomAmount = (res: BalancesResponse, metadata: AssetsResponse[]) => {
const getDenomAmount = (
res: BalancesResponse,
metadata: AssetsResponse[],
): { amount: Amount; denomMetadata: DenomMetadata } => {
const assetId = uint8ArrayToBase64(res.balance!.assetId!.inner);
const match = metadata.find(m => {
if (!m.denomMetadata?.penumbraAssetId?.inner) return false;
return assetId === uint8ArrayToBase64(m.denomMetadata.penumbraAssetId.inner);
});

const { display, exponent } = displayDenom(match);
const amount = res.balance?.amount ?? new Amount();

return { display, exponent, amount };
return { amount, denomMetadata: match?.denomMetadata ?? new DenomMetadata() };
};

const normalize =
Expand All @@ -66,10 +53,10 @@ const normalize =
const index = res.account?.account ?? 0;
const address = indexAddrRecord?.[index] ?? '';

const { display, exponent, amount } = getDenomAmount(res, metadata);
const { denomMetadata, amount } = getDenomAmount(res, metadata);

return {
denom: { display, exponent },
denomMetadata,
assetId: res.balance!.assetId!,
amount,
//usdcValue: amount * 0.93245, // TODO: Temporary until pricing implemented
Expand All @@ -81,7 +68,7 @@ const normalize =
const groupByAccount = (balances: AccountBalance[], curr: NormalizedBalance): AccountBalance[] => {
const match = balances.find(b => b.index === curr.account.index);
const newBalance = {
denom: curr.denom,
denomMetadata: curr.denomMetadata,
amount: curr.amount,
usdcValue: curr.usdcValue,
assetId: curr.assetId,
Expand Down Expand Up @@ -109,7 +96,7 @@ const sortByAmount = (a: AssetBalance, b: AssetBalance): number => {
return Number(joinLoHiAmount(b.amount) - joinLoHiAmount(a.amount));

// If both are equal, sort by asset name in ascending order
return a.denom.display.localeCompare(b.denom.display);
return a.denomMetadata.display.localeCompare(b.denomMetadata.display);
};

// Sort by account (lowest first)
Expand Down
10 changes: 7 additions & 3 deletions apps/webapp/src/state/ibc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { beforeEach, describe, expect, test } from 'vitest';
import { create, StoreApi, UseBoundStore } from 'zustand';
import { AllSlices, initializeStore } from './index.ts';
import { Chain } from '@penumbra-zone/types';
import { AssetId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1alpha1/asset_pb';
import {
AssetId,
DenomMetadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1alpha1/asset_pb';
import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1alpha1/num_pb';
import { sendValidationErrors } from './send.ts';
import { Selection } from './types.ts';

// TODO: Revisit tests when re-implementing ibc form

Expand All @@ -15,14 +19,14 @@ describe.skip('IBC Slice', () => {
lo: 0n,
hi: 0n,
}),
denom: { display: 'test_usd', exponent: 18 },
denomMetadata: new DenomMetadata({ display: 'test_usd', denomUnits: [{ exponent: 18 }] }),
usdcValue: 0,
assetId: new AssetId().fromJson({ inner: 'reum7wQmk/owgvGMWMZn/6RFPV24zIKq3W6In/WwZgg=' }),
},
address:
'penumbra1e8k5c3ds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uurrgkvtjpny3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd1',
accountIndex: 0,
};
} satisfies Selection;
let useStore: UseBoundStore<StoreApi<AllSlices>>;

beforeEach(() => {
Expand Down
8 changes: 6 additions & 2 deletions apps/webapp/src/state/ibc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AddressIndex } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/c
import { Selection } from './types';
import { ibcClient, viewClient } from '../clients/grpc';
import { planWitnessBuildBroadcast } from './helpers';
import { getDisplayDenomExponent } from '@penumbra-zone/types/src/denom-metadata';

export interface IbcSendSlice {
selection: Selection | undefined;
Expand Down Expand Up @@ -130,8 +131,11 @@ const getPlanRequest = async ({
return new TransactionPlannerRequest({
ics20Withdrawals: [
{
amount: toBaseUnit(BigNumber(amount), selection.asset.denom.exponent),
denom: { denom: selection.asset.denom.display },
amount: toBaseUnit(
BigNumber(amount),
getDisplayDenomExponent(selection.asset.denomMetadata),
),
denom: { denom: selection.asset.denomMetadata.display },
destinationChainAddress,
returnAddress,
timeoutHeight,
Expand Down
10 changes: 7 additions & 3 deletions apps/webapp/src/state/send.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { create, StoreApi, UseBoundStore } from 'zustand';
import { AllSlices, initializeStore } from './index.ts';
import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1alpha1/num_pb';
import { sendValidationErrors } from './send.ts';
import { AssetId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1alpha1/asset_pb';
import {
AssetId,
DenomMetadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1alpha1/asset_pb';
import { Fee } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/fee/v1alpha1/fee_pb';
import { viewClient } from '../clients/grpc.ts';
import {
AddressByIndexResponse,
TransactionPlannerResponse,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb';
import { Selection } from './types.ts';

vi.mock('../fetchers/address', () => ({
getAddressByIndex: vi.fn(),
Expand All @@ -22,14 +26,14 @@ describe('Send Slice', () => {
lo: 0n,
hi: 0n,
}),
denom: { display: 'test_usd', exponent: 18 },
denomMetadata: new DenomMetadata({ display: 'test_usd', denomUnits: [{ exponent: 18 }] }),
usdcValue: 0,
assetId: new AssetId().fromJson({ inner: 'reum7wQmk/owgvGMWMZn/6RFPV24zIKq3W6In/WwZgg=' }),
},
address:
'penumbra1e8k5c3ds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uurrgkvtjpny3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd1',
accountIndex: 0,
};
} satisfies Selection;

let useStore: UseBoundStore<StoreApi<AllSlices>>;

Expand Down
25 changes: 20 additions & 5 deletions apps/webapp/src/state/send.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { AllSlices, SliceCreator } from './index';
import { fromBaseUnitAmount, isPenumbraAddr, toBaseUnit } from '@penumbra-zone/types';
import {
fromBaseUnitAmountAndDenomMetadata,
isPenumbraAddr,
toBaseUnit,
} from '@penumbra-zone/types';
import { TransactionPlannerRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb';
import { toast } from '@penumbra-zone/ui/components/ui/use-toast';
import BigNumber from 'bignumber.js';
Expand All @@ -11,6 +15,7 @@ import { MemoPlaintext } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/
import { getAddressByIndex } from '../fetchers/address';
import { getTransactionPlan, planWitnessBuildBroadcast } from './helpers.ts';
import { Fee } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/fee/v1alpha1/fee_pb';
import { getDisplayDenomExponent } from '@penumbra-zone/types/src/denom-metadata.ts';

export interface SendSlice {
selection: Selection | undefined;
Expand Down Expand Up @@ -113,7 +118,10 @@ const assembleRequest = async ({ amount, recipient, selection, memo }: SendSlice
{
address: { altBech32m: recipient },
value: {
amount: toBaseUnit(BigNumber(amount), selection.asset.denom.exponent),
amount: toBaseUnit(
BigNumber(amount),
getDisplayDenomExponent(selection.asset.denomMetadata),
),
assetId: { inner: selection.asset.assetId.inner },
},
},
Expand All @@ -126,9 +134,16 @@ const assembleRequest = async ({ amount, recipient, selection, memo }: SendSlice
});
};

export const validateAmount = (asset: AssetBalance, amount: string): boolean => {
const balanceAmt = fromBaseUnitAmount(asset.amount, asset.denom.exponent);
return Boolean(amount) && BigNumber(amount).gt(balanceAmt);
const validateAmount = (
asset: AssetBalance,
/**
* The amount that a user types into the interface will always be in the
* display denomination -- e.g., in `penumbra`, not in `upenumbra`.
*/
amountInDisplayDenom: string,
): boolean => {
const balanceAmt = fromBaseUnitAmountAndDenomMetadata(asset.amount, asset.denomMetadata);
return Boolean(amountInDisplayDenom) && BigNumber(amountInDisplayDenom).gt(balanceAmt);
};

export interface SendValidationFields {
Expand Down
Loading
Loading