From d255fcdefbd4c24cd41e7d2d39f41cb58617c538 Mon Sep 17 00:00:00 2001 From: Daniel <80175477+dan437@users.noreply.github.com> Date: Tue, 9 Aug 2022 19:56:52 +0200 Subject: [PATCH] Only check if a user has enough token balance before calling STX (#15218) --- app/scripts/metamask-controller.js | 3 + package.json | 2 +- ui/ducks/swaps/swaps.js | 23 +++-- ui/helpers/utils/conversions.util.js | 9 ++ ui/helpers/utils/conversions.util.test.js | 12 +++ ui/pages/swaps/build-quote/build-quote.js | 10 +++ .../swaps/build-quote/build-quote.test.js | 1 + ui/pages/swaps/view-quote/view-quote.js | 90 ++++++++++++++++--- ui/store/actions.js | 26 ++++-- yarn.lock | 8 +- 10 files changed, 151 insertions(+), 33 deletions(-) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 88ad384b0..f025712e2 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1891,6 +1891,9 @@ export default class MetamaskController extends EventEmitter { fetchSmartTransactionFees: smartTransactionsController.getFees.bind( smartTransactionsController, ), + clearSmartTransactionFees: smartTransactionsController.clearFees.bind( + smartTransactionsController, + ), submitSignedTransactions: smartTransactionsController.submitSignedTransactions.bind( smartTransactionsController, diff --git a/package.json b/package.json index 4dc090537..ebba350fe 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "@metamask/providers": "^9.0.0", "@metamask/rpc-methods": "^0.18.1", "@metamask/slip44": "^2.1.0", - "@metamask/smart-transactions-controller": "^2.1.0", + "@metamask/smart-transactions-controller": "^2.3.0", "@metamask/snap-controllers": "^0.18.1", "@ngraveio/bc-ur": "^1.1.6", "@popperjs/core": "^2.4.0", diff --git a/ui/ducks/swaps/swaps.js b/ui/ducks/swaps/swaps.js index f8bffbaad..9641f6fe8 100644 --- a/ui/ducks/swaps/swaps.js +++ b/ui/ducks/swaps/swaps.js @@ -928,10 +928,11 @@ export const signAndSendSwapsSmartTransaction = ({ }; } const fees = await dispatch( - fetchSwapsSmartTransactionFees( + fetchSwapsSmartTransactionFees({ unsignedTransaction, - updatedApproveTxParams, - ), + approveTxParams: updatedApproveTxParams, + fallbackOnNotEnoughFunds: true, + }), ); if (!fees) { log.error('"fetchSwapsSmartTransactionFees" failed'); @@ -994,7 +995,7 @@ export const signAndSendSwapsSmartTransaction = ({ } = getState(); if (e.message.startsWith('Fetch error:') && isFeatureFlagLoaded) { const errorObj = parseSmartTransactionsError(e.message); - dispatch(setCurrentSmartTransactionsError(errorObj?.type)); + dispatch(setCurrentSmartTransactionsError(errorObj?.error)); } } }; @@ -1306,10 +1307,11 @@ export function fetchMetaSwapsGasPriceEstimates() { }; } -export function fetchSwapsSmartTransactionFees( +export function fetchSwapsSmartTransactionFees({ unsignedTransaction, approveTxParams, -) { + fallbackOnNotEnoughFunds = false, +}) { return async (dispatch, getState) => { const { swaps: { isFeatureFlagLoaded }, @@ -1321,7 +1323,12 @@ export function fetchSwapsSmartTransactionFees( } catch (e) { if (e.message.startsWith('Fetch error:') && isFeatureFlagLoaded) { const errorObj = parseSmartTransactionsError(e.message); - dispatch(setCurrentSmartTransactionsError(errorObj?.type)); + if ( + fallbackOnNotEnoughFunds || + errorObj?.error !== stxErrorTypes.NOT_ENOUGH_FUNDS + ) { + dispatch(setCurrentSmartTransactionsError(errorObj?.error)); + } } } return null; @@ -1338,7 +1345,7 @@ export function cancelSwapsSmartTransaction(uuid) { } = getState(); if (e.message.startsWith('Fetch error:') && isFeatureFlagLoaded) { const errorObj = parseSmartTransactionsError(e.message); - dispatch(setCurrentSmartTransactionsError(errorObj?.type)); + dispatch(setCurrentSmartTransactionsError(errorObj?.error)); } } }; diff --git a/ui/helpers/utils/conversions.util.js b/ui/helpers/utils/conversions.util.js index cdfee12b9..fab3d5be9 100644 --- a/ui/helpers/utils/conversions.util.js +++ b/ui/helpers/utils/conversions.util.js @@ -162,6 +162,15 @@ export function hexWEIToDecETH(hexWEI) { }); } +export function decWEIToDecETH(hexWEI) { + return conversionUtil(hexWEI, { + fromNumericBase: 'dec', + toNumericBase: 'dec', + fromDenomination: 'WEI', + toDenomination: 'ETH', + }); +} + export function addHexes(aHexWEI, bHexWEI) { return addCurrencies(aHexWEI, bHexWEI, { aBase: 16, diff --git a/ui/helpers/utils/conversions.util.test.js b/ui/helpers/utils/conversions.util.test.js index 1947975a6..af2f0d7a0 100644 --- a/ui/helpers/utils/conversions.util.test.js +++ b/ui/helpers/utils/conversions.util.test.js @@ -39,4 +39,16 @@ describe('conversion utils', () => { expect(weiValue).toStrictEqual('1000000000000000000'); }); }); + + describe('decWEIToDecETH', () => { + it('converts 10000000000000 WEI to ETH', () => { + const ethDec = utils.decWEIToDecETH('10000000000000'); + expect('0.00001').toStrictEqual(ethDec); + }); + + it('converts 9358749494527040 WEI to ETH', () => { + const ethDec = utils.decWEIToDecETH('9358749494527040'); + expect('0.009358749').toStrictEqual(ethDec); + }); + }); }); diff --git a/ui/pages/swaps/build-quote/build-quote.js b/ui/pages/swaps/build-quote/build-quote.js index 2902c33db..010a0f6c8 100644 --- a/ui/pages/swaps/build-quote/build-quote.js +++ b/ui/pages/swaps/build-quote/build-quote.js @@ -58,6 +58,7 @@ import { getMaxSlippage, getIsFeatureFlagLoaded, getCurrentSmartTransactionsError, + getSmartTransactionFees, } from '../../../ducks/swaps/swaps'; import { getSwapsDefaultToken, @@ -100,6 +101,7 @@ import { clearSwapsQuotes, stopPollingForQuotes, setSmartTransactionsOptInStatus, + clearSmartTransactionFees, } from '../../../store/actions'; import { countDecimals, @@ -165,6 +167,7 @@ export default function BuildQuote({ const currentSmartTransactionsEnabled = useSelector( getCurrentSmartTransactionsEnabled, ); + const smartTransactionFees = useSelector(getSmartTransactionFees); const smartTransactionsOptInPopoverDisplayed = smartTransactionsOptInStatus !== undefined; const currentSmartTransactionsError = useSelector( @@ -470,6 +473,13 @@ export default function BuildQuote({ trackBuildQuotePageLoadedEvent(); }, [dispatch, trackBuildQuotePageLoadedEvent]); + useEffect(() => { + if (smartTransactionsEnabled && smartTransactionFees?.tradeTxFees) { + // We want to clear STX fees, because we only want to use fresh ones on the View Quote page. + clearSmartTransactionFees(); + } + }, [smartTransactionsEnabled, smartTransactionFees]); + const BlockExplorerLink = () => { return ( { diff --git a/ui/pages/swaps/view-quote/view-quote.js b/ui/pages/swaps/view-quote/view-quote.js index 6755d3ee3..b2ac7eaab 100644 --- a/ui/pages/swaps/view-quote/view-quote.js +++ b/ui/pages/swaps/view-quote/view-quote.js @@ -45,6 +45,7 @@ import { signAndSendSwapsSmartTransaction, getSwapsNetworkConfig, getSmartTransactionsEnabled, + getSmartTransactionsError, getCurrentSmartTransactionsError, getCurrentSmartTransactionsErrorMessageDismissed, getSwapsSTXLoading, @@ -74,6 +75,7 @@ import { showModal, setSwapsQuotesPollingLimitEnabled, } from '../../../store/actions'; +import { SET_SMART_TRANSACTIONS_ERROR } from '../../../store/actionConstants'; import { ASSET_ROUTE, BUILD_QUOTE_ROUTE, @@ -91,6 +93,7 @@ import { decGWEIToHexWEI, hexWEIToDecGWEI, addHexes, + decWEIToDecETH, } from '../../../helpers/utils/conversions.util'; import MainQuoteSummary from '../main-quote-summary'; import { calcGasTotal } from '../../send/send.utils'; @@ -184,6 +187,7 @@ export default function ViewQuote() { const currentSmartTransactionsError = useSelector( getCurrentSmartTransactionsError, ); + const smartTransactionsError = useSelector(getSmartTransactionsError); const currentSmartTransactionsErrorMessageDismissed = useSelector( getCurrentSmartTransactionsErrorMessageDismissed, ); @@ -443,15 +447,35 @@ export default function ViewQuote() { ) : null; - const destinationToken = useSelector(getDestinationTokenInfo, isEqual); + let ethBalanceNeededStx; + if (smartTransactionsError?.balanceNeededWei) { + ethBalanceNeededStx = decWEIToDecETH( + smartTransactionsError.balanceNeededWei - + smartTransactionsError.currentBalanceWei, + ); + } + const destinationToken = useSelector(getDestinationTokenInfo, isEqual); useEffect(() => { - if (insufficientTokens || insufficientEth) { + if (currentSmartTransactionsEnabled && smartTransactionsOptInStatus) { + if (insufficientTokens) { + dispatch(setBalanceError(true)); + } else if (balanceError && !insufficientTokens) { + dispatch(setBalanceError(false)); + } + } else if (insufficientTokens || insufficientEth) { dispatch(setBalanceError(true)); } else if (balanceError && !insufficientTokens && !insufficientEth) { dispatch(setBalanceError(false)); } - }, [insufficientTokens, insufficientEth, balanceError, dispatch]); + }, [ + insufficientTokens, + insufficientEth, + balanceError, + dispatch, + currentSmartTransactionsEnabled, + smartTransactionsOptInStatus, + ]); useEffect(() => { const currentTime = Date.now(); @@ -480,8 +504,24 @@ export default function ViewQuote() { } }, [originalApproveAmount, approveAmount]); + // If it's not a Smart Transaction and ETH balance is needed, we want to show a warning. + const isNotStxAndEthBalanceIsNeeded = + (!currentSmartTransactionsEnabled || !smartTransactionsOptInStatus) && + ethBalanceNeeded; + + // If it's a Smart Transaction and ETH balance is needed, we want to show a warning. + const isStxAndEthBalanceIsNeeded = + currentSmartTransactionsEnabled && + smartTransactionsOptInStatus && + ethBalanceNeededStx; + + // Indicates if we should show to a user a warning about insufficient funds for swapping. const showInsufficientWarning = - (balanceError || tokenBalanceNeeded || ethBalanceNeeded) && !warningHidden; + (balanceError || + tokenBalanceNeeded || + isNotStxAndEthBalanceIsNeeded || + isStxAndEthBalanceIsNeeded) && + !warningHidden; const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletType = useSelector(getHardwareWalletType); @@ -656,12 +696,11 @@ export default function ViewQuote() { }), ); }; - const actionableBalanceErrorMessage = tokenBalanceUnavailable ? t('swapTokenBalanceUnavailable', [sourceTokenSymbol]) : t('swapApproveNeedMoreTokens', [ - {tokenBalanceNeeded || ethBalanceNeeded} + {tokenBalanceNeeded || ethBalanceNeededStx || ethBalanceNeeded} , tokenBalanceNeeded && !(sourceTokenSymbol === defaultSwapsToken.symbol) ? sourceTokenSymbol @@ -755,14 +794,18 @@ export default function ViewQuote() { baseAndPriorityFeePerGas === undefined) || (!networkAndAccountSupports1559 && (gasPrice === null || gasPrice === undefined)) || - (currentSmartTransactionsEnabled && currentSmartTransactionsError), + (currentSmartTransactionsEnabled && + (currentSmartTransactionsError || smartTransactionsError)) || + (currentSmartTransactionsEnabled && + smartTransactionsOptInStatus && + !smartTransactionFees?.tradeTxFees), ); useEffect(() => { if ( currentSmartTransactionsEnabled && smartTransactionsOptInStatus && - !isSwapButtonDisabled + !insufficientTokens ) { const unsignedTx = { from: unsignedTransaction.from, @@ -774,10 +817,22 @@ export default function ViewQuote() { }; intervalId = setInterval(() => { if (!swapsSTXLoading) { - dispatch(fetchSwapsSmartTransactionFees(unsignedTx, approveTxParams)); + dispatch( + fetchSwapsSmartTransactionFees({ + unsignedTransaction: unsignedTx, + approveTxParams, + fallbackOnNotEnoughFunds: false, + }), + ); } }, swapsNetworkConfig.stxGetTransactionsRefreshTime); - dispatch(fetchSwapsSmartTransactionFees(unsignedTx, approveTxParams)); + dispatch( + fetchSwapsSmartTransactionFees({ + unsignedTransaction: unsignedTx, + approveTxParams, + fallbackOnNotEnoughFunds: false, + }), + ); } else if (intervalId) { clearInterval(intervalId); } @@ -794,7 +849,7 @@ export default function ViewQuote() { unsignedTransaction.to, chainId, swapsNetworkConfig.stxGetTransactionsRefreshTime, - isSwapButtonDisabled, + insufficientTokens, ]); useEffect(() => { @@ -820,6 +875,16 @@ export default function ViewQuote() { submitClicked, ]); + useEffect(() => { + if (currentSmartTransactionsEnabled && smartTransactionsOptInStatus) { + // Removes a smart transactions error when the component loads. + dispatch({ + type: SET_SMART_TRANSACTIONS_ERROR, + payload: null, + }); + } + }, [currentSmartTransactionsEnabled, smartTransactionsOptInStatus, dispatch]); + return (
{currentSmartTransactionsEnabled && smartTransactionsOptInStatus && - !smartTransactionFees?.tradeTxFees && ( + !smartTransactionFees?.tradeTxFees && + !showInsufficientWarning && ( diff --git a/ui/store/actions.js b/ui/store/actions.js index 483291c53..620127be0 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -3565,6 +3565,10 @@ export async function setSmartTransactionsOptInStatus( await promisifiedBackground.setSmartTransactionsOptInStatus(optInState); } +export function clearSmartTransactionFees() { + promisifiedBackground.clearSmartTransactionFees(); +} + export function fetchSmartTransactionFees( unsignedTransaction, approveTxParams, @@ -3574,17 +3578,23 @@ export function fetchSmartTransactionFees( approveTxParams.value = '0x0'; } try { - return await promisifiedBackground.fetchSmartTransactionFees( - unsignedTransaction, - approveTxParams, - ); + const smartTransactionFees = + await promisifiedBackground.fetchSmartTransactionFees( + unsignedTransaction, + approveTxParams, + ); + dispatch({ + type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, + payload: null, + }); + return smartTransactionFees; } catch (e) { log.error(e); if (e.message.startsWith('Fetch error:')) { const errorObj = parseSmartTransactionsError(e.message); dispatch({ type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj.type, + payload: errorObj, }); } throw e; @@ -3647,7 +3657,7 @@ export function signAndSendSmartTransaction({ const errorObj = parseSmartTransactionsError(e.message); dispatch({ type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj.type, + payload: errorObj, }); } throw e; @@ -3668,7 +3678,7 @@ export function updateSmartTransaction(uuid, txData) { const errorObj = parseSmartTransactionsError(e.message); dispatch({ type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj.type, + payload: errorObj, }); } throw e; @@ -3696,7 +3706,7 @@ export function cancelSmartTransaction(uuid) { const errorObj = parseSmartTransactionsError(e.message); dispatch({ type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, - payload: errorObj.type, + payload: errorObj, }); } throw e; diff --git a/yarn.lock b/yarn.lock index 1ca971087..80e056607 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3124,10 +3124,10 @@ resolved "https://registry.yarnpkg.com/@metamask/slip44/-/slip44-2.1.0.tgz#f76764ca54afc162fbfe563f1994b79ed4711bba" integrity sha512-wkFDdY4XtpF+XCqbgwhsrLRgEM/bYfIt47927JTQZQ2QxQYRbSZ6u0QygnVjIR1eqMteRGx2jtUUZ+bxYQTo/w== -"@metamask/smart-transactions-controller@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@metamask/smart-transactions-controller/-/smart-transactions-controller-2.1.0.tgz#a1bfbdab05c0ecd93bac5587b8bc56336bc28cc5" - integrity sha512-nhvR44ELv/8iBVHaMT8D6B1KLwm9PAb7BSSLAiteDfncxHj7syNGD0nOFynzKp7TPQuRlPXdFo6Z8zXt6O/krg== +"@metamask/smart-transactions-controller@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@metamask/smart-transactions-controller/-/smart-transactions-controller-2.3.0.tgz#8e451975fbfc624f7cd55b2d1bc406f78fe95119" + integrity sha512-ef8RolP/synZZ9RVMaRAApZmiUbRlAAs0Pt3u/R0+fXeB0NTdUljQ7TfKa4kfulcxW7EbSnJ7kNabeBinyE4vw== dependencies: "@metamask/controllers" "^30.0.0" "@types/lodash" "^4.14.176"