Skip to content

Commit

Permalink
Refactor the user's balance to use a ValueViewComponent
Browse files Browse the repository at this point in the history
  • Loading branch information
jessepinho committed Jan 31, 2024
1 parent f0efaad commit 22db3d0
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 53 deletions.
20 changes: 14 additions & 6 deletions apps/webapp/src/components/dashboard/assets-table.tsx
Original file line number Diff line number Diff line change
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()}
{fromBaseUnitAmount(
asset.amount,
asset.denomMetadata.denomUnits[0]?.exponent,
).toFormat()}
</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()}
{fromBaseUnitAmount(
asset.amount,
asset.denomMetadata.denomUnits[0]?.exponent,
).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();
};

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
15 changes: 10 additions & 5 deletions apps/webapp/src/components/shared/select-token-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,14 @@ 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()}
{fromBaseUnitAmount(
k.amount,
k.denomMetadata.denomUnits[0]?.exponent,
).toFormat()}
</p>
</div>
</DialogClose>
Expand Down
35 changes: 11 additions & 24 deletions apps/webapp/src/fetchers/balances.ts
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 };
};

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
7 changes: 5 additions & 2 deletions apps/webapp/src/state/ibc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,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),
selection.asset.denomMetadata.denomUnits[0]?.exponent,
),
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';

describe('Send Slice', () => {
const selectionExample = {
Expand All @@ -18,14 +22,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
7 changes: 5 additions & 2 deletions apps/webapp/src/state/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,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),
selection.asset.denomMetadata.denomUnits[0]?.exponent,
),
assetId: { inner: selection.asset.assetId.inner },
},
},
Expand All @@ -127,7 +130,7 @@ const assembleRequest = async ({ amount, recipient, selection, memo }: SendSlice
};

export const validateAmount = (asset: AssetBalance, amount: string): boolean => {
const balanceAmt = fromBaseUnitAmount(asset.amount, asset.denom.exponent);
const balanceAmt = fromBaseUnitAmount(asset.amount, asset.denomMetadata.denomUnits[0]?.exponent);
return Boolean(amount) && BigNumber(amount).gt(balanceAmt);
};

Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/amount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const joinLoHiAmount = (amount: Amount): bigint => {
return joinLoHi(amount.lo, amount.hi);
};

export const fromBaseUnitAmount = (amount: Amount, exponent: number): BigNumber => {
export const fromBaseUnitAmount = (amount: Amount, exponent = 0): BigNumber => {
return fromBaseUnit(amount.lo, amount.hi, exponent);
};

Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/lo-hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const fromBaseUnit = (lo = 0n, hi = 0n, exponent: number): BigNumber => {
* @param {number} exponent - The exponent to be applied.
* @returns {LoHi} An object with properties `lo` and `hi`, representing the low and high 64 bits of the multiplied value.
*/
export const toBaseUnit = (value: BigNumber, exponent: number): LoHi => {
export const toBaseUnit = (value: BigNumber, exponent = 0): LoHi => {
const multipliedValue = value.multipliedBy(new BigNumber(10).pow(exponent));
const bigInt = BigInt(multipliedValue.toFixed());

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/components/ui/tx/view/value.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { CopyToClipboard } from '../../copy-to-clipboard';
import { CopyIcon } from '@radix-ui/react-icons';
import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1alpha1/num_pb';

interface ValueViewPrpos {
interface ValueViewProps {
view: ValueView | undefined;
}

export const ValueViewComponent = ({ view }: ValueViewPrpos) => {
export const ValueViewComponent = ({ view }: ValueViewProps) => {
if (!view) return <></>;

if (view.valueView.case === 'unknownDenom') {
Expand Down

0 comments on commit 22db3d0

Please sign in to comment.