1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Swaps optimizations (#12842)

* Trigger Build

* Trigger Build

* Move swaps index variables to redux

* all optimizations so far

* Add better equality checks for selectors in swaps index and build quote

* Clean up PR, remove extra code and logs

* Clean up lavamoat file

* Fixes for optimizations

* Update tests and test snapshots

* Remove unnecessary tests

* Remove unnecessary console log

* Trigger Build

* Trigger Build

* Add delay to account for remote call made by trezor keyring

Co-authored-by: Dan Miller <danjm.com@gmail.com>
This commit is contained in:
Matthew Epps 2021-12-01 09:25:09 -07:00 committed by GitHub
parent 81ea24f08a
commit 7a92e22111
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 161 additions and 183 deletions

View File

@ -85,6 +85,10 @@ const initialState = {
balanceError: false, balanceError: false,
fetchingQuotes: false, fetchingQuotes: false,
fromToken: null, fromToken: null,
fromTokenInputValue: '',
fromTokenError: null,
isFeatureFlagLoaded: false,
maxSlippage: 3,
quotesFetchStartTime: null, quotesFetchStartTime: null,
reviewSwapClickedTimestamp: null, reviewSwapClickedTimestamp: null,
topAssets: {}, topAssets: {},
@ -128,6 +132,18 @@ const slice = createSlice({
setFromToken: (state, action) => { setFromToken: (state, action) => {
state.fromToken = action.payload; state.fromToken = action.payload;
}, },
setFromTokenInputValue: (state, action) => {
state.fromTokenInputValue = action.payload;
},
setFromTokenError: (state, action) => {
state.fromTokenError = action.payload;
},
setIsFeatureFlagLoaded: (state, action) => {
state.isFeatureFlagLoaded = action.payload;
},
setMaxSlippage: (state, action) => {
state.maxSlippage = action.payload;
},
setQuotesFetchStartTime: (state, action) => { setQuotesFetchStartTime: (state, action) => {
state.quotesFetchStartTime = action.payload; state.quotesFetchStartTime = action.payload;
}, },
@ -178,6 +194,16 @@ export const getBalanceError = (state) => state.swaps.balanceError;
export const getFromToken = (state) => state.swaps.fromToken; export const getFromToken = (state) => state.swaps.fromToken;
export const getFromTokenError = (state) => state.swaps.fromTokenError;
export const getFromTokenInputValue = (state) =>
state.swaps.fromTokenInputValue;
export const getIsFeatureFlagLoaded = (state) =>
state.swaps.isFeatureFlagLoaded;
export const getMaxSlippage = (state) => state.swaps.maxSlippage;
export const getTopAssets = (state) => state.swaps.topAssets; export const getTopAssets = (state) => state.swaps.topAssets;
export const getToToken = (state) => state.swaps.toToken; export const getToToken = (state) => state.swaps.toToken;
@ -329,6 +355,10 @@ const {
setBalanceError, setBalanceError,
setFetchingQuotes, setFetchingQuotes,
setFromToken, setFromToken,
setFromTokenError,
setFromTokenInputValue,
setIsFeatureFlagLoaded,
setMaxSlippage,
setQuotesFetchStartTime, setQuotesFetchStartTime,
setReviewSwapClickedTimestamp, setReviewSwapClickedTimestamp,
setTopAssets, setTopAssets,
@ -345,6 +375,10 @@ export {
setBalanceError, setBalanceError,
setFetchingQuotes, setFetchingQuotes,
setFromToken as setSwapsFromToken, setFromToken as setSwapsFromToken,
setFromTokenError,
setFromTokenInputValue,
setIsFeatureFlagLoaded,
setMaxSlippage,
setQuotesFetchStartTime as setSwapQuotesFetchStartTime, setQuotesFetchStartTime as setSwapQuotesFetchStartTime,
setReviewSwapClickedTimestamp, setReviewSwapClickedTimestamp,
setTopAssets, setTopAssets,
@ -411,6 +445,7 @@ export const fetchSwapsLiveness = () => {
log.error('Failed to fetch Swaps liveness, defaulting to false.', error); log.error('Failed to fetch Swaps liveness, defaulting to false.', error);
} }
await dispatch(setSwapsLiveness(swapsLivenessForNetwork)); await dispatch(setSwapsLiveness(swapsLivenessForNetwork));
dispatch(setIsFeatureFlagLoaded(true));
return swapsLivenessForNetwork; return swapsLivenessForNetwork;
}; };
}; };

View File

@ -71,7 +71,7 @@ describe('Ducks - Swaps', () => {
createGetState(), createGetState(),
); );
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
@ -91,7 +91,7 @@ describe('Ducks - Swaps', () => {
createGetState(), createGetState(),
); );
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
@ -112,7 +112,7 @@ describe('Ducks - Swaps', () => {
createGetState(), createGetState(),
); );
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
@ -130,7 +130,7 @@ describe('Ducks - Swaps', () => {
createGetState(), createGetState(),
); );
expect(featureFlagApiNock.isDone()).toBe(true); expect(featureFlagApiNock.isDone()).toBe(true);
expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledTimes(2);
expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });
@ -154,7 +154,7 @@ describe('Ducks - Swaps', () => {
createGetState(), createGetState(),
); );
expect(featureFlagApiNock2.isDone()).toBe(false); // Second API call wasn't made, cache was used instead. expect(featureFlagApiNock2.isDone()).toBe(false); // Second API call wasn't made, cache was used instead.
expect(mockDispatch).toHaveBeenCalledTimes(2); expect(mockDispatch).toHaveBeenCalledTimes(4);
expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness); expect(setSwapsLiveness).toHaveBeenCalledWith(expectedSwapsLiveness);
expect(swapsLiveness).toMatchObject(expectedSwapsLiveness); expect(swapsLiveness).toMatchObject(expectedSwapsLiveness);
}); });

