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

feature/default_network_editable
Daniel 2 years ago committed by GitHub
parent 753666d9c2
commit d255fcdefb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      app/scripts/metamask-controller.js
  2. 2
      package.json
  3. 23
      ui/ducks/swaps/swaps.js
  4. 9
      ui/helpers/utils/conversions.util.js
  5. 12
      ui/helpers/utils/conversions.util.test.js
  6. 10
      ui/pages/swaps/build-quote/build-quote.js
  7. 1
      ui/pages/swaps/build-quote/build-quote.test.js
  8. 90
      ui/pages/swaps/view-quote/view-quote.js
  9. 26
      ui/store/actions.js
  10. 8
      yarn.lock

@ -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,

@ -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",

@ -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));
}
}
};

@ -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,

@ -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);
});
});
});

@ -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 (
<a

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

@ -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', [
<span key="swapApproveNeedMoreTokens-1" className="view-quote__bold">
{tokenBalanceNeeded || ethBalanceNeeded}
{tokenBalanceNeeded || ethBalanceNeededStx || ethBalanceNeeded}
</span>,
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 (
<div className="view-quote">
<div
@ -876,7 +941,8 @@ export default function ViewQuote() {
/>
{currentSmartTransactionsEnabled &&
smartTransactionsOptInStatus &&
!smartTransactionFees?.tradeTxFees && (
!smartTransactionFees?.tradeTxFees &&
!showInsufficientWarning && (
<Box marginTop={0} marginBottom={10}>
<PulseLoader />
</Box>

@ -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;

@ -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"

Loading…
Cancel
Save