1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Only check if a user has enough token balance before calling STX (#15218)

This commit is contained in:
Daniel 2022-08-09 19:56:52 +02:00 committed by GitHub
parent 753666d9c2
commit d255fcdefb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 151 additions and 33 deletions

View File

@ -1891,6 +1891,9 @@ export default class MetamaskController extends EventEmitter {
fetchSmartTransactionFees: smartTransactionsController.getFees.bind( fetchSmartTransactionFees: smartTransactionsController.getFees.bind(
smartTransactionsController, smartTransactionsController,
), ),
clearSmartTransactionFees: smartTransactionsController.clearFees.bind(
smartTransactionsController,
),
submitSignedTransactions: submitSignedTransactions:
smartTransactionsController.submitSignedTransactions.bind( smartTransactionsController.submitSignedTransactions.bind(
smartTransactionsController, smartTransactionsController,

View File

@ -132,7 +132,7 @@
"@metamask/providers": "^9.0.0", "@metamask/providers": "^9.0.0",
"@metamask/rpc-methods": "^0.18.1", "@metamask/rpc-methods": "^0.18.1",
"@metamask/slip44": "^2.1.0", "@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", "@metamask/snap-controllers": "^0.18.1",
"@ngraveio/bc-ur": "^1.1.6", "@ngraveio/bc-ur": "^1.1.6",
"@popperjs/core": "^2.4.0", "@popperjs/core": "^2.4.0",

View File

@ -928,10 +928,11 @@ export const signAndSendSwapsSmartTransaction = ({
}; };
} }
const fees = await dispatch( const fees = await dispatch(
fetchSwapsSmartTransactionFees( fetchSwapsSmartTransactionFees({
unsignedTransaction, unsignedTransaction,
updatedApproveTxParams, approveTxParams: updatedApproveTxParams,
), fallbackOnNotEnoughFunds: true,
}),
); );
if (!fees) { if (!fees) {
log.error('"fetchSwapsSmartTransactionFees" failed'); log.error('"fetchSwapsSmartTransactionFees" failed');
@ -994,7 +995,7 @@ export const signAndSendSwapsSmartTransaction = ({
} = getState(); } = getState();
if (e.message.startsWith('Fetch error:') && isFeatureFlagLoaded) { if (e.message.startsWith('Fetch error:') && isFeatureFlagLoaded) {
const errorObj = parseSmartTransactionsError(e.message); 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, unsignedTransaction,
approveTxParams, approveTxParams,
) { fallbackOnNotEnoughFunds = false,
}) {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const { const {
swaps: { isFeatureFlagLoaded }, swaps: { isFeatureFlagLoaded },
@ -1321,7 +1323,12 @@ export function fetchSwapsSmartTransactionFees(
} catch (e) { } catch (e) {
if (e.message.startsWith('Fetch error:') && isFeatureFlagLoaded) { if (e.message.startsWith('Fetch error:') && isFeatureFlagLoaded) {
const errorObj = parseSmartTransactionsError(e.message); const errorObj = parseSmartTransactionsError(e.message);
dispatch(setCurrentSmartTransactionsError(errorObj?.type)); if (
fallbackOnNotEnoughFunds ||
errorObj?.error !== stxErrorTypes.NOT_ENOUGH_FUNDS
) {
dispatch(setCurrentSmartTransactionsError(errorObj?.error));
}
} }
} }
return null; return null;
@ -1338,7 +1345,7 @@ export function cancelSwapsSmartTransaction(uuid) {
} = getState(); } = getState();
if (e.message.startsWith('Fetch error:') && isFeatureFlagLoaded) { if (e.message.startsWith('Fetch error:') && isFeatureFlagLoaded) {
const errorObj = parseSmartTransactionsError(e.message); const errorObj = parseSmartTransactionsError(e.message);
dispatch(setCurrentSmartTransactionsError(errorObj?.type)); dispatch(setCurrentSmartTransactionsError(errorObj?.error));
} }
} }
}; };

View File

@ -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) { export function addHexes(aHexWEI, bHexWEI) {
return addCurrencies(aHexWEI, bHexWEI, { return addCurrencies(aHexWEI, bHexWEI, {
aBase: 16, aBase: 16,

View File

@ -39,4 +39,16 @@ describe('conversion utils', () => {
expect(weiValue).toStrictEqual('1000000000000000000'); 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);
});
});
}); });

