Skip to content

Commit

Permalink
fix(minifront): #1675: decimals validation (#1770)
Browse files Browse the repository at this point in the history
* fix(minifront): #1675: render decimals validation errors in swap

* chore: changeset
  • Loading branch information
VanishMax authored Sep 9, 2024
1 parent 0558dc2 commit e5c76a6
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-suns-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'minifront': patch
---

Render the error in swap page in case of incorrect decimals or insufficient funds
8 changes: 0 additions & 8 deletions apps/minifront/src/components/shared/input-token.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { useMemo } from 'react';
import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata';
import { getMetadataFromBalancesResponse } from '@penumbra-zone/getters/balances-response';
import { BalanceValueView } from '@penumbra-zone/ui/components/ui/balance-value-view';
import { cn } from '@penumbra-zone/ui/lib/utils';
import BalanceSelector from './selectors/balance-selector';
Expand Down Expand Up @@ -37,10 +34,6 @@ export default function InputToken({
onInputChange,
loading,
}: InputTokenProps) {
const tokenExponent = useMemo(() => {
return getDisplayDenomExponent.optional(getMetadataFromBalancesResponse.optional(selection));
}, [selection]);

const setInputToBalanceMax = () => {
const match = balances.find(b => b.balanceView?.equals(selection?.balanceView));
if (match?.balanceView) {
Expand All @@ -55,7 +48,6 @@ export default function InputToken({
<NumberInput
variant='transparent'
placeholder={placeholder}
maxExponent={tokenExponent}
className={cn(
'md:h-8 xl:h-10 md:w-[calc(100%-80px)] xl:w-[calc(100%-160px)] md:text-xl xl:text-3xl font-bold leading-10',
inputClassName,
Expand Down
31 changes: 3 additions & 28 deletions apps/minifront/src/components/shared/number-input/index.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,9 @@
import type { FC, KeyboardEventHandler } from 'react';
import type { FC } from 'react';
import { Input, InputProps } from '@penumbra-zone/ui/components/ui/input';
import { useWheelPrevent } from './use-wheel-prevent';

export interface NumberInputProps extends InputProps {
/** If present, prevents users from entering the fractional number part longer than `maxExponent` */
maxExponent?: number;
}

export const NumberInput: FC<NumberInputProps> = ({ maxExponent, ...props }) => {
export const NumberInput: FC<InputProps> = props => {
const inputRef = useWheelPrevent();

const onKeyDown: KeyboardEventHandler<HTMLInputElement> = event => {
if (maxExponent === 0 && (event.key === '.' || event.key === ',')) {
event.preventDefault();
return;
}

if (
typeof maxExponent !== 'undefined' &&
typeof props.value === 'string' &&
!Number.isNaN(Number(event.key))
) {
const fraction = `${props.value}${event.key}`.split('.')[1]?.length;
if (fraction && fraction > maxExponent) {
event.preventDefault();
return;
}
}
props.onKeyDown?.(event);
};

return <Input ref={inputRef} {...props} type='number' onKeyDown={onKeyDown} />;
return <Input ref={inputRef} {...props} type='number' />;
};
23 changes: 23 additions & 0 deletions apps/minifront/src/components/swap/swap-form/token-input-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useStoreShallow } from '../../../utils/use-store-shallow.ts';
import { swapErrorSelector } from '../../../state/swap';

export const TokenInputError = () => {
const {
incorrectDecimalErr,
amountMoreThanBalanceErr,
swappableAssetsError,
balanceResponsesError,
} = useStoreShallow(swapErrorSelector);

const error =
amountMoreThanBalanceErr ||
incorrectDecimalErr ||
swappableAssetsError ||
balanceResponsesError;

if (!error) {
return null;
}

return <div className='ml-auto text-xs text-red-400'>{error}</div>;
};
23 changes: 3 additions & 20 deletions apps/minifront/src/components/swap/swap-form/token-swap-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,8 @@ import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_
import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { Box } from '@penumbra-zone/ui/components/ui/box';
import { joinLoHiAmount } from '@penumbra-zone/types/amount';
import {
getAmount,
getBalanceView,
getMetadataFromBalancesResponse,
} from '@penumbra-zone/getters/balances-response';
import { getAmount, getBalanceView } from '@penumbra-zone/getters/balances-response';
import { ArrowRight } from 'lucide-react';
import { useMemo } from 'react';
import { AllSlices } from '../../../state';
import { useStoreShallow } from '../../../utils/use-store-shallow';
import { getFormattedAmtFromValueView } from '@penumbra-zone/types/value-view';
Expand All @@ -21,13 +16,12 @@ import { isValidAmount } from '../../../state/helpers';
import { NonNativeFeeWarning } from '../../shared/non-native-fee-warning';
import { NumberInput } from '../../shared/number-input';
import { useBalancesResponses, useAssets } from '../../../state/shared';
import { FadeIn } from '@penumbra-zone/ui/components/ui/fade-in';
import { getBalanceByMatchingMetadataAndAddressIndex } from '../../../state/swap/getters';
import {
swappableAssetsSelector,
swappableBalancesResponsesSelector,
} from '../../../state/swap/helpers';
import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata';
import { TokenInputError } from './token-input-error.tsx';

const getAssetOutBalance = (
balancesResponses: BalancesResponse[] = [],
Expand Down Expand Up @@ -69,9 +63,6 @@ export const TokenSwapInput = () => {
const { amount, setAmount, assetIn, setAssetIn, assetOut, setAssetOut, reverse } =
useStoreShallow(tokenSwapInputSelector);
const assetOutBalance = getAssetOutBalance(balancesResponses?.data, assetIn, assetOut);
const assetInExponent = useMemo(() => {
return getDisplayDenomExponent.optional(getMetadataFromBalancesResponse.optional(assetIn));
}, [assetIn]);

const maxAmount = getAmount.optional(assetIn);
const maxAmountAsString = maxAmount ? joinLoHiAmount(maxAmount).toString() : undefined;
Expand All @@ -84,15 +75,14 @@ export const TokenSwapInput = () => {
};

return (
<Box label='Trade' layout>
<Box label='Trade' layout headerContent={<TokenInputError />}>
<div className='flex flex-col items-stretch gap-4 sm:flex-row'>
<NumberInput
value={amount}
inputMode='decimal'
variant='transparent'
placeholder='Enter an amount...'
max={maxAmountAsString}
maxExponent={assetInExponent}
step='any'
className={'font-bold leading-10 md:h-8 md:text-xl xl:h-10 xl:text-3xl'}
onChange={e => {
Expand All @@ -108,13 +98,6 @@ export const TokenSwapInput = () => {
</div>
)}

<FadeIn condition={!!balancesResponses?.error || !!swappableAssets?.error}>
<div className='flex gap-4 text-red'>
{balancesResponses?.error instanceof Error && balancesResponses.error.toString()}
{swappableAssets?.error instanceof Error && swappableAssets.error.toString()}
</div>
</FadeIn>

<div className='flex gap-4'>
<div className='flex h-full flex-col justify-end gap-2'>
<BalanceSelector
Expand Down
23 changes: 22 additions & 1 deletion apps/minifront/src/state/swap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
instantSwapSubmitButtonDisabledSelector,
} from './instant-swap';
import { createPriceHistorySlice, PriceHistorySlice } from './price-history';
import { isValidAmount } from '../helpers';
import { amountMoreThanBalance, isIncorrectDecimal, isValidAmount } from '../helpers';

import { setSwapQueryParams } from './query-params';
import { swappableAssetsSelector, swappableBalancesResponsesSelector } from './helpers';
Expand All @@ -29,6 +29,7 @@ import {
getFirstMetadataNotMatchingBalancesResponse,
} from './getters';
import { createLpPositionsSlice, LpPositionsSlice } from './lp-positions.ts';
import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata';

export interface SimulateSwapResult {
metadataByAssetId: Record<string, Metadata>;
Expand Down Expand Up @@ -144,6 +145,26 @@ export const createSwapSlice = (): SliceCreator<SwapSlice> => (set, get, store)
},
});

export const swapErrorSelector = (state: AllSlices) => ({
balanceResponsesError:
state.shared.balancesResponses.error instanceof Error &&
state.shared.balancesResponses.error.toString(),
swappableAssetsError:
state.shared.assets.error instanceof Error && state.shared.assets.error.toString(),
amountMoreThanBalanceErr:
state.swap.amount &&
state.swap.assetIn &&
amountMoreThanBalance(state.swap.assetIn, state.swap.amount)
? 'Insufficient funds'
: '',
incorrectDecimalErr:
state.swap.amount &&
state.swap.assetIn &&
isIncorrectDecimal(state.swap.assetIn, state.swap.amount)
? `Incorrect decimals, maximum ${getDisplayDenomExponent.optional(getMetadataFromBalancesResponse.optional(state.swap.assetIn))} allowed`
: '',
});

export const submitButtonDisabledSelector = (state: AllSlices) =>
!state.swap.amount ||
!isValidAmount(state.swap.amount, state.swap.assetIn) ||
Expand Down

0 comments on commit e5c76a6

Please sign in to comment.