View File

@ -1,4 +1,5 @@
import { useSelector } from 'react-redux'; import isEqual from 'lodash/isEqual';
import { shallowEqual, useSelector } from 'react-redux';
import { import {
getEstimatedGasFeeTimeBounds, getEstimatedGasFeeTimeBounds,
getGasEstimateType, getGasEstimateType,
@ -31,8 +32,11 @@ import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling';
*/ */
export function useGasFeeEstimates() { export function useGasFeeEstimates() {
const gasEstimateType = useSelector(getGasEstimateType); const gasEstimateType = useSelector(getGasEstimateType);
const gasFeeEstimates = useSelector(getGasFeeEstimates); const gasFeeEstimates = useSelector(getGasFeeEstimates, isEqual);
const estimatedGasFeeTimeBounds = useSelector(getEstimatedGasFeeTimeBounds); const estimatedGasFeeTimeBounds = useSelector(
getEstimatedGasFeeTimeBounds,
shallowEqual,
);
const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading); const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading);
useSafeGasEstimatePolling(); useSafeGasEstimatePolling();

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
import { import {
getTokenExchangeRates, getTokenExchangeRates,
getCurrentCurrency, getCurrentCurrency,
@ -29,7 +29,10 @@ export function useTokenFiatAmount(
overrides = {}, overrides = {},
hideCurrencySymbol, hideCurrencySymbol,
) { ) {
const contractExchangeRates = useSelector(getTokenExchangeRates); const contractExchangeRates = useSelector(
getTokenExchangeRates,
shallowEqual,
);
const conversionRate = useSelector(getConversionRate); const conversionRate = useSelector(getConversionRate);
const currentCurrency = useSelector(getCurrentCurrency); const currentCurrency = useSelector(getCurrentCurrency);
const userPrefersShownFiat = useSelector(getShouldShowFiat); const userPrefersShownFiat = useSelector(getShouldShowFiat);

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import TokenTracker from '@metamask/eth-token-tracker'; import TokenTracker from '@metamask/eth-token-tracker';
import { useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
import { getCurrentChainId, getSelectedAddress } from '../selectors'; import { getCurrentChainId, getSelectedAddress } from '../selectors';
import { SECOND } from '../../shared/constants/time'; import { SECOND } from '../../shared/constants/time';
import { isEqualCaseInsensitive } from '../helpers/utils/util'; import { isEqualCaseInsensitive } from '../helpers/utils/util';
@ -12,7 +12,7 @@ export function useTokenTracker(
hideZeroBalanceTokens = false, hideZeroBalanceTokens = false,
) { ) {
const chainId = useSelector(getCurrentChainId); const chainId = useSelector(getCurrentChainId);
const userAddress = useSelector(getSelectedAddress); const userAddress = useSelector(getSelectedAddress, shallowEqual);
const [loading, setLoading] = useState(() => tokens?.length >= 0); const [loading, setLoading] = useState(() => tokens?.length >= 0);
const [tokensWithBalances, setTokensWithBalances] = useState([]); const [tokensWithBalances, setTokensWithBalances] = useState([]);
const [error, setError] = useState(null); const [error, setError] = useState(null);

View File

@ -1,5 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useSelector } from 'react-redux'; import { shallowEqual, useSelector } from 'react-redux';
import contractMap from '@metamask/contract-metadata'; import contractMap from '@metamask/contract-metadata';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { isEqual, shuffle, uniqBy } from 'lodash'; import { isEqual, shuffle, uniqBy } from 'lodash';
@ -94,8 +94,8 @@ export function useTokensToSearch({
const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual); const tokenConversionRates = useSelector(getTokenExchangeRates, isEqual);
const conversionRate = useSelector(getConversionRate); const conversionRate = useSelector(getConversionRate);
const currentCurrency = useSelector(getCurrentCurrency); const currentCurrency = useSelector(getCurrentCurrency);
const defaultSwapsToken = useSelector(getSwapsDefaultToken); const defaultSwapsToken = useSelector(getSwapsDefaultToken, shallowEqual);
const tokenList = useSelector(getTokenList); const tokenList = useSelector(getTokenList, isEqual);
const useTokenDetection = useSelector(getUseTokenDetection); const useTokenDetection = useSelector(getUseTokenDetection);
// token from dynamic api list is fetched when useTokenDetection is true // token from dynamic api list is fetched when useTokenDetection is true
const shuffledTokenList = useTokenDetection const shuffledTokenList = useTokenDetection
@ -115,7 +115,7 @@ export function useTokensToSearch({
); );
const memoizedDefaultToken = useEqualityCheck(defaultToken); const memoizedDefaultToken = useEqualityCheck(defaultToken);
const swapsTokens = useSelector(getSwapsTokens) || []; const swapsTokens = useSelector(getSwapsTokens, isEqual) || [];
const tokensToSearch = swapsTokens.length const tokensToSearch = swapsTokens.length
? swapsTokens ? swapsTokens

View File

@ -77,8 +77,6 @@ export default class Routes extends Component {
alertMessage: PropTypes.string, alertMessage: PropTypes.string,
textDirection: PropTypes.string, textDirection: PropTypes.string,
isNetworkLoading: PropTypes.bool, isNetworkLoading: PropTypes.bool,
provider: PropTypes.object,
frequentRpcListDetail: PropTypes.array,
alertOpen: PropTypes.bool, alertOpen: PropTypes.bool,
isUnlocked: PropTypes.bool, isUnlocked: PropTypes.bool,
setLastActiveTime: PropTypes.func, setLastActiveTime: PropTypes.func,
@ -88,10 +86,12 @@ export default class Routes extends Component {
isMouseUser: PropTypes.bool, isMouseUser: PropTypes.bool,
setMouseUserState: PropTypes.func, setMouseUserState: PropTypes.func,
providerId: PropTypes.string, providerId: PropTypes.string,
providerType: PropTypes.string,
autoLockTimeLimit: PropTypes.number, autoLockTimeLimit: PropTypes.number,
pageChanged: PropTypes.func.isRequired, pageChanged: PropTypes.func.isRequired,
prepareToLeaveSwaps: PropTypes.func, prepareToLeaveSwaps: PropTypes.func,
browserEnvironment: PropTypes.object, browserEnvironmentOs: PropTypes.string,
browserEnvironmentBrowser: PropTypes.string,
}; };
static contextTypes = { static contextTypes = {
@ -287,6 +287,13 @@ export default class Routes extends Component {
); );
} }
onAppHeaderClick = async () => {
const { prepareToLeaveSwaps } = this.props;
if (this.onSwapsPage()) {
await prepareToLeaveSwaps();
}
};
render() { render() {
const { const {
isLoading, isLoading,
@ -295,19 +302,16 @@ export default class Routes extends Component {
textDirection, textDirection,
loadingMessage, loadingMessage,
isNetworkLoading, isNetworkLoading,
provider,
frequentRpcListDetail,
setMouseUserState, setMouseUserState,
isMouseUser, isMouseUser,
prepareToLeaveSwaps, browserEnvironmentOs: os,
browserEnvironment, browserEnvironmentBrowser: browser,
} = this.props; } = this.props;
const loadMessage = const loadMessage =
loadingMessage || isNetworkLoading loadingMessage || isNetworkLoading
? this.getConnectingLabel(loadingMessage) ? this.getConnectingLabel(loadingMessage)
: null; : null;
const { os, browser } = browserEnvironment;
return ( return (
<div <div
className={classnames('app', { className={classnames('app', {
@ -330,11 +334,7 @@ export default class Routes extends Component {
<AppHeader <AppHeader
hideNetworkIndicator={this.onInitializationUnlockPage()} hideNetworkIndicator={this.onInitializationUnlockPage()}
disableNetworkIndicator={this.onSwapsPage()} disableNetworkIndicator={this.onSwapsPage()}
onClick={async () => { onClick={this.onAppHeaderClick}
if (this.onSwapsPage()) {
await prepareToLeaveSwaps();
}
}}
disabled={ disabled={
this.onConfirmPage() || this.onConfirmPage() ||
(this.onSwapsPage() && !this.onSwapsBuildQuotePage()) (this.onSwapsPage() && !this.onSwapsBuildQuotePage())
@ -344,10 +344,7 @@ export default class Routes extends Component {
{process.env.ONBOARDING_V2 && this.showOnboardingHeader() && ( {process.env.ONBOARDING_V2 && this.showOnboardingHeader() && (
<OnboardingAppHeader /> <OnboardingAppHeader />
)} )}
<NetworkDropdown <NetworkDropdown />
provider={provider}
frequentRpcListDetail={frequentRpcListDetail}
/>
<AccountMenu /> <AccountMenu />
<div className="main-container-wrapper"> <div className="main-container-wrapper">
{isLoading ? <Loading loadingMessage={loadMessage} /> : null} {isLoading ? <Loading loadingMessage={loadMessage} /> : null}
@ -377,9 +374,9 @@ export default class Routes extends Component {
if (loadingMessage) { if (loadingMessage) {
return loadingMessage; return loadingMessage;
} }
const { provider, providerId } = this.props; const { providerType, providerId } = this.props;
switch (provider.type) { switch (providerType) {
case 'mainnet': case 'mainnet':
return this.context.t('connectingToMainnet'); return this.context.t('connectingToMainnet');
case 'ropsten': case 'ropsten':
@ -394,21 +391,4 @@ export default class Routes extends Component {
return this.context.t('connectingTo', [providerId]); return this.context.t('connectingTo', [providerId]);
} }
} }
getNetworkName() {
switch (this.props.provider.type) {
case 'mainnet':
return this.context.t('mainnet');
case 'ropsten':
return this.context.t('ropsten');
case 'kovan':
return this.context.t('kovan');
case 'rinkeby':
return this.context.t('rinkeby');
case 'goerli':
return this.context.t('goerli');
default:
return this.context.t('unknownNetwork');
}
}
} }

View File

@ -5,7 +5,6 @@ import {
getNetworkIdentifier, getNetworkIdentifier,
getPreferences, getPreferences,
isNetworkLoading, isNetworkLoading,
submittedPendingTransactionsSelector,
} from '../../selectors'; } from '../../selectors';
import { import {
lockMetamask, lockMetamask,
@ -29,15 +28,14 @@ function mapStateToProps(state) {
isLoading, isLoading,
loadingMessage, loadingMessage,
isUnlocked: state.metamask.isUnlocked, isUnlocked: state.metamask.isUnlocked,
submittedPendingTransactions: submittedPendingTransactionsSelector(state),
isNetworkLoading: isNetworkLoading(state), isNetworkLoading: isNetworkLoading(state),
provider: state.metamask.provider,
frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
currentCurrency: state.metamask.currentCurrency, currentCurrency: state.metamask.currentCurrency,
isMouseUser: state.appState.isMouseUser, isMouseUser: state.appState.isMouseUser,
providerId: getNetworkIdentifier(state),
autoLockTimeLimit, autoLockTimeLimit,
browserEnvironment: state.metamask.browserEnvironment, browserEnvironmentOs: state.metamask.browserEnvironment?.os,
browserEnvironmentContainter: state.metamask.browserEnvironment?.browser,
providerId: getNetworkIdentifier(state),
providerType: state.metamask.provider?.type,
}; };
} }

View File

@ -26,6 +26,8 @@ import {
navigateBackToBuildQuote, navigateBackToBuildQuote,
prepareForRetryGetQuotes, prepareForRetryGetQuotes,
prepareToLeaveSwaps, prepareToLeaveSwaps,
getFromTokenInputValue,
getMaxSlippage,
} from '../../../ducks/swaps/swaps'; } from '../../../ducks/swaps/swaps';
import Mascot from '../../../components/ui/mascot'; import Mascot from '../../../components/ui/mascot';
import Box from '../../../components/ui/box'; import Box from '../../../components/ui/box';
@ -58,8 +60,6 @@ export default function AwaitingSwap({
txHash, txHash,
tokensReceived, tokensReceived,
submittingSwap, submittingSwap,
inputValue,
maxSlippage,
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const metaMetricsEvent = useContext(MetaMetricsContext); const metaMetricsEvent = useContext(MetaMetricsContext);
@ -69,6 +69,8 @@ export default function AwaitingSwap({
const fetchParams = useSelector(getFetchParams); const fetchParams = useSelector(getFetchParams);
const { destinationTokenInfo, sourceTokenInfo } = fetchParams?.metaData || {}; const { destinationTokenInfo, sourceTokenInfo } = fetchParams?.metaData || {};
const fromTokenInputValue = useSelector(getFromTokenInputValue);
const maxSlippage = useSelector(getMaxSlippage);
const usedQuote = useSelector(getUsedQuote); const usedQuote = useSelector(getUsedQuote);
const approveTxParams = useSelector(getApproveTxParams); const approveTxParams = useSelector(getApproveTxParams);
const swapsGasPrice = useSelector(getUsedSwapsGasPrice); const swapsGasPrice = useSelector(getUsedSwapsGasPrice);
@ -283,7 +285,7 @@ export default function AwaitingSwap({
await dispatch( await dispatch(
fetchQuotesAndSetQuoteState( fetchQuotesAndSetQuoteState(
history, history,
inputValue, fromTokenInputValue,
maxSlippage, maxSlippage,
metaMetricsEvent, metaMetricsEvent,
), ),
@ -321,6 +323,4 @@ AwaitingSwap.propTypes = {
CONTRACT_DATA_DISABLED_ERROR, CONTRACT_DATA_DISABLED_ERROR,
]), ]),
submittingSwap: PropTypes.bool, submittingSwap: PropTypes.bool,
inputValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxSlippage: PropTypes.number,
}; };

View File

@ -14,25 +14,20 @@ exports[`BuildQuote renders the component with initial props 1`] = `
2% 2%
</button> </button>
<button <button
aria-checked="false" aria-checked="true"
class="button-group__button radio-button" class="button-group__button radio-button button-group__button--active radio-button--active"
data-testid="button-group__button1" data-testid="button-group__button1"
role="radio" role="radio"
> >
3% 3%
</button> </button>
<button <button
aria-checked="true" aria-checked="false"
class="button-group__button slippage-buttons__button-group-custom-button radio-button--danger radio-button button-group__button--active radio-button--active" class="button-group__button slippage-buttons__button-group-custom-button radio-button"
data-testid="button-group__button2" data-testid="button-group__button2"
role="radio" role="radio"
> >
15 custom
<div
class="slippage-buttons__percentage-suffix"
>
%
</div>
</button> </button>
</div> </div>
`; `;

View File

@ -1,6 +1,7 @@
import React, { useContext, useEffect, useState, useCallback } from 'react'; import React, { useContext, useEffect, useState, useCallback } from 'react';
import BigNumber from 'bignumber.js';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import { uniqBy, isEqual } from 'lodash'; import { uniqBy, isEqual } from 'lodash';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
@ -34,7 +35,15 @@ import {
getTopAssets, getTopAssets,
getFetchParams, getFetchParams,
getQuotes, getQuotes,
setBalanceError,
setFromTokenInputValue,
setFromTokenError,
setMaxSlippage,
setReviewSwapClickedTimestamp, setReviewSwapClickedTimestamp,
getFromTokenInputValue,
getFromTokenError,
getMaxSlippage,
getIsFeatureFlagLoaded,
} from '../../../ducks/swaps/swaps'; } from '../../../ducks/swaps/swaps';
import { import {
getSwapsDefaultToken, getSwapsDefaultToken,
@ -77,6 +86,7 @@ import {
stopPollingForQuotes, stopPollingForQuotes,
} from '../../../store/actions'; } from '../../../store/actions';
import { import {
countDecimals,
fetchTokenPrice, fetchTokenPrice,
fetchTokenBalance, fetchTokenBalance,
shouldEnableDirectWrapping, shouldEnableDirectWrapping,
@ -94,14 +104,8 @@ const MAX_ALLOWED_SLIPPAGE = 15;
let timeoutIdForQuotesPrefetching; let timeoutIdForQuotesPrefetching;
export default function BuildQuote({ export default function BuildQuote({
inputValue,
onInputChange,
ethBalance, ethBalance,
setMaxSlippage,
maxSlippage,
selectedAccountAddress, selectedAccountAddress,
isFeatureFlagLoaded,
tokenFromError,
shuffledTokensList, shuffledTokensList,
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
@ -114,18 +118,22 @@ export default function BuildQuote({
); );
const [verificationClicked, setVerificationClicked] = useState(false); const [verificationClicked, setVerificationClicked] = useState(false);
const isFeatureFlagLoaded = useSelector(getIsFeatureFlagLoaded);
const balanceError = useSelector(getBalanceError); const balanceError = useSelector(getBalanceError);
const fetchParams = useSelector(getFetchParams); const fetchParams = useSelector(getFetchParams, isEqual);
const { sourceTokenInfo = {}, destinationTokenInfo = {} } = const { sourceTokenInfo = {}, destinationTokenInfo = {} } =
fetchParams?.metaData || {}; fetchParams?.metaData || {};
const tokens = useSelector(getTokens); const tokens = useSelector(getTokens, isEqual);
const topAssets = useSelector(getTopAssets); const topAssets = useSelector(getTopAssets);
const fromToken = useSelector(getFromToken); const fromToken = useSelector(getFromToken, isEqual);
const fromTokenInputValue = useSelector(getFromTokenInputValue);
const fromTokenError = useSelector(getFromTokenError);
const maxSlippage = useSelector(getMaxSlippage);
const toToken = useSelector(getToToken) || destinationTokenInfo; const toToken = useSelector(getToToken) || destinationTokenInfo;
const defaultSwapsToken = useSelector(getSwapsDefaultToken); const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual);
const chainId = useSelector(getCurrentChainId); const chainId = useSelector(getCurrentChainId);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider, shallowEqual);
const tokenList = useSelector(getTokenList); const tokenList = useSelector(getTokenList, isEqual);
const useTokenDetection = useSelector(getUseTokenDetection); const useTokenDetection = useSelector(getUseTokenDetection);
const quotes = useSelector(getQuotes, isEqual); const quotes = useSelector(getQuotes, isEqual);
const areQuotesPresent = Object.keys(quotes).length > 0; const areQuotesPresent = Object.keys(quotes).length > 0;
@ -198,7 +206,7 @@ export default function BuildQuote({
const swapFromTokenFiatValue = useTokenFiatAmount( const swapFromTokenFiatValue = useTokenFiatAmount(
fromTokenAddress, fromTokenAddress,
inputValue || 0, fromTokenInputValue || 0,
fromTokenSymbol, fromTokenSymbol,
{ {
showFiat: true, showFiat: true,
@ -206,7 +214,7 @@ export default function BuildQuote({
true, true,
); );
const swapFromEthFiatValue = useEthFiatAmount( const swapFromEthFiatValue = useEthFiatAmount(
inputValue || 0, fromTokenInputValue || 0,
{ showFiat: true }, { showFiat: true },
true, true,
); );
@ -214,6 +222,27 @@ export default function BuildQuote({
? swapFromEthFiatValue ? swapFromEthFiatValue
: swapFromTokenFiatValue; : swapFromTokenFiatValue;
const onInputChange = useCallback(
(newInputValue, balance) => {
dispatch(setFromTokenInputValue(newInputValue));
const newBalanceError = new BigNumber(newInputValue || 0).gt(
balance || 0,
);
// "setBalanceError" is just a warning, a user can still click on the "Review Swap" button.
if (balanceError !== newBalanceError) {
dispatch(setBalanceError(newBalanceError));
}
dispatch(
setFromTokenError(
fromToken && countDecimals(newInputValue) > fromToken.decimals
? 'tooManyDecimals'
: null,
),
);
},
[dispatch, fromToken, balanceError],
);
const onFromSelect = (token) => { const onFromSelect = (token) => {
if ( if (
token?.address && token?.address &&
@ -255,7 +284,7 @@ export default function BuildQuote({
} }
dispatch(setSwapsFromToken(token)); dispatch(setSwapsFromToken(token));
onInputChange( onInputChange(
token?.address ? inputValue : '', token?.address ? fromTokenInputValue : '',
token.string, token.string,
token.decimals, token.decimals,
); );
@ -364,9 +393,14 @@ export default function BuildQuote({
useEffect(() => { useEffect(() => {
if (prevFromTokenBalance !== fromTokenBalance) { if (prevFromTokenBalance !== fromTokenBalance) {
onInputChange(inputValue, fromTokenBalance); onInputChange(fromTokenInputValue, fromTokenBalance);
} }
}, [onInputChange, prevFromTokenBalance, inputValue, fromTokenBalance]); }, [
onInputChange,
prevFromTokenBalance,
fromTokenInputValue,
fromTokenBalance,
]);
useEffect(() => { useEffect(() => {
dispatch(resetSwapsPostFetchState()); dispatch(resetSwapsPostFetchState());
@ -416,9 +450,9 @@ export default function BuildQuote({
selectedToToken.address, selectedToToken.address,
); );
const isReviewSwapButtonDisabled = const isReviewSwapButtonDisabled =
tokenFromError || fromTokenError ||
!isFeatureFlagLoaded || !isFeatureFlagLoaded ||
!Number(inputValue) || !Number(fromTokenInputValue) ||
!selectedToToken?.address || !selectedToToken?.address ||
Number(maxSlippage) < 0 || Number(maxSlippage) < 0 ||
Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE || Number(maxSlippage) > MAX_ALLOWED_SLIPPAGE ||
@ -433,7 +467,7 @@ export default function BuildQuote({
await dispatch( await dispatch(
fetchQuotesAndSetQuoteState( fetchQuotesAndSetQuoteState(
history, history,
inputValue, fromTokenInputValue,
maxSlippage, maxSlippage,
metaMetricsEvent, metaMetricsEvent,
pageRedirectionDisabled, pageRedirectionDisabled,
@ -456,7 +490,7 @@ export default function BuildQuote({
maxSlippage, maxSlippage,
metaMetricsEvent, metaMetricsEvent,
isReviewSwapButtonDisabled, isReviewSwapButtonDisabled,
inputValue, fromTokenInputValue,
fromTokenAddress, fromTokenAddress,
toTokenAddress, toTokenAddress,
]); ]);
@ -484,8 +518,8 @@ export default function BuildQuote({
onInputChange={(value) => { onInputChange={(value) => {
onInputChange(value, fromTokenBalance); onInputChange(value, fromTokenBalance);
}} }}
inputValue={inputValue} inputValue={fromTokenInputValue}
leftValue={inputValue && swapFromFiatValue} leftValue={fromTokenInputValue && swapFromFiatValue}
selectedItem={selectedFromToken} selectedItem={selectedFromToken}
maxListItems={30} maxListItems={30}
loading={ loading={
@ -504,14 +538,14 @@ export default function BuildQuote({
<div <div
className={classnames('build-quote__balance-message', { className={classnames('build-quote__balance-message', {
'build-quote__balance-message--error': 'build-quote__balance-message--error':
balanceError || tokenFromError, balanceError || fromTokenError,
})} })}
> >
{!tokenFromError && {!fromTokenError &&
!balanceError && !balanceError &&
fromTokenSymbol && fromTokenSymbol &&
swapYourTokenBalance} swapYourTokenBalance}
{!tokenFromError && balanceError && fromTokenSymbol && ( {!fromTokenError && 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])}
@ -521,7 +555,7 @@ export default function BuildQuote({
</div> </div>
</div> </div>
)} )}
{tokenFromError && ( {fromTokenError && (
<> <>
<div className="build-quote__form-error"> <div className="build-quote__form-error">
{t('swapTooManyDecimalsError', [ {t('swapTooManyDecimalsError', [
@ -645,7 +679,7 @@ export default function BuildQuote({
<div className="build-quote__slippage-buttons-container"> <div className="build-quote__slippage-buttons-container">
<SlippageButtons <SlippageButtons
onSelect={(newSlippage) => { onSelect={(newSlippage) => {
setMaxSlippage(newSlippage); dispatch(setMaxSlippage(newSlippage));
}} }}
maxAllowedSlippage={MAX_ALLOWED_SLIPPAGE} maxAllowedSlippage={MAX_ALLOWED_SLIPPAGE}
currentSlippage={maxSlippage} currentSlippage={maxSlippage}
@ -664,7 +698,7 @@ export default function BuildQuote({
dispatch( dispatch(
fetchQuotesAndSetQuoteState( fetchQuotesAndSetQuoteState(
history, history,
inputValue, fromTokenInputValue,
maxSlippage, maxSlippage,
metaMetricsEvent, metaMetricsEvent,
), ),
@ -688,13 +722,7 @@ export default function BuildQuote({
} }
BuildQuote.propTypes = { BuildQuote.propTypes = {
maxSlippage: PropTypes.number,
inputValue: PropTypes.string,
onInputChange: PropTypes.func,
ethBalance: PropTypes.string, ethBalance: PropTypes.string,
setMaxSlippage: PropTypes.func,
selectedAccountAddress: PropTypes.string, selectedAccountAddress: PropTypes.string,
isFeatureFlagLoaded: PropTypes.bool.isRequired,
tokenFromError: PropTypes.string,
shuffledTokensList: PropTypes.array, shuffledTokensList: PropTypes.array,
}; };

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import configureMockStore from 'redux-mock-store'; import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { fireEvent } from '@testing-library/react';
import { import {
renderWithProvider, renderWithProvider,
@ -13,11 +12,7 @@ import BuildQuote from '.';
const middleware = [thunk]; const middleware = [thunk];
const createProps = (customProps = {}) => { const createProps = (customProps = {}) => {
return { return {
inputValue: '5',
onInputChange: jest.fn(),
ethBalance: '0x8', ethBalance: '0x8',
setMaxSlippage: jest.fn(),
maxSlippage: 15,
selectedAccountAddress: 'selectedAccountAddress', selectedAccountAddress: 'selectedAccountAddress',
isFeatureFlagLoaded: false, isFeatureFlagLoaded: false,
shuffledTokensList: [], shuffledTokensList: [],
@ -49,28 +44,4 @@ describe('BuildQuote', () => {
document.querySelector('.slippage-buttons__button-group'), document.querySelector('.slippage-buttons__button-group'),
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
it('clicks on the max button', () => {
const store = configureMockStore(middleware)(createSwapsMockStore());
const props = createProps();
const { getByTestId } = renderWithProvider(
<BuildQuote {...props} />,
store,
);
fireEvent.click(getByTestId('build-quote__max-button'));
expect(props.onInputChange).toHaveBeenCalled();
});
it('types a number inside the input field', () => {
const store = configureMockStore(middleware)(createSwapsMockStore());
const props = createProps();
const { getByDisplayValue } = renderWithProvider(
<BuildQuote {...props} />,
store,
);
fireEvent.change(getByDisplayValue('5'), {
target: { value: '8' },
});
expect(props.onInputChange).toHaveBeenCalled();
});
}); });

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef, useContext } from 'react'; import React, { useEffect, useRef, useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { import {
Switch, Switch,
Route, Route,
@ -7,8 +7,7 @@ import {
useHistory, useHistory,
Redirect, Redirect,
} from 'react-router-dom'; } from 'react-router-dom';
import BigNumber from 'bignumber.js'; import { shuffle, isEqual } from 'lodash';
import { shuffle } from 'lodash';
import { I18nContext } from '../../contexts/i18n'; import { I18nContext } from '../../contexts/i18n';
import { import {
getSelectedAccount, getSelectedAccount,
@ -24,7 +23,6 @@ import {
getTradeTxId, getTradeTxId,
getApproveTxId, getApproveTxId,
getFetchingQuotes, getFetchingQuotes,
setBalanceError,
setTopAssets, setTopAssets,
getFetchParams, getFetchParams,
setAggregatorMetadata, setAggregatorMetadata,
@ -35,7 +33,6 @@ import {
prepareToLeaveSwaps, prepareToLeaveSwaps,
fetchAndSetSwapsGasPriceInfo, fetchAndSetSwapsGasPriceInfo,
fetchSwapsLiveness, fetchSwapsLiveness,
getFromToken,
getReviewSwapClickedTimestamp, getReviewSwapClickedTimestamp,
} from '../../ducks/swaps/swaps'; } from '../../ducks/swaps/swaps';
import { import {
@ -77,7 +74,6 @@ 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';
@ -96,21 +92,16 @@ export default function Swap() {
const isSwapsErrorRoute = pathname === SWAPS_ERROR_ROUTE; const isSwapsErrorRoute = pathname === SWAPS_ERROR_ROUTE;
const isLoadingQuotesRoute = pathname === LOADING_QUOTES_ROUTE; const isLoadingQuotesRoute = pathname === LOADING_QUOTES_ROUTE;
const fetchParams = useSelector(getFetchParams); const fetchParams = useSelector(getFetchParams, isEqual);
const { destinationTokenInfo = {} } = fetchParams?.metaData || {}; const { destinationTokenInfo = {} } = fetchParams?.metaData || {};
const [inputValue, setInputValue] = useState(fetchParams?.value || '');
const [maxSlippage, setMaxSlippage] = useState(fetchParams?.slippage || 3);
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, shallowEqual);
const quotes = useSelector(getQuotes); const quotes = useSelector(getQuotes, isEqual);
const txList = useSelector(currentNetworkTxListSelector); const txList = useSelector(currentNetworkTxListSelector, shallowEqual);
const tradeTxId = useSelector(getTradeTxId); const tradeTxId = useSelector(getTradeTxId);
const approveTxId = useSelector(getApproveTxId); const approveTxId = useSelector(getApproveTxId);
const aggregatorMetadata = useSelector(getAggregatorMetadata); const aggregatorMetadata = useSelector(getAggregatorMetadata, shallowEqual);
const fetchingQuotes = useSelector(getFetchingQuotes); const fetchingQuotes = useSelector(getFetchingQuotes);
let swapsErrorKey = useSelector(getSwapsErrorKey); let swapsErrorKey = useSelector(getSwapsErrorKey);
const swapsEnabled = useSelector(getSwapsFeatureIsLive); const swapsEnabled = useSelector(getSwapsFeatureIsLive);
@ -119,8 +110,7 @@ export default function Swap() {
const networkAndAccountSupports1559 = useSelector( const networkAndAccountSupports1559 = useSelector(
checkNetworkAndAccountSupports1559, checkNetworkAndAccountSupports1559,
); );
const fromToken = useSelector(getFromToken); const tokenList = useSelector(getTokenList, isEqual);
const tokenList = useSelector(getTokenList);
const listTokenValues = shuffle(Object.values(tokenList)); const listTokenValues = shuffle(Object.values(tokenList));
const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp); const reviewSwapClickedTimestamp = useSelector(getReviewSwapClickedTimestamp);
const reviewSwapClicked = Boolean(reviewSwapClickedTimestamp); const reviewSwapClicked = Boolean(reviewSwapClickedTimestamp);
@ -237,7 +227,6 @@ export default function Swap() {
useEffect(() => { useEffect(() => {
const fetchSwapsLivenessWrapper = async () => { const fetchSwapsLivenessWrapper = async () => {
await dispatch(fetchSwapsLiveness()); await dispatch(fetchSwapsLiveness());
setIsFeatureFlagLoaded(true);
}; };
fetchSwapsLivenessWrapper(); fetchSwapsLivenessWrapper();
return () => { return () => {
@ -308,31 +297,10 @@ export default function Swap() {
return <Redirect to={{ pathname: LOADING_QUOTES_ROUTE }} />; return <Redirect to={{ pathname: LOADING_QUOTES_ROUTE }} />;
} }
const onInputChange = (newInputValue, balance) => {
setInputValue(newInputValue);
const balanceError = 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(
fromToken &&
countDecimals(newInputValue) > fromToken.decimals
? 'tooManyDecimals'
: null,
);
};
return ( return (
<BuildQuote <BuildQuote
inputValue={inputValue}
onInputChange={onInputChange}
ethBalance={ethBalance} ethBalance={ethBalance}
setMaxSlippage={setMaxSlippage}
selectedAccountAddress={selectedAccountAddress} selectedAccountAddress={selectedAccountAddress}
maxSlippage={maxSlippage}
isFeatureFlagLoaded={isFeatureFlagLoaded}
tokenFromError={tokenFromError}
shuffledTokensList={listTokenValues} shuffledTokensList={listTokenValues}
/> />
); );
@ -364,8 +332,6 @@ export default function Swap() {
swapComplete={false} swapComplete={false}
errorKey={swapsErrorKey} errorKey={swapsErrorKey}
txHash={tradeTxData?.hash} txHash={tradeTxData?.hash}
inputValue={inputValue}
maxSlippage={maxSlippage}
submittedTime={tradeTxData?.submittedTime} submittedTime={tradeTxData?.submittedTime}
/> />
); );
@ -433,8 +399,6 @@ export default function Swap() {
submittingSwap={ submittingSwap={
routeState === 'awaiting' && !(approveTxId || tradeTxId) routeState === 'awaiting' && !(approveTxId || tradeTxId)
} }
inputValue={inputValue}
maxSlippage={maxSlippage}
/> />
) : ( ) : (
<Redirect to={{ pathname: DEFAULT_ROUTE }} /> <Redirect to={{ pathname: DEFAULT_ROUTE }} />