View File

@ -58,6 +58,7 @@ import {
getMaxSlippage, getMaxSlippage,
getIsFeatureFlagLoaded, getIsFeatureFlagLoaded,
getCurrentSmartTransactionsError, getCurrentSmartTransactionsError,
getSmartTransactionFees,
} from '../../../ducks/swaps/swaps'; } from '../../../ducks/swaps/swaps';
import { import {
getSwapsDefaultToken, getSwapsDefaultToken,
@ -100,6 +101,7 @@ import {
clearSwapsQuotes, clearSwapsQuotes,
stopPollingForQuotes, stopPollingForQuotes,
setSmartTransactionsOptInStatus, setSmartTransactionsOptInStatus,
clearSmartTransactionFees,
} from '../../../store/actions'; } from '../../../store/actions';
import { import {
countDecimals, countDecimals,
@ -165,6 +167,7 @@ export default function BuildQuote({
const currentSmartTransactionsEnabled = useSelector( const currentSmartTransactionsEnabled = useSelector(
getCurrentSmartTransactionsEnabled, getCurrentSmartTransactionsEnabled,
); );
const smartTransactionFees = useSelector(getSmartTransactionFees);
const smartTransactionsOptInPopoverDisplayed = const smartTransactionsOptInPopoverDisplayed =
smartTransactionsOptInStatus !== undefined; smartTransactionsOptInStatus !== undefined;
const currentSmartTransactionsError = useSelector( const currentSmartTransactionsError = useSelector(
@ -470,6 +473,13 @@ export default function BuildQuote({
trackBuildQuotePageLoadedEvent(); trackBuildQuotePageLoadedEvent();
}, [dispatch, 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 = () => { const BlockExplorerLink = () => {
return ( return (
<a <a

View File

@ -26,6 +26,7 @@ setBackgroundConnection({
setBackgroundSwapRouteState: jest.fn(), setBackgroundSwapRouteState: jest.fn(),
clearSwapsQuotes: jest.fn(), clearSwapsQuotes: jest.fn(),
stopPollingForQuotes: jest.fn(), stopPollingForQuotes: jest.fn(),
clearSmartTransactionFees: jest.fn(),
}); });
describe('BuildQuote', () => { describe('BuildQuote', () => {

View File

@ -45,6 +45,7 @@ import {
signAndSendSwapsSmartTransaction, signAndSendSwapsSmartTransaction,
getSwapsNetworkConfig, getSwapsNetworkConfig,
getSmartTransactionsEnabled, getSmartTransactionsEnabled,
getSmartTransactionsError,
getCurrentSmartTransactionsError, getCurrentSmartTransactionsError,
getCurrentSmartTransactionsErrorMessageDismissed, getCurrentSmartTransactionsErrorMessageDismissed,
getSwapsSTXLoading, getSwapsSTXLoading,
@ -74,6 +75,7 @@ import {
showModal, showModal,
setSwapsQuotesPollingLimitEnabled, setSwapsQuotesPollingLimitEnabled,
} from '../../../store/actions'; } from '../../../store/actions';
import { SET_SMART_TRANSACTIONS_ERROR } from '../../../store/actionConstants';
import { import {
ASSET_ROUTE, ASSET_ROUTE,
BUILD_QUOTE_ROUTE, BUILD_QUOTE_ROUTE,
@ -91,6 +93,7 @@ import {
decGWEIToHexWEI, decGWEIToHexWEI,
hexWEIToDecGWEI, hexWEIToDecGWEI,
addHexes, addHexes,
decWEIToDecETH,
} from '../../../helpers/utils/conversions.util'; } from '../../../helpers/utils/conversions.util';
import MainQuoteSummary from '../main-quote-summary'; import MainQuoteSummary from '../main-quote-summary';
import { calcGasTotal } from '../../send/send.utils'; import { calcGasTotal } from '../../send/send.utils';
@ -184,6 +187,7 @@ export default function ViewQuote() {
const currentSmartTransactionsError = useSelector( const currentSmartTransactionsError = useSelector(
getCurrentSmartTransactionsError, getCurrentSmartTransactionsError,
); );
const smartTransactionsError = useSelector(getSmartTransactionsError);
const currentSmartTransactionsErrorMessageDismissed = useSelector( const currentSmartTransactionsErrorMessageDismissed = useSelector(
getCurrentSmartTransactionsErrorMessageDismissed, getCurrentSmartTransactionsErrorMessageDismissed,
); );
@ -443,15 +447,35 @@ export default function ViewQuote() {
) )
: null; : null;
const destinationToken = useSelector(getDestinationTokenInfo, isEqual); let ethBalanceNeededStx;
if (smartTransactionsError?.balanceNeededWei) {
ethBalanceNeededStx = decWEIToDecETH(
smartTransactionsError.balanceNeededWei -
smartTransactionsError.currentBalanceWei,
);
}
const destinationToken = useSelector(getDestinationTokenInfo, isEqual);
useEffect(() => { 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)); dispatch(setBalanceError(true));
} else if (balanceError && !insufficientTokens && !insufficientEth) { } else if (balanceError && !insufficientTokens && !insufficientEth) {
dispatch(setBalanceError(false)); dispatch(setBalanceError(false));
} }
}, [insufficientTokens, insufficientEth, balanceError, dispatch]); }, [
insufficientTokens,
insufficientEth,
balanceError,
dispatch,
currentSmartTransactionsEnabled,
smartTransactionsOptInStatus,
]);
useEffect(() => { useEffect(() => {
const currentTime = Date.now(); const currentTime = Date.now();
@ -480,8 +504,24 @@ export default function ViewQuote() {
} }
}, [originalApproveAmount, approveAmount]); }, [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 = const showInsufficientWarning =
(balanceError || tokenBalanceNeeded || ethBalanceNeeded) && !warningHidden; (balanceError ||
tokenBalanceNeeded ||
isNotStxAndEthBalanceIsNeeded ||
isStxAndEthBalanceIsNeeded) &&
!warningHidden;
const hardwareWalletUsed = useSelector(isHardwareWallet); const hardwareWalletUsed = useSelector(isHardwareWallet);
const hardwareWalletType = useSelector(getHardwareWalletType); const hardwareWalletType = useSelector(getHardwareWalletType);
@ -656,12 +696,11 @@ export default function ViewQuote() {
}), }),
); );
}; };
const actionableBalanceErrorMessage = tokenBalanceUnavailable const actionableBalanceErrorMessage = tokenBalanceUnavailable
? t('swapTokenBalanceUnavailable', [sourceTokenSymbol]) ? t('swapTokenBalanceUnavailable', [sourceTokenSymbol])
: t('swapApproveNeedMoreTokens', [ : t('swapApproveNeedMoreTokens', [
<span key="swapApproveNeedMoreTokens-1" className="view-quote__bold"> <span key="swapApproveNeedMoreTokens-1" className="view-quote__bold">
{tokenBalanceNeeded || ethBalanceNeeded} {tokenBalanceNeeded || ethBalanceNeededStx || ethBalanceNeeded}
</span>, </span>,
tokenBalanceNeeded && !(sourceTokenSymbol === defaultSwapsToken.symbol) tokenBalanceNeeded && !(sourceTokenSymbol === defaultSwapsToken.symbol)
? sourceTokenSymbol ? sourceTokenSymbol
@ -755,14 +794,18 @@ export default function ViewQuote() {
baseAndPriorityFeePerGas === undefined) || baseAndPriorityFeePerGas === undefined) ||
(!networkAndAccountSupports1559 && (!networkAndAccountSupports1559 &&
(gasPrice === null || gasPrice === undefined)) || (gasPrice === null || gasPrice === undefined)) ||
(currentSmartTransactionsEnabled && currentSmartTransactionsError), (currentSmartTransactionsEnabled &&
(currentSmartTransactionsError || smartTransactionsError)) ||
(currentSmartTransactionsEnabled &&
smartTransactionsOptInStatus &&
!smartTransactionFees?.tradeTxFees),
); );
useEffect(() => { useEffect(() => {
if ( if (
currentSmartTransactionsEnabled && currentSmartTransactionsEnabled &&
smartTransactionsOptInStatus && smartTransactionsOptInStatus &&
!isSwapButtonDisabled !insufficientTokens
) { ) {
const unsignedTx = { const unsignedTx = {
from: unsignedTransaction.from, from: unsignedTransaction.from,
@ -774,10 +817,22 @@ export default function ViewQuote() {
}; };
intervalId = setInterval(() => { intervalId = setInterval(() => {
if (!swapsSTXLoading) { if (!swapsSTXLoading) {
dispatch(fetchSwapsSmartTransactionFees(unsignedTx, approveTxParams)); dispatch(
fetchSwapsSmartTransactionFees({
unsignedTransaction: unsignedTx,
approveTxParams,
fallbackOnNotEnoughFunds: false,
}),
);
} }
}, swapsNetworkConfig.stxGetTransactionsRefreshTime); }, swapsNetworkConfig.stxGetTransactionsRefreshTime);
dispatch(fetchSwapsSmartTransactionFees(unsignedTx, approveTxParams)); dispatch(
fetchSwapsSmartTransactionFees({
unsignedTransaction: unsignedTx,
approveTxParams,
fallbackOnNotEnoughFunds: false,
}),
);
} else if (intervalId) { } else if (intervalId) {
clearInterval(intervalId); clearInterval(intervalId);
} }
@ -794,7 +849,7 @@ export default function ViewQuote() {
unsignedTransaction.to, unsignedTransaction.to,
chainId, chainId,
swapsNetworkConfig.stxGetTransactionsRefreshTime, swapsNetworkConfig.stxGetTransactionsRefreshTime,
isSwapButtonDisabled, insufficientTokens,
]); ]);
useEffect(() => { useEffect(() => {
@ -820,6 +875,16 @@ export default function ViewQuote() {
submitClicked, 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 ( return (
<div className="view-quote"> <div className="view-quote">
<div <div
@ -876,7 +941,8 @@ export default function ViewQuote() {
/> />
{currentSmartTransactionsEnabled && {currentSmartTransactionsEnabled &&
smartTransactionsOptInStatus && smartTransactionsOptInStatus &&
!smartTransactionFees?.tradeTxFees && ( !smartTransactionFees?.tradeTxFees &&
!showInsufficientWarning && (
<Box marginTop={0} marginBottom={10}> <Box marginTop={0} marginBottom={10}>
<PulseLoader /> <PulseLoader />
</Box> </Box>

View File

@ -3565,6 +3565,10 @@ export async function setSmartTransactionsOptInStatus(
await promisifiedBackground.setSmartTransactionsOptInStatus(optInState); await promisifiedBackground.setSmartTransactionsOptInStatus(optInState);
} }
export function clearSmartTransactionFees() {
promisifiedBackground.clearSmartTransactionFees();
}
export function fetchSmartTransactionFees( export function fetchSmartTransactionFees(
unsignedTransaction, unsignedTransaction,
approveTxParams, approveTxParams,
@ -3574,17 +3578,23 @@ export function fetchSmartTransactionFees(
approveTxParams.value = '0x0'; approveTxParams.value = '0x0';
} }
try { try {
return await promisifiedBackground.fetchSmartTransactionFees( const smartTransactionFees =
unsignedTransaction, await promisifiedBackground.fetchSmartTransactionFees(
approveTxParams, unsignedTransaction,
); approveTxParams,
);
dispatch({
type: actionConstants.SET_SMART_TRANSACTIONS_ERROR,
payload: null,
});
return smartTransactionFees;
} catch (e) { } catch (e) {
log.error(e); log.error(e);
if (e.message.startsWith('Fetch error:')) { if (e.message.startsWith('Fetch error:')) {
const errorObj = parseSmartTransactionsError(e.message); const errorObj = parseSmartTransactionsError(e.message);
dispatch({ dispatch({
type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, type: actionConstants.SET_SMART_TRANSACTIONS_ERROR,
payload: errorObj.type, payload: errorObj,
}); });
} }
throw e; throw e;
@ -3647,7 +3657,7 @@ export function signAndSendSmartTransaction({
const errorObj = parseSmartTransactionsError(e.message); const errorObj = parseSmartTransactionsError(e.message);
dispatch({ dispatch({
type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, type: actionConstants.SET_SMART_TRANSACTIONS_ERROR,
payload: errorObj.type, payload: errorObj,
}); });
} }
throw e; throw e;
@ -3668,7 +3678,7 @@ export function updateSmartTransaction(uuid, txData) {
const errorObj = parseSmartTransactionsError(e.message); const errorObj = parseSmartTransactionsError(e.message);
dispatch({ dispatch({
type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, type: actionConstants.SET_SMART_TRANSACTIONS_ERROR,
payload: errorObj.type, payload: errorObj,
}); });
} }
throw e; throw e;
@ -3696,7 +3706,7 @@ export function cancelSmartTransaction(uuid) {
const errorObj = parseSmartTransactionsError(e.message); const errorObj = parseSmartTransactionsError(e.message);
dispatch({ dispatch({
type: actionConstants.SET_SMART_TRANSACTIONS_ERROR, type: actionConstants.SET_SMART_TRANSACTIONS_ERROR,
payload: errorObj.type, payload: errorObj,
}); });
} }
throw e; throw e;

View File

@ -3124,10 +3124,10 @@
resolved "https://registry.yarnpkg.com/@metamask/slip44/-/slip44-2.1.0.tgz#f76764ca54afc162fbfe563f1994b79ed4711bba" resolved "https://registry.yarnpkg.com/@metamask/slip44/-/slip44-2.1.0.tgz#f76764ca54afc162fbfe563f1994b79ed4711bba"
integrity sha512-wkFDdY4XtpF+XCqbgwhsrLRgEM/bYfIt47927JTQZQ2QxQYRbSZ6u0QygnVjIR1eqMteRGx2jtUUZ+bxYQTo/w== integrity sha512-wkFDdY4XtpF+XCqbgwhsrLRgEM/bYfIt47927JTQZQ2QxQYRbSZ6u0QygnVjIR1eqMteRGx2jtUUZ+bxYQTo/w==
"@metamask/smart-transactions-controller@^2.1.0": "@metamask/smart-transactions-controller@^2.3.0":
version "2.1.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/@metamask/smart-transactions-controller/-/smart-transactions-controller-2.1.0.tgz#a1bfbdab05c0ecd93bac5587b8bc56336bc28cc5" resolved "https://registry.yarnpkg.com/@metamask/smart-transactions-controller/-/smart-transactions-controller-2.3.0.tgz#8e451975fbfc624f7cd55b2d1bc406f78fe95119"
integrity sha512-nhvR44ELv/8iBVHaMT8D6B1KLwm9PAb7BSSLAiteDfncxHj7syNGD0nOFynzKp7TPQuRlPXdFo6Z8zXt6O/krg== integrity sha512-ef8RolP/synZZ9RVMaRAApZmiUbRlAAs0Pt3u/R0+fXeB0NTdUljQ7TfKa4kfulcxW7EbSnJ7kNabeBinyE4vw==
dependencies: dependencies:
"@metamask/controllers" "^30.0.0" "@metamask/controllers" "^30.0.0"
"@types/lodash" "^4.14.176" "@types/lodash" "^4.14.176"