From 59131c4f79d92254ac470ff61a4f5c176d226919 Mon Sep 17 00:00:00 2001 From: Kieran O'Neill Date: Sat, 23 Dec 2023 18:09:02 +0200 Subject: [PATCH] fix: send asset number input allows for decimals (#68) --- src/common/utils/formatCurrencyUnit.ts | 1 + .../SendAssetModal/SendAmountInput.tsx | 63 +++++++++---------- .../SendAssetModal/SendAssetModal.tsx | 14 +++-- .../SendAssetModalSummaryContent.tsx | 10 ++- src/extension/features/send-assets/slice.ts | 4 +- .../thunks/submitTransactionThunk.ts | 13 ++-- .../send-assets/types/ISendAssetsState.ts | 4 +- .../send-assets/utils/getInitialState.ts | 2 +- src/extension/selectors/index.ts | 2 +- ...electSendingAssetAmountInStandardUnits.ts} | 4 +- 10 files changed, 64 insertions(+), 53 deletions(-) rename src/extension/selectors/{useSelectSendingAssetAmount.ts => useSelectSendingAssetAmountInStandardUnits.ts} (54%) diff --git a/src/common/utils/formatCurrencyUnit.ts b/src/common/utils/formatCurrencyUnit.ts index 0438b5c7..93c22758 100644 --- a/src/common/utils/formatCurrencyUnit.ts +++ b/src/common/utils/formatCurrencyUnit.ts @@ -4,6 +4,7 @@ import numbro from 'numbro'; /** * Formats a given unit to display on the frontend. * @param {BigNumber} input - the unit as a BigNumber. + * @param {decimals} decimals - the number of decimals for the currency. * @returns {string} the formatted unit. */ export default function formatCurrencyUnit( diff --git a/src/extension/components/SendAssetModal/SendAmountInput.tsx b/src/extension/components/SendAssetModal/SendAmountInput.tsx index 178ecb4c..169f1a20 100644 --- a/src/extension/components/SendAssetModal/SendAmountInput.tsx +++ b/src/extension/components/SendAssetModal/SendAmountInput.tsx @@ -10,10 +10,14 @@ import { VStack, } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; -import React, { FC, ReactElement } from 'react'; +import numbro from 'numbro'; +import React, { FC, FocusEvent, ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import { IoInformationCircleOutline } from 'react-icons/io5'; +// constants +import { DEFAULT_GAP } from '@extension/constants'; + // hooks import useButtonHoverBackgroundColor from '@extension/hooks/useButtonHoverBackgroundColor'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; @@ -33,11 +37,7 @@ import { } from '@extension/types'; // utils -import { - convertToAtomicUnit, - convertToStandardUnit, - formatCurrencyUnit, -} from '@common/utils'; +import { convertToStandardUnit, formatCurrencyUnit } from '@common/utils'; import { convertGenesisHashToHex } from '@extension/utils'; interface IProps { @@ -86,18 +86,28 @@ const SendAmountInput: FC = ({ assetDecimals ); // handlers - const handleMaximumAmountClick = () => - onValueChange(maximumTransactionAmount); - const handleValueChange = (valueInStandardUnit: string) => { + const handleOnBlur = (event: FocusEvent) => { + const blurValue: BigNumber = new BigNumber(event.target.value || '0'); + + // if the entered value is greater than the maximum allowed, use the max + if (blurValue.gt(maximumTransactionAmountInStandardUnit)) { + onValueChange(maximumTransactionAmountInStandardUnit.toString()); + + return; + } + + // format the number to use an absolute value (no negatives), the maximum decimals for the asset and trim any zeroes onValueChange( - valueInStandardUnit?.length > 1 - ? convertToAtomicUnit( - new BigNumber(valueInStandardUnit), - assetDecimals - ).toString() - : '0' + numbro(blurValue.absoluteValue().toString()).format({ + mantissa: assetDecimals, + trimMantissa: true, + }) ); }; + const handleMaximumAmountClick = () => + onValueChange(maximumTransactionAmountInStandardUnit.toString()); + const handleOnChange = (valueAsString: string | undefined) => + onValueChange(valueAsString || '0'); // renders const renderMaximumTransactionAmountLabel = () => { const maximumTransactionAmountLabel: ReactElement = ( @@ -108,7 +118,7 @@ const SendAmountInput: FC = ({ @@ -191,25 +201,12 @@ const SendAmountInput: FC = ({ {/*input*/} diff --git a/src/extension/components/SendAssetModal/SendAssetModal.tsx b/src/extension/components/SendAssetModal/SendAssetModal.tsx index e9dd9458..93404ba0 100644 --- a/src/extension/components/SendAssetModal/SendAssetModal.tsx +++ b/src/extension/components/SendAssetModal/SendAssetModal.tsx @@ -61,7 +61,7 @@ import usePrimaryColor from '@extension/hooks/usePrimaryColor'; import { useSelectAccounts, useSelectSelectedNetwork, - useSelectSendingAssetAmount, + useSelectSendingAssetAmountInStandardUnits, useSelectSendingAssetConfirming, useSelectSendingAssetError, useSelectSendingAssetFromAccount, @@ -100,7 +100,8 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { const dispatch: IAppThunkDispatch = useDispatch(); // selectors const accounts: IAccount[] = useSelectAccounts(); - const amount: string = useSelectSendingAssetAmount(); + const amountInStandardUnits: string = + useSelectSendingAssetAmountInStandardUnits(); const assets: IStandardAsset[] = useSelectStandardAssetsBySelectedNetwork(); const confirming: boolean = useSelectSendingAssetConfirming(); const error: BaseExtensionError | null = useSelectSendingAssetError(); @@ -209,7 +210,7 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { if (showSummary) { return ( = ({ onClose }: IProps) => { maximumTransactionAmount={maximumTransactionAmount} onValueChange={handleAmountChange} selectedAsset={selectedAsset} - value={amount} + value={amountInStandardUnits} /> {/*select asset*/} @@ -377,7 +378,10 @@ const SendAssetModal: FC = ({ onClose }: IProps) => { setMaximumTransactionAmount(newMaximumTransactionAmount.toString()); // if the amount exceeds the new maximum transaction amount, set the amount to the maximum transaction amount - if (amount && new BigNumber(amount).gt(newMaximumTransactionAmount)) { + if ( + amountInStandardUnits && + new BigNumber(amountInStandardUnits).gt(newMaximumTransactionAmount) + ) { dispatch(setAmount(newMaximumTransactionAmount.toString())); } diff --git a/src/extension/components/SendAssetModal/SendAssetModalSummaryContent.tsx b/src/extension/components/SendAssetModal/SendAssetModalSummaryContent.tsx index 6e2598fa..43d2addd 100644 --- a/src/extension/components/SendAssetModal/SendAssetModalSummaryContent.tsx +++ b/src/extension/components/SendAssetModal/SendAssetModalSummaryContent.tsx @@ -29,9 +29,10 @@ import { // utils import { createIconFromDataUri } from '@extension/utils'; +import { convertToAtomicUnit } from '@common/utils'; interface IProps { - amount: string; + amountInStandardUnits: string; asset: IStandardAsset; fromAccount: IAccount; network: INetworkWithTransactionParams; @@ -40,7 +41,7 @@ interface IProps { } const SendAssetModalSummaryContent: FC = ({ - amount, + amountInStandardUnits, asset, fromAccount, network, @@ -64,7 +65,10 @@ const SendAssetModalSummaryContent: FC = ({ fontSize="sm" item={ ) => { - state.amount = '0'; + state.amountInStandardUnits = '0'; state.confirming = false; state.error = null; state.fromAddress = null; @@ -78,7 +78,7 @@ const slice = createSlice({ state: Draft, action: PayloadAction ) => { - state.amount = action.payload; + state.amountInStandardUnits = action.payload; }, setError: ( state: Draft, diff --git a/src/extension/features/send-assets/thunks/submitTransactionThunk.ts b/src/extension/features/send-assets/thunks/submitTransactionThunk.ts index 27a16595..8543abb8 100644 --- a/src/extension/features/send-assets/thunks/submitTransactionThunk.ts +++ b/src/extension/features/send-assets/thunks/submitTransactionThunk.ts @@ -37,9 +37,10 @@ import { } from '@extension/types'; // utils -import { getAlgodClient } from '@common/utils'; +import { convertToAtomicUnit, getAlgodClient } from '@common/utils'; import { selectNetworkFromSettings } from '@extension/utils'; import { createSendAssetTransaction } from '../utils'; +import BigNumber from 'bignumber.js'; interface AsyncThunkConfig { state: IMainRootState; @@ -53,7 +54,8 @@ const submitTransactionThunk: AsyncThunk< > = createAsyncThunk( SendAssetsThunkEnum.SubmitTransaction, async (password, { getState, rejectWithValue }) => { - const amount: string | null = getState().sendAssets.amount; + const amountInStandardUnits: string | null = + getState().sendAssets.amountInStandardUnits; const asset: IStandardAsset | null = getState().sendAssets.selectedAsset; const fromAddress: string | null = getState().sendAssets.fromAddress; const logger: ILogger = getState().system.logger; @@ -74,7 +76,7 @@ const submitTransactionThunk: AsyncThunk< let transactionResponse: IAlgorandPendingTransactionResponse; let unsignedTransaction: Transaction; - if (!amount || !asset || !fromAddress || !toAddress) { + if (!amountInStandardUnits || !asset || !fromAddress || !toAddress) { logger.debug( `${SendAssetsThunkEnum.SubmitTransaction}: required fields not completed` ); @@ -157,7 +159,10 @@ const submitTransactionThunk: AsyncThunk< try { suggestedParams = await algodClient.getTransactionParams().do(); unsignedTransaction = createSendAssetTransaction({ - amount, + amount: convertToAtomicUnit( + new BigNumber(amountInStandardUnits), + asset.decimals + ).toString(), // convert to atomic units asset, fromAddress, note, diff --git a/src/extension/features/send-assets/types/ISendAssetsState.ts b/src/extension/features/send-assets/types/ISendAssetsState.ts index f4b47cf9..79153d77 100644 --- a/src/extension/features/send-assets/types/ISendAssetsState.ts +++ b/src/extension/features/send-assets/types/ISendAssetsState.ts @@ -5,7 +5,7 @@ import { BaseExtensionError } from '@extension/errors'; import { IStandardAsset } from '@extension/types'; /** - * @property {string} amount - the amount to send. Default is "0". + * @property {string} amountInStandardUnits - the amount, in standard units, to send. Defaults to "0". * @property {boolean} confirming - confirming the transaction to the network. * @property {BaseExtensionError | null} error - if an error occurred. * @property {string | null} fromAddress - the address to send from. @@ -15,7 +15,7 @@ import { IStandardAsset } from '@extension/types'; * @property {string | null} transactionId - the ID of the confirmed transaction. */ interface ISendAssetsState { - amount: string; + amountInStandardUnits: string; confirming: boolean; error: BaseExtensionError | null; fromAddress: string | null; diff --git a/src/extension/features/send-assets/utils/getInitialState.ts b/src/extension/features/send-assets/utils/getInitialState.ts index f490ceaa..b4b9791a 100644 --- a/src/extension/features/send-assets/utils/getInitialState.ts +++ b/src/extension/features/send-assets/utils/getInitialState.ts @@ -3,7 +3,7 @@ import { ISendAssetsState } from '../types'; export default function getInitialState(): ISendAssetsState { return { - amount: '0', + amountInStandardUnits: '0', confirming: false, error: null, fromAddress: null, diff --git a/src/extension/selectors/index.ts b/src/extension/selectors/index.ts index 927ccf4a..19fb5350 100644 --- a/src/extension/selectors/index.ts +++ b/src/extension/selectors/index.ts @@ -30,7 +30,7 @@ export { default as useSelectSavingRegistration } from './useSelectSavingRegistr export { default as useSelectSavingSessions } from './useSelectSavingSessions'; export { default as useSelectSavingSettings } from './useSelectSavingSettings'; export { default as useSelectSelectedNetwork } from './useSelectSelectedNetwork'; -export { default as useSelectSendingAssetAmount } from './useSelectSendingAssetAmount'; +export { default as useSelectSendingAssetAmountInStandardUnits } from './useSelectSendingAssetAmountInStandardUnits'; export { default as useSelectSendingAssetConfirming } from './useSelectSendingAssetConfirming'; export { default as useSelectSendingAssetError } from './useSelectSendingAssetError'; export { default as useSelectSendingAssetFromAccount } from './useSelectSendingAssetFromAccount'; diff --git a/src/extension/selectors/useSelectSendingAssetAmount.ts b/src/extension/selectors/useSelectSendingAssetAmountInStandardUnits.ts similarity index 54% rename from src/extension/selectors/useSelectSendingAssetAmount.ts rename to src/extension/selectors/useSelectSendingAssetAmountInStandardUnits.ts index 36a34c24..c6606d17 100644 --- a/src/extension/selectors/useSelectSendingAssetAmount.ts +++ b/src/extension/selectors/useSelectSendingAssetAmountInStandardUnits.ts @@ -3,8 +3,8 @@ import { useSelector } from 'react-redux'; // types import { IMainRootState } from '@extension/types'; -export default function useSelectSendingAssetAmount(): string { +export default function useSelectSendingAssetAmountInStandardUnits(): string { return useSelector( - (state) => state.sendAssets.amount + (state) => state.sendAssets.amountInStandardUnits ); }