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

Unable to proceed with tx bc of inaccurate/overly aggressive "Insufficient funds for gas" warning #13087 (#14634)

Co-authored-by: VSaric <vladimir.saric@consensys.net>
This commit is contained in:
dragana8 2022-11-28 17:41:42 +01:00 committed by GitHub
parent f44af06f9b
commit 2680340e27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 646 additions and 26 deletions

View File

@ -978,6 +978,9 @@
"deleteNetworkDescription": { "deleteNetworkDescription": {
"message": "Are you sure you want to delete this network?" "message": "Are you sure you want to delete this network?"
}, },
"deposit": {
"message": "Deposit"
},
"depositCrypto": { "depositCrypto": {
"message": "Deposit $1", "message": "Deposit $1",
"description": "$1 represents the crypto symbol to be purchased" "description": "$1 represents the crypto symbol to be purchased"
@ -1756,6 +1759,10 @@
"message": "You do not have enough $1 in your account to pay for transaction fees on $2 network. $3 or deposit from another account.", "message": "You do not have enough $1 in your account to pay for transaction fees on $2 network. $3 or deposit from another account.",
"description": "$1 is the native currency of the network, $2 is the name of the current network, $3 is the key 'buy' + the ticker symbol of the native currency of the chain wrapped in a button" "description": "$1 is the native currency of the network, $2 is the name of the current network, $3 is the key 'buy' + the ticker symbol of the native currency of the chain wrapped in a button"
}, },
"insufficientCurrencyBuyOrReceive": {
"message": "You do not have enough $1 in your account to pay for transaction fees on $2 network. $3 or $4 from another account.",
"description": "$1 is the native currency of the network, $2 is the name of the current network, $3 is the key 'buy' + the ticker symbol of the native currency of the chain wrapped in a button, $4 is the key 'deposit' button"
},
"insufficientCurrencyDeposit": { "insufficientCurrencyDeposit": {
"message": "You do not have enough $1 in your account to pay for transaction fees on $2 network. Deposit $1 from another account.", "message": "You do not have enough $1 in your account to pay for transaction fees on $2 network. Deposit $1 from another account.",
"description": "$1 is the native currency of the network, $2 is the name of the current network" "description": "$1 is the native currency of the network, $2 is the name of the current network"

View File

@ -37,8 +37,8 @@ describe('Send ETH from inside MetaMask using default gas', function () {
const errorAmount = await driver.findElement('.send-v2__error-amount'); const errorAmount = await driver.findElement('.send-v2__error-amount');
assert.equal( assert.equal(
await errorAmount.getText(), await errorAmount.getText(),
'Insufficient funds.', 'Insufficient funds for gas',
'send screen should render an insufficient fund error message', 'send screen should render an insufficient fund for gas error message',
); );
await inputAmount.press(driver.Key.BACK_SPACE); await inputAmount.press(driver.Key.BACK_SPACE);

View File

@ -14,6 +14,7 @@ import {
getGasFeeEstimates, getGasFeeEstimates,
getIsGasEstimatesLoading, getIsGasEstimatesLoading,
} from '../../../ducks/metamask/metamask'; } from '../../../ducks/metamask/metamask';
import { getEIP1559V2Enabled } from '../../../selectors';
import Typography from '../../ui/typography/typography'; import Typography from '../../ui/typography/typography';
import { import {
@ -45,6 +46,7 @@ export default function GasTiming({
const gasEstimateType = useSelector(getGasEstimateType); const gasEstimateType = useSelector(getGasEstimateType);
const gasFeeEstimates = useSelector(getGasFeeEstimates); const gasFeeEstimates = useSelector(getGasFeeEstimates);
const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading); const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading);
const eip1559V2Enabled = useSelector(getEIP1559V2Enabled);
const [customEstimatedTime, setCustomEstimatedTime] = useState(null); const [customEstimatedTime, setCustomEstimatedTime] = useState(null);
const t = useContext(I18nContext); const t = useContext(I18nContext);
@ -195,8 +197,10 @@ export default function GasTiming({
<Typography <Typography
variant={TYPOGRAPHY.H7} variant={TYPOGRAPHY.H7}
className={classNames('gas-timing', { className={classNames('gas-timing', {
[`gas-timing--${attitude}`]: attitude && !supportsEIP1559V2, [`gas-timing--${attitude}`]:
[`gas-timing--${attitude}-V2`]: attitude && supportsEIP1559V2, attitude && (eip1559V2Enabled || !supportsEIP1559V2),
[`gas-timing--${attitude}-V2`]:
attitude && (eip1559V2Enabled || supportsEIP1559V2),
})} })}
> >
{text} {text}

View File

@ -13,6 +13,7 @@ import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../../shared/constants/gas';
import { import {
CONTRACT_ADDRESS_ERROR, CONTRACT_ADDRESS_ERROR,
INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_FUNDS_ERROR,
INSUFFICIENT_FUNDS_FOR_GAS_ERROR,
INSUFFICIENT_TOKENS_ERROR, INSUFFICIENT_TOKENS_ERROR,
INVALID_RECIPIENT_ADDRESS_ERROR, INVALID_RECIPIENT_ADDRESS_ERROR,
INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR, INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR,
@ -1277,7 +1278,7 @@ const slice = createSlice({
const draftTransaction = const draftTransaction =
state.draftTransactions[state.currentTransactionUUID]; state.draftTransactions[state.currentTransactionUUID];
switch (true) { switch (true) {
// set error to INSUFFICIENT_FUNDS_ERROR if the account balance is lower // set error to INSUFFICIENT_FUNDS_FOR_GAS_ERROR if the account balance is lower
// than the total price of the transaction inclusive of gas fees. // than the total price of the transaction inclusive of gas fees.
case draftTransaction.asset.type === ASSET_TYPES.NATIVE && case draftTransaction.asset.type === ASSET_TYPES.NATIVE &&
!isBalanceSufficient({ !isBalanceSufficient({
@ -1285,9 +1286,9 @@ const slice = createSlice({
balance: draftTransaction.asset.balance, balance: draftTransaction.asset.balance,
gasTotal: draftTransaction.gas.gasTotal ?? '0x0', gasTotal: draftTransaction.gas.gasTotal ?? '0x0',
}): }):
draftTransaction.amount.error = INSUFFICIENT_FUNDS_ERROR; draftTransaction.amount.error = INSUFFICIENT_FUNDS_FOR_GAS_ERROR;
break; break;
// set error to INSUFFICIENT_FUNDS_ERROR if the token balance is lower // set error to INSUFFICIENT_TOKENS_ERROR if the token balance is lower
// than the amount of token the user is attempting to send. // than the amount of token the user is attempting to send.
case draftTransaction.asset.type === ASSET_TYPES.TOKEN && case draftTransaction.asset.type === ASSET_TYPES.TOKEN &&
!isTokenBalanceSufficient({ !isTokenBalanceSufficient({

View File

@ -5,6 +5,7 @@ import { ethers } from 'ethers';
import { import {
CONTRACT_ADDRESS_ERROR, CONTRACT_ADDRESS_ERROR,
INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_FUNDS_ERROR,
INSUFFICIENT_FUNDS_FOR_GAS_ERROR,
INSUFFICIENT_TOKENS_ERROR, INSUFFICIENT_TOKENS_ERROR,
INVALID_RECIPIENT_ADDRESS_ERROR, INVALID_RECIPIENT_ADDRESS_ERROR,
KNOWN_RECIPIENT_ADDRESS_WARNING, KNOWN_RECIPIENT_ADDRESS_WARNING,
@ -856,7 +857,7 @@ describe('Send Slice', () => {
const draftTransaction = getTestUUIDTx(result); const draftTransaction = getTestUUIDTx(result);
expect(draftTransaction.amount.error).toStrictEqual( expect(draftTransaction.amount.error).toStrictEqual(
INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_FUNDS_FOR_GAS_ERROR,
); );
}); });

View File

@ -15,6 +15,7 @@
@import 'create-account/index'; @import 'create-account/index';
@import 'error/index'; @import 'error/index';
@import 'first-time-flow/index'; @import 'first-time-flow/index';
@import 'send/gas-display/index';
@import 'home/index'; @import 'home/index';
@import 'keychains/index'; @import 'keychains/index';
@import 'permissions-connect/index'; @import 'permissions-connect/index';

View File

@ -0,0 +1,500 @@
import React, { useContext, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { I18nContext } from '../../../contexts/i18n';
import { useGasFeeContext } from '../../../contexts/gasFee';
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common';
import { isLegacyTransaction } from '../../../helpers/utils/transactions.util';
import { hexWEIToDecGWEI } from '../../../../shared/lib/transactions-controller-utils';
import UserPreferencedCurrencyDisplay from '../../../components/app/user-preferenced-currency-display';
import GasTiming from '../../../components/app/gas-timing';
import InfoTooltip from '../../../components/ui/info-tooltip';
import Typography from '../../../components/ui/typography';
import Button from '../../../components/ui/button';
import Box from '../../../components/ui/box';
import {
TYPOGRAPHY,
DISPLAY,
FLEX_DIRECTION,
BLOCK_SIZES,
COLORS,
FONT_STYLE,
FONT_WEIGHT,
} from '../../../helpers/constants/design-system';
import {
ERC1155,
ERC20,
ERC721,
} from '../../../../shared/constants/transaction';
import LoadingHeartBeat from '../../../components/ui/loading-heartbeat';
import TransactionDetailItem from '../../../components/app/transaction-detail-item';
import { NETWORK_TO_NAME_MAP } from '../../../../shared/constants/network';
import TransactionDetail from '../../../components/app/transaction-detail';
import ActionableMessage from '../../../components/ui/actionable-message';
import DepositPopover from '../../../components/app/deposit-popover';
import {
getProvider,
getPreferences,
getIsBuyableChain,
transactionFeeSelector,
getIsMainnet,
getEIP1559V2Enabled,
checkNetworkAndAccountSupports1559,
} from '../../../selectors';
import {
hexWEIToDecETH,
addHexes,
} from '../../../helpers/utils/conversions.util';
import { INSUFFICIENT_TOKENS_ERROR } from '../send.constants';
import { getCurrentDraftTransaction } from '../../../ducks/send';
import { showModal } from '../../../store/actions';
export default function GasDisplay({ gasError }) {
const t = useContext(I18nContext);
const dispatch = useDispatch();
const { estimateUsed } = useGasFeeContext();
const [showDepositPopover, setShowDepositPopover] = useState(false);
const currentProvider = useSelector(getProvider);
const isMainnet = useSelector(getIsMainnet);
const isBuyableChain = useSelector(getIsBuyableChain);
const draftTransaction = useSelector(getCurrentDraftTransaction);
const eip1559V2Enabled = useSelector(getEIP1559V2Enabled);
const networkAndAccountSupports1559 = useSelector(
checkNetworkAndAccountSupports1559,
);
const { useNativeCurrencyAsPrimaryCurrency } = useSelector(getPreferences);
const { nativeCurrency, provider, unapprovedTxs } = useSelector(
(state) => state.metamask,
);
const { confirmTransaction } = useSelector((state) => state);
const { txData } = confirmTransaction;
const { txParams = {} } = txData;
const supportsEIP1559 =
networkAndAccountSupports1559 && !isLegacyTransaction(txParams);
const { chainId } = provider;
const networkName = NETWORK_TO_NAME_MAP[chainId];
const isInsufficientTokenError =
draftTransaction?.amount.error === INSUFFICIENT_TOKENS_ERROR;
const editingTransaction = unapprovedTxs[draftTransaction.id];
const supportsEIP1559V2 = eip1559V2Enabled && supportsEIP1559;
const transactionData = {
txParams: {
gasPrice: draftTransaction.gas?.gasPrice,
gas: editingTransaction?.userEditedGasLimit
? editingTransaction?.txParams?.gas
: draftTransaction.gas?.gasLimit,
maxFeePerGas: editingTransaction?.txParams?.maxFeePerGas
? editingTransaction?.txParams?.maxFeePerGas
: draftTransaction.gas?.maxFeePerGas,
maxPriorityFeePerGas: editingTransaction?.txParams?.maxPriorityFeePerGas
? editingTransaction?.txParams?.maxPriorityFeePerGas
: draftTransaction.gas?.maxPriorityFeePerGas,
value: draftTransaction.amount?.value,
type: draftTransaction.transactionType,
},
userFeeLevel: editingTransaction?.userFeeLevel,
};
const {
hexMinimumTransactionFee,
hexMaximumTransactionFee,
hexTransactionTotal,
} = useSelector((state) => transactionFeeSelector(state, transactionData));
let title;
if (
draftTransaction?.asset.details?.standard === ERC721 ||
draftTransaction?.asset.details?.standard === ERC1155
) {
title = draftTransaction?.asset.details?.name;
} else if (draftTransaction?.asset.details?.standard === ERC20) {
title = `${hexWEIToDecETH(draftTransaction.amount.value)} ${
draftTransaction?.asset.details?.symbol
}`;
}
const ethTransactionTotalMaxAmount = Number(
hexWEIToDecETH(hexMaximumTransactionFee),
);
const primaryTotalTextOverrideMaxAmount = `${title} + ${ethTransactionTotalMaxAmount} ${nativeCurrency}`;
let detailTotal, maxAmount;
if (draftTransaction?.asset.type === 'NATIVE') {
detailTotal = (
<Box
height={BLOCK_SIZES.MAX}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
className="gas-display__total-value"
>
<LoadingHeartBeat estimateUsed={transactionData?.userFeeLevel} />
<UserPreferencedCurrencyDisplay
type={PRIMARY}
key="total-detail-value"
value={hexTransactionTotal}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</Box>
);
maxAmount = (
<UserPreferencedCurrencyDisplay
type={PRIMARY}
key="total-max-amount"
value={addHexes(
draftTransaction.amount.value,
hexMaximumTransactionFee,
)}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
);
} else if (useNativeCurrencyAsPrimaryCurrency) {
detailTotal = primaryTotalTextOverrideMaxAmount;
maxAmount = primaryTotalTextOverrideMaxAmount;
}
return (
<>
{showDepositPopover && (
<DepositPopover onClose={() => setShowDepositPopover(false)} />
)}
<Box className="gas-display">
<TransactionDetail
userAcknowledgedGasMissing={false}
rows={[
supportsEIP1559V2 ? (
<TransactionDetailItem
key="gas-item"
detailTitle={
<Box display={DISPLAY.FLEX}>
<Box marginRight={1}>{t('gas')}</Box>
<Typography
as="span"
marginTop={0}
color={COLORS.TEXT_MUTED}
fontStyle={FONT_STYLE.ITALIC}
fontWeight={FONT_WEIGHT.NORMAL}
className="gas-display__title__estimate"
>
({t('transactionDetailGasInfoV2')})
</Typography>
<InfoTooltip
contentText={
<>
<Typography variant={TYPOGRAPHY.H7}>
{t('transactionDetailGasTooltipIntro', [
isMainnet ? t('networkNameEthereum') : '',
])}
</Typography>
<Typography variant={TYPOGRAPHY.H7}>
{t('transactionDetailGasTooltipExplanation')}
</Typography>
<Typography variant={TYPOGRAPHY.H7}>
<a
href="https://community.metamask.io/t/what-is-gas-why-do-transactions-take-so-long/3172"
target="_blank"
rel="noopener noreferrer"
>
{t('transactionDetailGasTooltipConversion')}
</a>
</Typography>
</>
}
position="right"
/>
</Box>
}
detailTitleColor={COLORS.TEXT_DEFAULT}
detailText={
<Box className="gas-display__currency-container">
<LoadingHeartBeat estimateUsed={estimateUsed} />
<UserPreferencedCurrencyDisplay
type={SECONDARY}
value={hexMinimumTransactionFee}
hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)}
/>
</Box>
}
detailTotal={
<Box className="gas-display__currency-container">
<LoadingHeartBeat estimateUsed={estimateUsed} />
<UserPreferencedCurrencyDisplay
type={PRIMARY}
value={hexMinimumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</Box>
}
subText={
<>
<Box
key="editGasSubTextFeeLabel"
display={DISPLAY.INLINE_FLEX}
className={classNames('gas-display__gas-fee-label', {
'gas-display__gas-fee-warning': estimateUsed === 'high',
})}
>
<LoadingHeartBeat estimateUsed={estimateUsed} />
<Box marginRight={1}>
<strong>
{estimateUsed === 'high' && '⚠ '}
{t('editGasSubTextFeeLabel')}
</strong>
</Box>
<Box
key="editGasSubTextFeeValue"
className="gas-display__currency-container"
>
<LoadingHeartBeat estimateUsed={estimateUsed} />
<UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount"
type={PRIMARY}
value={hexMaximumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</Box>
</Box>
</>
}
subTitle={
<GasTiming
maxPriorityFeePerGas={hexWEIToDecGWEI(
draftTransaction.gas.maxPriorityFeePerGas,
)}
maxFeePerGas={hexWEIToDecGWEI(
draftTransaction.gas.maxFeePerGas,
)}
/>
}
/>
) : (
<TransactionDetailItem
key="gas-item"
detailTitle={
<>
{t('transactionDetailGasHeading')}
<InfoTooltip
contentText={
<>
<p>
{t('transactionDetailGasTooltipIntro', [
isMainnet ? t('networkNameEthereum') : '',
])}
</p>
<p>{t('transactionDetailGasTooltipExplanation')}</p>
<p>
<a
href="https://community.metamask.io/t/what-is-gas-why-do-transactions-take-so-long/3172"
target="_blank"
rel="noopener noreferrer"
>
{t('transactionDetailGasTooltipConversion')}
</a>
</p>
</>
}
position="right"
>
<i className="fa fa-info-circle" />
</InfoTooltip>
</>
}
detailText={
<Box
height={BLOCK_SIZES.MAX}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
className="gas-display__currency-container"
>
<LoadingHeartBeat
estimateUsed={transactionData?.userFeeLevel}
/>
<UserPreferencedCurrencyDisplay
type={SECONDARY}
value={hexMinimumTransactionFee}
hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)}
/>
</Box>
}
detailTotal={
<Box
height={BLOCK_SIZES.MAX}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
className="gas-display__currency-container"
>
<LoadingHeartBeat
estimateUsed={transactionData?.userFeeLevel}
/>
<UserPreferencedCurrencyDisplay
type={PRIMARY}
value={hexMinimumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
numberOfDecimals={6}
/>
</Box>
}
subText={
<>
<strong key="editGasSubTextFeeLabel">
{t('editGasSubTextFeeLabel')}
</strong>
<Box
height={BLOCK_SIZES.MAX}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
className="gas-display__currency-container"
>
<LoadingHeartBeat
estimateUsed={
transactionData?.userFeeLevel ?? estimateUsed
}
/>
<UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount"
type={PRIMARY}
value={hexMaximumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</Box>
</>
}
subTitle={
<>
<GasTiming
maxPriorityFeePerGas={hexWEIToDecGWEI(
draftTransaction.gas.maxPriorityFeePerGas,
)}
maxFeePerGas={hexWEIToDecGWEI(
draftTransaction.gas.maxFeePerGas,
)}
/>
</>
}
/>
),
(gasError || isInsufficientTokenError) && (
<TransactionDetailItem
key="total-item"
detailTitle={t('total')}
detailText={
<Box
height={BLOCK_SIZES.MAX}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
className="gas-display__total-value"
>
<LoadingHeartBeat
estimateUsed={transactionData?.userFeeLevel}
/>
<UserPreferencedCurrencyDisplay
type={SECONDARY}
key="total-detail-text"
value={hexTransactionTotal}
hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)}
/>
</Box>
}
detailTotal={detailTotal}
subTitle={t('transactionDetailGasTotalSubtitle')}
subText={
<Box
height={BLOCK_SIZES.MAX}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
className="gas-display__total-amount"
>
<LoadingHeartBeat
estimateUsed={
transactionData?.userFeeLevel ?? estimateUsed
}
/>
<strong key="editGasSubTextAmountLabel">
{t('editGasSubTextAmountLabel')}
</strong>{' '}
{maxAmount}
</Box>
}
/>
),
]}
/>
</Box>
{(gasError || isInsufficientTokenError) && (
<Box className="gas-display__warning-message">
<Box
paddingTop={0}
paddingRight={4}
paddingBottom={4}
paddingLeft={4}
className="gas-display__confirm-approve-content__warning"
>
<ActionableMessage
message={
isBuyableChain && draftTransaction.asset.type === 'NATIVE' ? (
<Typography variant={TYPOGRAPHY.H7} align="left">
{t('insufficientCurrencyBuyOrReceive', [
nativeCurrency,
networkName ?? currentProvider.nickname,
<Button
type="inline"
className="confirm-page-container-content__link"
onClick={() => {
setShowDepositPopover(true);
}}
key={`${nativeCurrency}-buy-button`}
>
{t('buyAsset', [nativeCurrency])}
</Button>,
<Button
type="inline"
className="gas-display__link"
onClick={() =>
dispatch(showModal({ name: 'ACCOUNT_DETAILS' }))
}
key="receive-button"
>
{t('deposit')}
</Button>,
])}
</Typography>
) : (
<Typography variant={TYPOGRAPHY.H7} align="left">
{t('insufficientCurrencyBuyOrReceive', [
draftTransaction.asset.details?.symbol ?? nativeCurrency,
networkName ?? currentProvider.nickname,
`${t('buyAsset', [
draftTransaction.asset.details?.symbol ??
nativeCurrency,
])}`,
<Button
type="inline"
className="gas-display__link"
onClick={() =>
dispatch(showModal({ name: 'ACCOUNT_DETAILS' }))
}
key="receive-button"
>
{t('deposit')}
</Button>,
])}
</Typography>
)
}
useIcon
iconFillColor="var(--color-error-default)"
type="danger"
/>
</Box>
</Box>
)}
</>
);
}
GasDisplay.propTypes = {
gasError: PropTypes.string,
};

View File

@ -0,0 +1 @@
export { default } from './gas-display';

View File

@ -0,0 +1,58 @@
.gas-display {
overflow-y: auto;
flex: 1;
.transaction-detail-rows {
padding: 10px;
border-radius: 8px;
border: 1px solid var(--color-border-default);
margin: 16px 16px;
.transaction-detail-item {
&:not(:first-child) {
border-top: 1px solid var(--color-border-default);
}
}
}
&__title {
&__estimate {
font-size: 12px;
line-height: inherit;
}
}
&__gas-fee-warning {
color: var(--color-warning-default);
}
&__gas-fee-label {
position: relative;
white-space: nowrap;
}
&__warning-message {
height: 120px;
}
&__currency-container,
&__total-amount,
&__total-value {
position: relative;
}
&__confirm-approve-content {
&__warning {
@media screen and (max-width: $break-small) {
padding: 0 32px 16px 16px;
position: fixed;
bottom: 80px;
z-index: 1;
}
}
}
&__link {
text-transform: lowercase;
}
}

View File

@ -12,6 +12,7 @@ import {
} from '../../../helpers/constants/error-keys'; } from '../../../helpers/constants/error-keys';
import { ASSET_TYPES } from '../../../../shared/constants/transaction'; import { ASSET_TYPES } from '../../../../shared/constants/transaction';
import { CONTRACT_ADDRESS_LINK } from '../../../helpers/constants/common'; import { CONTRACT_ADDRESS_LINK } from '../../../helpers/constants/common';
import GasDisplay from '../gas-display';
import SendAmountRow from './send-amount-row'; import SendAmountRow from './send-amount-row';
import SendHexDataRow from './send-hex-data-row'; import SendHexDataRow from './send-hex-data-row';
import SendAssetRow from './send-asset-row'; import SendAssetRow from './send-asset-row';
@ -81,7 +82,6 @@ export default class SendContent extends Component {
<PageContainerContent> <PageContainerContent>
<div className="send-v2__form"> <div className="send-v2__form">
{assetError ? this.renderError(assetError) : null} {assetError ? this.renderError(assetError) : null}
{gasError ? this.renderError(gasError) : null}
{isEthGasPrice {isEthGasPrice
? this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY) ? this.renderWarning(ETH_GAS_PRICE_FETCH_WARNING_KEY)
: null} : null}
@ -97,6 +97,7 @@ export default class SendContent extends Component {
<SendAmountRow /> <SendAmountRow />
{networkOrAccountNotSupports1559 ? <SendGasRow /> : null} {networkOrAccountNotSupports1559 ? <SendGasRow /> : null}
{showHexData ? <SendHexDataRow /> : null} {showHexData ? <SendHexDataRow /> : null}
<GasDisplay gasError={gasError} />
</div> </div>
</PageContainerContent> </PageContainerContent>
); );

View File

@ -150,21 +150,6 @@ describe('SendContent Component', () => {
).toStrictEqual(true); ).toStrictEqual(true);
expect(wrapper.find(Dialog)).toHaveLength(0); expect(wrapper.find(Dialog)).toHaveLength(0);
}); });
it('should render insufficient gas dialog', () => {
wrapper.setProps({
showHexData: false,
getIsBalanceInsufficient: true,
});
const PageContainerContentChild = wrapper
.find(PageContainerContent)
.children();
const errorDialogProps = PageContainerContentChild.childAt(0).props();
expect(errorDialogProps.className).toStrictEqual('send__error-dialog');
expect(errorDialogProps.children).toStrictEqual(
'insufficientFundsForGas_t',
);
});
}); });
it('should not render the asset dropdown if token length is 0', () => { it('should not render the asset dropdown if token length is 0', () => {

View File

@ -15,7 +15,6 @@ import {
acknowledgeRecipientWarning, acknowledgeRecipientWarning,
getRecipientWarningAcknowledgement, getRecipientWarningAcknowledgement,
} from '../../../ducks/send'; } from '../../../ducks/send';
import SendContent from './send-content.component'; import SendContent from './send-content.component';
function mapStateToProps(state) { function mapStateToProps(state) {
@ -24,6 +23,7 @@ function mapStateToProps(state) {
const recipient = getRecipient(state); const recipient = getRecipient(state);
const recipientWarningAcknowledged = const recipientWarningAcknowledged =
getRecipientWarningAcknowledgement(state); getRecipientWarningAcknowledgement(state);
return { return {
isOwnedAccount: Boolean( isOwnedAccount: Boolean(
ownedAccounts.find( ownedAccounts.find(

View File

@ -31,6 +31,7 @@ const TOKEN_TRANSFER_FUNCTION_SIGNATURE = '0xa9059cbb';
const COLLECTIBLE_TRANSFER_FROM_FUNCTION_SIGNATURE = '0x23b872dd'; const COLLECTIBLE_TRANSFER_FROM_FUNCTION_SIGNATURE = '0x23b872dd';
const INSUFFICIENT_FUNDS_ERROR = 'insufficientFunds'; const INSUFFICIENT_FUNDS_ERROR = 'insufficientFunds';
const INSUFFICIENT_FUNDS_FOR_GAS_ERROR = 'insufficientFundsForGas';
const INSUFFICIENT_TOKENS_ERROR = 'insufficientTokens'; const INSUFFICIENT_TOKENS_ERROR = 'insufficientTokens';
const NEGATIVE_ETH_ERROR = 'negativeETH'; const NEGATIVE_ETH_ERROR = 'negativeETH';
const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient'; const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient';
@ -56,6 +57,7 @@ export {
MAX_GAS_LIMIT_DEC, MAX_GAS_LIMIT_DEC,
HIGH_FEE_WARNING_MULTIPLIER, HIGH_FEE_WARNING_MULTIPLIER,
INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_FUNDS_ERROR,
INSUFFICIENT_FUNDS_FOR_GAS_ERROR,
INSUFFICIENT_TOKENS_ERROR, INSUFFICIENT_TOKENS_ERROR,
INVALID_RECIPIENT_ADDRESS_ERROR, INVALID_RECIPIENT_ADDRESS_ERROR,
KNOWN_RECIPIENT_ADDRESS_WARNING, KNOWN_RECIPIENT_ADDRESS_WARNING,

View File

@ -4,8 +4,11 @@ import thunk from 'redux-thunk';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { SEND_STAGES, startNewDraftTransaction } from '../../ducks/send'; import { SEND_STAGES, startNewDraftTransaction } from '../../ducks/send';
import { domainInitialState } from '../../ducks/domains'; import { domainInitialState } from '../../ducks/domains';
import { renderWithProvider } from '../../../test/jest';
import { CHAIN_IDS } from '../../../shared/constants/network'; import { CHAIN_IDS } from '../../../shared/constants/network';
import {
renderWithProvider,
setBackgroundConnection,
} from '../../../test/jest';
import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas'; import { GAS_ESTIMATE_TYPES } from '../../../shared/constants/gas';
import { KEYRING_TYPES } from '../../../shared/constants/keyrings'; import { KEYRING_TYPES } from '../../../shared/constants/keyrings';
import { INITIAL_SEND_STATE_FOR_EXISTING_DRAFT } from '../../../test/jest/mocks'; import { INITIAL_SEND_STATE_FOR_EXISTING_DRAFT } from '../../../test/jest/mocks';
@ -38,6 +41,12 @@ jest.mock('react-router-dom', () => {
}; };
}); });
setBackgroundConnection({
getGasFeeTimeEstimate: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest.fn(),
promisifiedBackground: jest.fn(),
});
jest.mock('ethers', () => { jest.mock('ethers', () => {
const originalModule = jest.requireActual('ethers'); const originalModule = jest.requireActual('ethers');
return { return {
@ -60,6 +69,14 @@ const baseStore = {
}, },
history: { mostRecentOverviewPage: 'activity' }, history: { mostRecentOverviewPage: 'activity' },
metamask: { metamask: {
unapprovedTxs: {
1: {
id: 1,
txParams: {
value: 'oldTxValue',
},
},
},
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: { gasFeeEstimates: {
low: '0', low: '0',
@ -204,6 +221,27 @@ describe('Send Page', () => {
const store = configureMockStore(middleware)({ const store = configureMockStore(middleware)({
...baseStore, ...baseStore,
send: { ...baseStore.send, stage: SEND_STAGES.DRAFT }, send: { ...baseStore.send, stage: SEND_STAGES.DRAFT },
confirmTransaction: {
txData: {
id: 3111025347726181,
time: 1620723786838,
status: 'unapproved',
metamaskNetworkId: '5',
chainId: '0x5',
loadingDefaults: false,
txParams: {
from: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4',
to: '0xaD6D458402F60fD3Bd25163575031ACDce07538D',
value: '0x0',
data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
gas: '0xea60',
gasPrice: '0x4a817c800',
},
type: 'transfer',
origin: 'https://metamask.github.io',
transactionCategory: 'approve',
},
},
}); });
const { getByText } = renderWithProvider(<Send />, store); const { getByText } = renderWithProvider(<Send />, store);
expect(getByText('Send')).toBeTruthy(); expect(getByText('Send')).toBeTruthy();
@ -221,6 +259,27 @@ describe('Send Page', () => {
const store = configureMockStore(middleware)({ const store = configureMockStore(middleware)({
...baseStore, ...baseStore,
send: { ...baseStore.send, stage: SEND_STAGES.DRAFT }, send: { ...baseStore.send, stage: SEND_STAGES.DRAFT },
confirmTransaction: {
txData: {
id: 3111025347726181,
time: 1620723786838,
status: 'unapproved',
metamaskNetworkId: '5',
chainId: '0x5',
loadingDefaults: false,
txParams: {
from: '0x64a845a5b02460acf8a3d84503b0d68d028b4bb4',
to: '0xaD6D458402F60fD3Bd25163575031ACDce07538D',
value: '0x0',
data: '0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
gas: '0xea60',
gasPrice: '0x4a817c800',
},
type: 'transfer',
origin: 'https://metamask.github.io',
transactionCategory: 'approve',
},
},
}); });
const { getByText } = renderWithProvider(<Send />, store); const { getByText } = renderWithProvider(<Send />, store);
expect(getByText('Next')).toBeTruthy(); expect(getByText('Next')).toBeTruthy();