Add token decimals validation in Swaps (#11587)

* Add token decimals validation in Swaps
* Use camelCase
feature/default_network_editable
Daniel 3 years ago committed by GitHub
parent c1d96676b5
commit a17c4462b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      app/_locales/en/messages.json
  2. 35
      ui/pages/swaps/build-quote/build-quote.js
  3. 5
      ui/pages/swaps/build-quote/index.scss
  4. 18
      ui/pages/swaps/index.js
  5. 9
      ui/pages/swaps/swaps.util.js
  6. 23
      ui/pages/swaps/swaps.util.test.js

@ -2302,6 +2302,10 @@
"message": "Verified on $1 sources.", "message": "Verified on $1 sources.",
"description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number." "description": "Indicates the number of token information sources that recognize the symbol + address. $1 is a decimal number."
}, },
"swapTooManyDecimalsError": {
"message": "$1 allows up to $2 decimals",
"description": "$1 is a token symbol and $2 is the max. number of decimals allowed for the token"
},
"swapTransactionComplete": { "swapTransactionComplete": {
"message": "Transaction complete" "message": "Transaction complete"
}, },

@ -74,6 +74,7 @@ export default function BuildQuote({
maxSlippage, maxSlippage,
selectedAccountAddress, selectedAccountAddress,
isFeatureFlagLoaded, isFeatureFlagLoaded,
tokenFromError,
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -367,6 +368,11 @@ export default function BuildQuote({
} }
} }
const swapYourTokenBalance = t('swapYourTokenBalance', [
fromTokenString || '0',
fromTokenSymbol,
]);
return ( return (
<div className="build-quote"> <div className="build-quote">
<div className="build-quote__content"> <div className="build-quote__content">
@ -406,27 +412,34 @@ export default function BuildQuote({
/> />
<div <div
className={classnames('build-quote__balance-message', { className={classnames('build-quote__balance-message', {
'build-quote__balance-message--error': balanceError, 'build-quote__balance-message--error':
balanceError || tokenFromError,
})} })}
> >
{!balanceError && {!tokenFromError &&
!balanceError &&
fromTokenSymbol && fromTokenSymbol &&
t('swapYourTokenBalance', [ swapYourTokenBalance}
fromTokenString || '0', {!tokenFromError && balanceError && fromTokenSymbol && (
fromTokenSymbol,
])}
{balanceError && fromTokenSymbol && (
<div className="build-quite__insufficient-funds"> <div className="build-quite__insufficient-funds">
<div className="build-quite__insufficient-funds-first"> <div className="build-quite__insufficient-funds-first">
{t('swapsNotEnoughForTx', [fromTokenSymbol])} {t('swapsNotEnoughForTx', [fromTokenSymbol])}
</div> </div>
<div className="build-quite__insufficient-funds-second"> <div className="build-quite__insufficient-funds-second">
{t('swapYourTokenBalance', [ {swapYourTokenBalance}
fromTokenString || '0', </div>
</div>
)}
{tokenFromError && (
<>
<div className="build-quote__form-error">
{t('swapTooManyDecimalsError', [
fromTokenSymbol, fromTokenSymbol,
fromTokenDecimals,
])} ])}
</div> </div>
</div> <div>{swapYourTokenBalance}</div>
</>
)} )}
</div> </div>
<div className="build-quote__swap-arrows-row"> <div className="build-quote__swap-arrows-row">
@ -560,6 +573,7 @@ export default function BuildQuote({
}} }}
submitText={t('swapReviewSwap')} submitText={t('swapReviewSwap')}
disabled={ disabled={
tokenFromError ||
!isFeatureFlagLoaded || !isFeatureFlagLoaded ||
!Number(inputValue) || !Number(inputValue) ||
!selectedToToken?.address || !selectedToToken?.address ||
@ -582,4 +596,5 @@ BuildQuote.propTypes = {
setMaxSlippage: PropTypes.func, setMaxSlippage: PropTypes.func,
selectedAccountAddress: PropTypes.string, selectedAccountAddress: PropTypes.string,
isFeatureFlagLoaded: PropTypes.bool.isRequired, isFeatureFlagLoaded: PropTypes.bool.isRequired,
tokenFromError: PropTypes.string,
}; };

@ -87,6 +87,11 @@
color: $Black-100; color: $Black-100;
} }
.build-quote__form-error:first-of-type {
font-weight: bold;
color: $Red-500;
}
div:last-of-type { div:last-of-type {
font-weight: normal; font-weight: normal;
color: $Grey-500; color: $Grey-500;

@ -34,6 +34,7 @@ import {
fetchAndSetSwapsGasPriceInfo, fetchAndSetSwapsGasPriceInfo,
fetchSwapsLiveness, fetchSwapsLiveness,
getUseNewSwapsApi, getUseNewSwapsApi,
getFromToken,
} from '../../ducks/swaps/swaps'; } from '../../ducks/swaps/swaps';
import { import {
AWAITING_SIGNATURES_ROUTE, AWAITING_SIGNATURES_ROUTE,
@ -70,6 +71,7 @@ import {
fetchTopAssets, fetchTopAssets,
getSwapsTokensReceivedFromTxMeta, getSwapsTokensReceivedFromTxMeta,
fetchAggregatorMetadata, fetchAggregatorMetadata,
countDecimals,
} from './swaps.util'; } from './swaps.util';
import AwaitingSignatures from './awaiting-signatures'; import AwaitingSignatures from './awaiting-signatures';
import AwaitingSwap from './awaiting-swap'; import AwaitingSwap from './awaiting-swap';
@ -94,6 +96,7 @@ export default function Swap() {
const [inputValue, setInputValue] = useState(fetchParams?.value || ''); const [inputValue, setInputValue] = useState(fetchParams?.value || '');
const [maxSlippage, setMaxSlippage] = useState(fetchParams?.slippage || 3); const [maxSlippage, setMaxSlippage] = useState(fetchParams?.slippage || 3);
const [isFeatureFlagLoaded, setIsFeatureFlagLoaded] = useState(false); const [isFeatureFlagLoaded, setIsFeatureFlagLoaded] = useState(false);
const [tokenFromError, setTokenFromError] = useState(null);
const routeState = useSelector(getBackgroundSwapRouteState); const routeState = useSelector(getBackgroundSwapRouteState);
const selectedAccount = useSelector(getSelectedAccount); const selectedAccount = useSelector(getSelectedAccount);
@ -108,6 +111,7 @@ export default function Swap() {
const chainId = useSelector(getCurrentChainId); const chainId = useSelector(getCurrentChainId);
const isSwapsChain = useSelector(getIsSwapsChain); const isSwapsChain = useSelector(getIsSwapsChain);
const useNewSwapsApi = useSelector(getUseNewSwapsApi); const useNewSwapsApi = useSelector(getUseNewSwapsApi);
const fromToken = useSelector(getFromToken);
const { const {
balance: ethBalance, balance: ethBalance,
@ -288,10 +292,15 @@ export default function Swap() {
const onInputChange = (newInputValue, balance) => { const onInputChange = (newInputValue, balance) => {
setInputValue(newInputValue); setInputValue(newInputValue);
dispatch( const balanceError = new BigNumber(newInputValue || 0).gt(
setBalanceError( balance || 0,
new BigNumber(newInputValue || 0).gt(balance || 0), );
), // "setBalanceError" is just a warning, a user can still click on the "Review Swap" button.
dispatch(setBalanceError(balanceError));
setTokenFromError(
countDecimals(newInputValue) > fromToken.decimals
? 'tooManyDecimals'
: null,
); );
}; };
@ -304,6 +313,7 @@ export default function Swap() {
selectedAccountAddress={selectedAccountAddress} selectedAccountAddress={selectedAccountAddress}
maxSlippage={maxSlippage} maxSlippage={maxSlippage}
isFeatureFlagLoaded={isFeatureFlagLoaded} isFeatureFlagLoaded={isFeatureFlagLoaded}
tokenFromError={tokenFromError}
/> />
); );
}} }}

@ -826,3 +826,12 @@ export const getSwapsLivenessForNetwork = (swapsFeatureFlags = {}, chainId) => {
useNewSwapsApi: false, useNewSwapsApi: false,
}; };
}; };
/**
* @param {number} value
* @returns number
*/
export const countDecimals = (value) => {
if (!value || Math.floor(value) === value) return 0;
return value.toString().split('.')[1]?.length || 0;
};

@ -31,6 +31,7 @@ import {
isContractAddressValid, isContractAddressValid,
getNetworkNameByChainId, getNetworkNameByChainId,
getSwapsLivenessForNetwork, getSwapsLivenessForNetwork,
countDecimals,
} from './swaps.util'; } from './swaps.util';
jest.mock('../../helpers/utils/storage-helpers.js', () => ({ jest.mock('../../helpers/utils/storage-helpers.js', () => ({
@ -450,4 +451,26 @@ describe('Swaps Util', () => {
).toMatchObject(expectedSwapsLiveness); ).toMatchObject(expectedSwapsLiveness);
}); });
}); });
describe('countDecimals', () => {
it('returns 0 decimals for an undefined value', () => {
expect(countDecimals()).toBe(0);
});
it('returns 0 decimals for number: 1', () => {
expect(countDecimals(1)).toBe(0);
});
it('returns 1 decimals for number: 1.1', () => {
expect(countDecimals(1.1)).toBe(1);
});
it('returns 3 decimals for number: 1.123', () => {
expect(countDecimals(1.123)).toBe(3);
});
it('returns 9 decimals for number: 1.123456789', () => {
expect(countDecimals(1.123456789)).toBe(9);
});
});
}); });

Loading…
Cancel
Save