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

Using EIP-1559 V2 for swaps (#12966)

This commit is contained in:
Jyoti Puri 2021-12-12 04:56:28 +05:30 committed by GitHub
parent 2085352de8
commit 19c3d021ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 647 additions and 312 deletions

View File

@ -2899,6 +2899,12 @@
"swapSourceInfo": {
"message": "We search multiple liquidity sources (exchanges, aggregators and professional market makers) to find the best rates and lowest network fees."
},
"swapSuggested": {
"message": "Swap suggested"
},
"swapSuggestedGasSettingToolTipMessage": {
"message": "Swaps are complex and time sensitive transactions. We recommend this gas fee for a good balance between cost and confidence of a successful Swap."
},
"swapSwapFrom": {
"message": "Swap from"
},

View File

@ -258,8 +258,12 @@ export default class ConfirmPageContainer extends Component {
transaction={currentTransaction}
/>
)}
<EditGasFeePopover />
<AdvancedGasFeePopover />
{supportsEIP1559V2 && (
<>
<EditGasFeePopover />
<AdvancedGasFeePopover />
</>
)}
</div>
</GasFeeContextProvider>
);

View File

@ -1,7 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { COLORS } from '../../../helpers/constants/design-system';
import {
EDIT_GAS_MODES,
PRIORITY_LEVELS,
} from '../../../../shared/constants/gas';
import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system';
import { PRIORITY_LEVEL_ICON_MAP } from '../../../helpers/constants/gas';
import { useGasFeeContext } from '../../../contexts/gasFee';
import { useI18nContext } from '../../../hooks/useI18nContext';
@ -12,6 +16,7 @@ import Typography from '../../ui/typography/typography';
export default function EditGasFeeButton({ userAcknowledgedGasMissing }) {
const t = useI18nContext();
const {
editGasMode,
gasLimit,
hasSimulationError,
estimateUsed,
@ -28,13 +33,23 @@ export default function EditGasFeeButton({ userAcknowledgedGasMissing }) {
return null;
}
let icon = estimateUsed;
let title = estimateUsed;
if (
estimateUsed === PRIORITY_LEVELS.HIGH &&
editGasMode === EDIT_GAS_MODES.SWAPS
) {
icon = 'swapSuggested';
title = 'swapSuggested';
}
return (
<div className="edit-gas-fee-button">
<button onClick={() => openModal('editGasFee')}>
<span className="edit-gas-fee-button__icon">
{`${PRIORITY_LEVEL_ICON_MAP[estimateUsed]} `}
{`${PRIORITY_LEVEL_ICON_MAP[icon]} `}
</span>
<span className="edit-gas-fee-button__label">{t(estimateUsed)}</span>
<span className="edit-gas-fee-button__label">{t(title)}</span>
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
</button>
{estimateUsed === 'custom' && (
@ -44,18 +59,18 @@ export default function EditGasFeeButton({ userAcknowledgedGasMissing }) {
<InfoTooltip
contentText={
<div className="edit-gas-fee-button__tooltip">
<Typography fontSize="12px" color={COLORS.GREY}>
<Typography variant={TYPOGRAPHY.H7} color={COLORS.GREY}>
{t('dappSuggestedTooltip', [transaction.origin])}
</Typography>
<Typography fontSize="12px">
<Typography variant={TYPOGRAPHY.H7}>
<b>{t('maxBaseFee')}</b>
{maxFeePerGas}
</Typography>
<Typography fontSize="12px">
<Typography variant={TYPOGRAPHY.H7}>
<b>{t('maxPriorityFee')}</b>
{maxPriorityFeePerGas}
</Typography>
<Typography fontSize="12px">
<Typography variant={TYPOGRAPHY.H7}>
<b>{t('gasLimit')}</b>
{gasLimit}
</Typography>

View File

@ -2,6 +2,7 @@ import React from 'react';
import { screen } from '@testing-library/react';
import {
EDIT_GAS_MODES,
GAS_ESTIMATE_TYPES,
PRIORITY_LEVELS,
} from '../../../../shared/constants/gas';
@ -87,6 +88,17 @@ describe('EditGasFeeButton', () => {
expect(document.getElementsByClassName('info-tooltip')).toHaveLength(1);
});
it('should render edit link with text swap suggested if high gas estimates are selected for swaps', () => {
render({
contextProps: {
transaction: { userFeeLevel: 'high' },
editGasMode: EDIT_GAS_MODES.SWAPS,
},
});
expect(screen.queryByText('🔄')).toBeInTheDocument();
expect(screen.queryByText('Swap suggested')).toBeInTheDocument();
});
it('should render edit link with text advance if custom gas estimates are used', () => {
render({
contextProps: {

View File

@ -1,6 +1,9 @@
import React from 'react';
import { PRIORITY_LEVELS } from '../../../../shared/constants/gas';
import {
EDIT_GAS_MODES,
PRIORITY_LEVELS,
} from '../../../../shared/constants/gas';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { useTransactionModalContext } from '../../../contexts/transaction-modal';
import ErrorMessage from '../../ui/error-message';
@ -15,7 +18,7 @@ import EditGasItem from './edit-gas-item';
import NetworkStatistics from './network-statistics';
const EditGasFeePopover = () => {
const { balanceError } = useGasFeeContext();
const { balanceError, editGasMode } = useGasFeeContext();
const t = useI18nContext();
const { closeModal, currentModal } = useTransactionModalContext();
@ -38,17 +41,23 @@ const EditGasFeePopover = () => {
<I18nValue messageKey="gasOption" />
</span>
<span className="edit-gas-fee-popover__content__header-time">
<I18nValue messageKey="time" />
{editGasMode !== EDIT_GAS_MODES.SWAPS && (
<I18nValue messageKey="time" />
)}
</span>
<span className="edit-gas-fee-popover__content__header-max-fee">
<I18nValue messageKey="maxFee" />
</span>
</div>
<EditGasItem priorityLevel={PRIORITY_LEVELS.LOW} />
{editGasMode !== EDIT_GAS_MODES.SWAPS && (
<EditGasItem priorityLevel={PRIORITY_LEVELS.LOW} />
)}
<EditGasItem priorityLevel={PRIORITY_LEVELS.MEDIUM} />
<EditGasItem priorityLevel={PRIORITY_LEVELS.HIGH} />
<div className="edit-gas-fee-popover__content__separator" />
<EditGasItem priorityLevel={PRIORITY_LEVELS.DAPP_SUGGESTED} />
{editGasMode !== EDIT_GAS_MODES.SWAPS && (
<EditGasItem priorityLevel={PRIORITY_LEVELS.DAPP_SUGGESTED} />
)}
<EditGasItem priorityLevel={PRIORITY_LEVELS.CUSTOM} />
<NetworkStatistics />
<Typography

View File

@ -1,6 +1,7 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import { ETH } from '../../../helpers/constants/common';
import configureStore from '../../../store/store';
@ -45,7 +46,7 @@ const MOCK_FEE_ESTIMATE = {
estimatedBaseFee: '50',
};
const render = (txProps) => {
const render = ({ txProps, contextProps } = {}) => {
const store = configureStore({
metamask: {
nativeCurrency: ETH,
@ -66,6 +67,7 @@ const render = (txProps) => {
return renderWithProvider(
<GasFeeContextProvider
transaction={{ txParams: { gas: '0x5208' }, ...txProps }}
{...contextProps}
>
<EditGasFeePopover />
</GasFeeContextProvider>,
@ -75,7 +77,7 @@ const render = (txProps) => {
describe('EditGasFeePopover', () => {
it('should renders low / medium / high options', () => {
render({ dappSuggestedGasFees: {} });
render({ txProps: { dappSuggestedGasFees: {} } });
expect(screen.queryByText('🐢')).toBeInTheDocument();
expect(screen.queryByText('🦊')).toBeInTheDocument();
@ -103,12 +105,40 @@ describe('EditGasFeePopover', () => {
});
it('should not show insufficient balance message if transaction value is less than balance', () => {
render({ userFeeLevel: 'high', txParams: { value: '0x64' } });
render({ txProps: { userFeeLevel: 'high', txParams: { value: '0x64' } } });
expect(screen.queryByText('Insufficient funds.')).not.toBeInTheDocument();
});
it('should show insufficient balance message if transaction value is more than balance', () => {
render({ userFeeLevel: 'high', txParams: { value: '0x5208' } });
render({
txProps: { userFeeLevel: 'high', txParams: { value: '0x5208' } },
});
expect(screen.queryByText('Insufficient funds.')).toBeInTheDocument();
});
it('should not show low, aggressive and dapp-suggested options for swap', () => {
render({
contextProps: { editGasMode: EDIT_GAS_MODES.SWAPS },
});
expect(screen.queryByText('🐢')).not.toBeInTheDocument();
expect(screen.queryByText('🦊')).toBeInTheDocument();
expect(screen.queryByText('🦍')).not.toBeInTheDocument();
expect(screen.queryByText('🌐')).not.toBeInTheDocument();
expect(screen.queryByText('🔄')).toBeInTheDocument();
expect(screen.queryByText('⚙')).toBeInTheDocument();
expect(screen.queryByText('Low')).not.toBeInTheDocument();
expect(screen.queryByText('Market')).toBeInTheDocument();
expect(screen.queryByText('Aggressive')).not.toBeInTheDocument();
expect(screen.queryByText('Site')).not.toBeInTheDocument();
expect(screen.queryByText('Swap suggested')).toBeInTheDocument();
expect(screen.queryByText('Advanced')).toBeInTheDocument();
});
it('should not show time estimates for swaps', () => {
render({
contextProps: { editGasMode: EDIT_GAS_MODES.SWAPS },
});
expect(screen.queryByText('Time')).not.toBeInTheDocument();
expect(screen.queryByText('Max fee')).toBeInTheDocument();
});
});

View File

@ -4,7 +4,10 @@ import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { getMaximumGasTotalInHexWei } from '../../../../../shared/modules/gas.utils';
import { PRIORITY_LEVELS } from '../../../../../shared/constants/gas';
import {
EDIT_GAS_MODES,
PRIORITY_LEVELS,
} from '../../../../../shared/constants/gas';
import { PRIORITY_LEVEL_ICON_MAP } from '../../../../helpers/constants/gas';
import { PRIMARY } from '../../../../helpers/constants/common';
import {
@ -27,17 +30,19 @@ import { useCustomTimeEstimate } from './useCustomTimeEstimate';
const EditGasItem = ({ priorityLevel }) => {
const {
editGasMode,
estimateUsed,
gasFeeEstimates,
gasLimit,
maxFeePerGas: maxFeePerGasValue,
maxPriorityFeePerGas: maxPriorityFeePerGasValue,
updateTransactionUsingGasFeeEstimates,
transaction: { dappSuggestedGasFees },
transaction,
} = useGasFeeContext();
const t = useI18nContext();
const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues);
const { closeModal, openModal } = useTransactionModalContext();
const { dappSuggestedGasFees } = transaction;
let maxFeePerGas;
let maxPriorityFeePerGas;
@ -45,6 +50,8 @@ const EditGasItem = ({ priorityLevel }) => {
if (gasFeeEstimates?.[priorityLevel]) {
maxFeePerGas = gasFeeEstimates[priorityLevel].suggestedMaxFeePerGas;
maxPriorityFeePerGas =
gasFeeEstimates[priorityLevel].suggestedMaxPriorityFeePerGas;
} else if (
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED &&
dappSuggestedGasFees
@ -105,6 +112,18 @@ const EditGasItem = ({ priorityLevel }) => {
return null;
}
let icon = priorityLevel;
let title = priorityLevel;
if (priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED) {
title = 'dappSuggestedShortLabel';
} else if (
priorityLevel === PRIORITY_LEVELS.HIGH &&
editGasMode === EDIT_GAS_MODES.SWAPS
) {
icon = 'swapSuggested';
title = 'swapSuggested';
}
return (
<button
className={classNames('edit-gas-item', {
@ -118,22 +137,15 @@ const EditGasItem = ({ priorityLevel }) => {
<span
className={`edit-gas-item__icon edit-gas-item__icon-${priorityLevel}`}
>
{PRIORITY_LEVEL_ICON_MAP[priorityLevel]}
{PRIORITY_LEVEL_ICON_MAP[icon]}
</span>
<I18nValue
messageKey={
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED
? 'dappSuggestedShortLabel'
: priorityLevel
}
/>
<I18nValue messageKey={title} />
</span>
<span
className={`edit-gas-item__time-estimate edit-gas-item__time-estimate-${priorityLevel}`}
>
{minWaitTime
? minWaitTime && toHumanReadableTime(t, minWaitTime)
: '--'}
{editGasMode !== EDIT_GAS_MODES.SWAPS &&
(minWaitTime ? toHumanReadableTime(t, minWaitTime) : '--')}
</span>
<span
className={`edit-gas-item__fee-estimate edit-gas-item__fee-estimate-${priorityLevel}`}
@ -159,6 +171,9 @@ const EditGasItem = ({ priorityLevel }) => {
priorityLevel={priorityLevel}
maxFeePerGas={maxFeePerGas}
maxPriorityFeePerGas={maxPriorityFeePerGas}
editGasMode={editGasMode}
gasLimit={gasLimit}
transaction={transaction}
/>
}
position="top"

View File

@ -1,6 +1,7 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { EDIT_GAS_MODES } from '../../../../../shared/constants/gas';
import { renderWithProvider } from '../../../../../test/lib/render-helpers';
import { ETH } from '../../../../helpers/constants/common';
import configureStore from '../../../../store/store';
@ -46,7 +47,11 @@ const DAPP_SUGGESTED_ESTIMATE = {
maxPriorityFeePerGas: '0x59682f00',
};
const renderComponent = (componentProps, transactionProps) => {
const renderComponent = ({
componentProps,
transactionProps,
contextProps,
} = {}) => {
const store = configureStore({
metamask: {
nativeCurrency: ETH,
@ -71,6 +76,7 @@ const renderComponent = (componentProps, transactionProps) => {
return renderWithProvider(
<GasFeeContextProvider
transaction={{ txParams: { gas: '0x5208' }, ...transactionProps }}
{...contextProps}
>
<EditGasItem priorityLevel="low" {...componentProps} />
</GasFeeContextProvider>,
@ -80,7 +86,7 @@ const renderComponent = (componentProps, transactionProps) => {
describe('EditGasItem', () => {
it('should renders low gas estimate option for priorityLevel low', () => {
renderComponent({ priorityLevel: 'low' });
renderComponent({ componentProps: { priorityLevel: 'low' } });
expect(screen.queryByRole('button', { name: 'low' })).toBeInTheDocument();
expect(screen.queryByText('🐢')).toBeInTheDocument();
expect(screen.queryByText('Low')).toBeInTheDocument();
@ -89,7 +95,7 @@ describe('EditGasItem', () => {
});
it('should renders market gas estimate option for priorityLevel medium', () => {
renderComponent({ priorityLevel: 'medium' });
renderComponent({ componentProps: { priorityLevel: 'medium' } });
expect(
screen.queryByRole('button', { name: 'medium' }),
).toBeInTheDocument();
@ -100,7 +106,7 @@ describe('EditGasItem', () => {
});
it('should renders aggressive gas estimate option for priorityLevel high', () => {
renderComponent({ priorityLevel: 'high' });
renderComponent({ componentProps: { priorityLevel: 'high' } });
expect(screen.queryByRole('button', { name: 'high' })).toBeInTheDocument();
expect(screen.queryByText('🦍')).toBeInTheDocument();
expect(screen.queryByText('Aggressive')).toBeInTheDocument();
@ -108,21 +114,33 @@ describe('EditGasItem', () => {
expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument();
});
it('should render priorityLevel high as "Swap suggested" for swaps', () => {
renderComponent({
componentProps: { priorityLevel: 'high' },
contextProps: { editGasMode: EDIT_GAS_MODES.SWAPS },
});
expect(screen.queryByRole('button', { name: 'high' })).toBeInTheDocument();
expect(screen.queryByText('🔄')).toBeInTheDocument();
expect(screen.queryByText('Swap suggested')).toBeInTheDocument();
expect(screen.queryByText('15 sec')).not.toBeInTheDocument();
expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument();
});
it('should highlight option is priorityLevel is currently selected', () => {
renderComponent({ priorityLevel: 'high' }, { userFeeLevel: 'high' });
renderComponent({
componentProps: { priorityLevel: 'high' },
transactionProps: { userFeeLevel: 'high' },
});
expect(
document.getElementsByClassName('edit-gas-item--selected'),
).toHaveLength(1);
});
it('should renders site gas estimate option for priorityLevel dappSuggested', () => {
renderComponent(
{ priorityLevel: 'dappSuggested' },
{
dappSuggestedGasFees: DAPP_SUGGESTED_ESTIMATE,
txParams: { gas: '0x5208', ...DAPP_SUGGESTED_ESTIMATE },
},
);
renderComponent({
componentProps: { priorityLevel: 'dappSuggested' },
transactionProps: { dappSuggestedGasFees: DAPP_SUGGESTED_ESTIMATE },
});
expect(
screen.queryByRole('button', { name: 'dappSuggested' }),
).toBeInTheDocument();
@ -132,7 +150,10 @@ describe('EditGasItem', () => {
});
it('should renders advance gas estimate option for priorityLevel custom', () => {
renderComponent({ priorityLevel: 'custom' }, { userFeeLevel: 'high' });
renderComponent({
componentProps: { priorityLevel: 'custom' },
transactionProps: { userFeeLevel: 'high' },
});
expect(
screen.queryByRole('button', { name: 'custom' }),
).toBeInTheDocument();

View File

@ -25,6 +25,7 @@
color: $ui-black;
font-size: 12px;
font-weight: bold;
white-space: nowrap;
width: 36%;
}

View File

@ -1,30 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import { PRIORITY_LEVELS } from '../../../../../shared/constants/gas';
import {
EDIT_GAS_MODES,
PRIORITY_LEVELS,
} from '../../../../../shared/constants/gas';
import {
COLORS,
FONT_WEIGHT,
TYPOGRAPHY,
} from '../../../../helpers/constants/design-system';
import Typography from '../../../ui/typography';
import { useGasFeeContext } from '../../../../contexts/gasFee';
const EditGasToolTip = ({
gasLimit,
priorityLevel,
// maxFeePerGas & maxPriorityFeePerGas are derived from conditional logic
// related to the source of the estimates. We pass these values from the
// the parent component (edit-gas-item) rather than recalculate them
maxFeePerGas,
maxPriorityFeePerGas,
editGasMode,
transaction,
t,
}) => {
const {
gasLimit,
maxFeePerGas: maxFeePerGasValue,
maxPriorityFeePerGas: maxPriorityFeePerGasValue,
transaction,
} = useGasFeeContext();
const toolTipMessage = () => {
switch (priorityLevel) {
case PRIORITY_LEVELS.LOW:
@ -40,6 +38,9 @@ const EditGasToolTip = ({
</span>,
]);
case PRIORITY_LEVELS.HIGH:
if (editGasMode === EDIT_GAS_MODES.SWAPS) {
return t('swapSuggestedGasSettingToolTipMessage');
}
return t('highGasSettingToolTipMessage', [
<span key={priorityLevel}>
<b>{t('high')}</b>
@ -64,10 +65,15 @@ const EditGasToolTip = ({
return (
<div className="edit-gas-tooltip__container">
{priorityLevel !== PRIORITY_LEVELS.CUSTOM &&
priorityLevel !== PRIORITY_LEVELS.DAPP_SUGGESTED ? (
priorityLevel !== PRIORITY_LEVELS.DAPP_SUGGESTED &&
!(
priorityLevel === PRIORITY_LEVELS.HIGH &&
editGasMode === EDIT_GAS_MODES.SWAPS
) ? (
<img alt="" src={`./images/curve-${priorityLevel}.svg`} />
) : null}
{priorityLevel === PRIORITY_LEVELS.HIGH ? (
{priorityLevel === PRIORITY_LEVELS.HIGH &&
editGasMode !== EDIT_GAS_MODES.SWAPS ? (
<div className="edit-gas-tooltip__container__dialog">
<Typography variant={TYPOGRAPHY.H7} color={COLORS.WHITE}>
{t('highGasSettingToolTipDialog')}
@ -92,7 +98,7 @@ const EditGasToolTip = ({
color={COLORS.NEUTRAL_GREY}
className="edit-gas-tooltip__container__value"
>
{maxFeePerGas ?? maxFeePerGasValue}
{maxFeePerGas}
</Typography>
</div>
<div>
@ -108,7 +114,7 @@ const EditGasToolTip = ({
color={COLORS.NEUTRAL_GREY}
className="edit-gas-tooltip__container__value"
>
{maxPriorityFeePerGas ?? maxPriorityFeePerGasValue}
{maxPriorityFeePerGas}
</Typography>
</div>
<div>
@ -138,6 +144,9 @@ EditGasToolTip.propTypes = {
maxFeePerGas: PropTypes.string,
maxPriorityFeePerGas: PropTypes.string,
t: PropTypes.func,
editGasMode: PropTypes.string,
gasLimit: PropTypes.number,
transaction: PropTypes.object,
};
export default EditGasToolTip;

View File

@ -30,7 +30,7 @@ const HIGH_GAS_OPTION = {
maxPriorityFeePerGas: '2',
};
const renderComponent = (props, transactionProps, gasFeeContextProps) => {
const renderComponent = (componentProps) => {
const mockStore = {
metamask: {
provider: {},
@ -43,21 +43,14 @@ const renderComponent = (props, transactionProps, gasFeeContextProps) => {
},
selectedAddress: '0xAddress',
featureFlags: { advancedInlineGas: true },
advancedGasFee: {
maxBaseFee: '1.5',
priorityFee: '2',
},
},
};
const store = configureStore(mockStore);
return renderWithProvider(
<GasFeeContextProvider
transaction={{ txParams: { gas: '0x5208' }, ...transactionProps }}
{...gasFeeContextProps}
>
<EditGasToolTip {...props} t={jest.fn()} />
<GasFeeContextProvider transaction={{ txParams: { gas: '0x5208' } }}>
<EditGasToolTip {...componentProps} t={jest.fn()} gasLimit={21000} />
</GasFeeContextProvider>,
store,
);

View File

@ -11,7 +11,7 @@ import TransactionDetailItem from '../transaction-detail-item/transaction-detail
export default function TransactionDetail({
rows = [],
onEdit,
userAcknowledgedGasMissing,
userAcknowledgedGasMissing = false,
}) {
const t = useI18nContext();
const { supportsEIP1559V2 } = useGasFeeContext();
@ -44,9 +44,5 @@ TransactionDetail.propTypes = {
* onClick handler for the Edit link
*/
onEdit: PropTypes.func,
/**
* If there is a error in getting correct estimates we show a message to the user
* which they can acknowledge and proceed with their transaction
*/
userAcknowledgedGasMissing: PropTypes.bool.isRequired,
userAcknowledgedGasMissing: PropTypes.bool,
};

View File

@ -37,5 +37,6 @@ export const PRIORITY_LEVEL_ICON_MAP = {
medium: '🦊',
high: '🦍',
dappSuggested: '🌐',
swapSuggested: '🔄',
custom: '⚙',
};

View File

@ -20,6 +20,10 @@ import {
import { ETH } from '../../helpers/constants/common';
import { useGasFeeEstimates } from '../useGasFeeEstimates';
import {
getCustomMaxFeePerGas,
getCustomMaxPriorityFeePerGas,
} from '../../ducks/swaps/swaps';
// Why this number?
// 20 gwei * 21000 gasLimit = 420,000 gwei
@ -122,6 +126,12 @@ export const generateUseSelectorRouter = ({
balance: '0x440aa47cc2556',
};
}
if (selector === getCustomMaxFeePerGas) {
return '0x5208';
}
if (selector === getCustomMaxPriorityFeePerGas) {
return '0x5208';
}
if (selector === checkNetworkAndAccountSupports1559) {
return checkNetworkAndAccountSupports1559Response;
}

View File

@ -170,12 +170,12 @@ export function useGasFeeInputs(
maxFeePerGasFiat,
setMaxFeePerGas,
} = useMaxFeePerGasInput({
supportsEIP1559V2,
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
gasPrice,
supportsEIP1559V2,
transaction,
});
@ -184,11 +184,11 @@ export function useGasFeeInputs(
maxPriorityFeePerGasFiat,
setMaxPriorityFeePerGas,
} = useMaxPriorityFeePerGasInput({
supportsEIP1559V2,
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
supportsEIP1559V2,
transaction,
});
@ -248,6 +248,7 @@ export function useGasFeeInputs(
updateTransactionUsingGasFeeEstimates,
} = useTransactionFunctions({
defaultEstimateToUse,
editGasMode,
gasFeeEstimates,
gasLimit,
transaction,
@ -313,6 +314,7 @@ export function useGasFeeInputs(
setGasPrice,
gasLimit,
setGasLimit,
editGasMode,
estimateToUse,
setEstimateToUse,
estimatedMinimumFiat,

View File

@ -4,6 +4,7 @@ import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transactio
import {
GAS_RECOMMENDATIONS,
CUSTOM_GAS_ESTIMATE,
EDIT_GAS_MODES,
} from '../../../shared/constants/gas';
import { ETH, PRIMARY } from '../../helpers/constants/common';
@ -350,4 +351,13 @@ describe('useGasFeeInputs', () => {
expect(result.current.supportsEIP1559V2).toBe(false);
});
});
describe('editGasMode', () => {
it('should return editGasMode passed', () => {
const { result } = renderHook(() =>
useGasFeeInputs(undefined, undefined, undefined, EDIT_GAS_MODES.SWAPS),
);
expect(result.current.editGasMode).toBe(EDIT_GAS_MODES.SWAPS);
});
});
});

View File

@ -34,12 +34,12 @@ const getMaxFeePerGasFromTransaction = (transaction) => {
* method to update the setMaxFeePerGas.
*/
export function useMaxFeePerGasInput({
supportsEIP1559V2,
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
gasPrice,
supportsEIP1559V2,
transaction,
}) {
const supportsEIP1559 =
@ -53,7 +53,7 @@ export function useMaxFeePerGasInput({
const showFiat = useSelector(getShouldShowFiat);
const maxFeePerGasFromTransaction = supportsEIP1559
const initialMaxFeePerGas = supportsEIP1559
? getMaxFeePerGasFromTransaction(transaction)
: 0;
@ -61,16 +61,16 @@ export function useMaxFeePerGasInput({
// transitional because it is only used to modify a transaction in the
// metamask (background) state tree.
const [maxFeePerGas, setMaxFeePerGas] = useState(() => {
if (maxFeePerGasFromTransaction && feeParamsAreCustom(transaction))
return maxFeePerGasFromTransaction;
if (initialMaxFeePerGas && feeParamsAreCustom(transaction))
return initialMaxFeePerGas;
return null;
});
useEffect(() => {
if (supportsEIP1559V2) {
setMaxFeePerGas(maxFeePerGasFromTransaction);
if (supportsEIP1559V2 && initialMaxFeePerGas) {
setMaxFeePerGas(initialMaxFeePerGas);
}
}, [maxFeePerGasFromTransaction, setMaxFeePerGas, supportsEIP1559V2]);
}, [initialMaxFeePerGas, setMaxFeePerGas, supportsEIP1559V2]);
let gasSettings = {
gasLimit: decimalToHex(gasLimit),
@ -117,7 +117,7 @@ export function useMaxFeePerGasInput({
gasFeeEstimates,
gasEstimateType,
estimateToUse,
maxFeePerGasFromTransaction,
initialMaxFeePerGas || 0,
);
return {

View File

@ -9,8 +9,8 @@ import {
checkNetworkAndAccountSupports1559,
getShouldShowFiat,
} from '../../selectors';
import { multiplyCurrencies } from '../../../shared/modules/conversion.utils';
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
import { multiplyCurrencies } from '../../../shared/modules/conversion.utils';
import { useCurrencyDisplay } from '../useCurrencyDisplay';
import { useUserPreferencedCurrency } from '../useUserPreferencedCurrency';
@ -34,11 +34,11 @@ const getMaxPriorityFeePerGasFromTransaction = (transaction) => {
* method to update the maxPriorityFeePerGas.
*/
export function useMaxPriorityFeePerGasInput({
supportsEIP1559V2,
estimateToUse,
gasEstimateType,
gasFeeEstimates,
gasLimit,
supportsEIP1559V2,
transaction,
}) {
const supportsEIP1559 =
@ -52,25 +52,21 @@ export function useMaxPriorityFeePerGasInput({
const showFiat = useSelector(getShouldShowFiat);
const maxPriorityFeePerGasFromTransaction = supportsEIP1559
const initialMaxPriorityFeePerGas = supportsEIP1559
? getMaxPriorityFeePerGasFromTransaction(transaction)
: 0;
const [maxPriorityFeePerGas, setMaxPriorityFeePerGas] = useState(() => {
if (maxPriorityFeePerGasFromTransaction && feeParamsAreCustom(transaction))
return maxPriorityFeePerGasFromTransaction;
if (initialMaxPriorityFeePerGas && feeParamsAreCustom(transaction))
return initialMaxPriorityFeePerGas;
return null;
});
useEffect(() => {
if (supportsEIP1559V2) {
setMaxPriorityFeePerGas(maxPriorityFeePerGasFromTransaction);
if (supportsEIP1559V2 && initialMaxPriorityFeePerGas) {
setMaxPriorityFeePerGas(initialMaxPriorityFeePerGas);
}
}, [
maxPriorityFeePerGasFromTransaction,
setMaxPriorityFeePerGas,
supportsEIP1559V2,
]);
}, [initialMaxPriorityFeePerGas, setMaxPriorityFeePerGas, supportsEIP1559V2]);
const maxPriorityFeePerGasToUse =
maxPriorityFeePerGas ??
@ -79,7 +75,7 @@ export function useMaxPriorityFeePerGasInput({
gasFeeEstimates,
gasEstimateType,
estimateToUse,
maxPriorityFeePerGasFromTransaction,
initialMaxPriorityFeePerGas || 0,
);
// We need to display the estimated fiat currency impact of the

View File

@ -1,15 +1,20 @@
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { PRIORITY_LEVELS } from '../../../shared/constants/gas';
import { EDIT_GAS_MODES, PRIORITY_LEVELS } from '../../../shared/constants/gas';
import {
decimalToHex,
decGWEIToHexWEI,
} from '../../helpers/utils/conversions.util';
import { updateTransaction as updateTransactionFn } from '../../store/actions';
import {
updateCustomSwapsEIP1559GasParams,
updateSwapsUserFeeLevel,
updateTransaction as updateTransactionFn,
} from '../../store/actions';
export const useTransactionFunctions = ({
defaultEstimateToUse,
editGasMode,
gasFeeEstimates,
gasLimit: gasLimitInTransaction,
transaction,
@ -38,16 +43,29 @@ export const useTransactionFunctions = ({
const updatedTxMeta = {
...transaction,
userFeeLevel: estimateUsed || 'custom',
userFeeLevel: estimateUsed || PRIORITY_LEVELS.CUSTOM,
txParams: {
...transaction.txParams,
...newGasSettings,
},
};
dispatch(updateTransactionFn(updatedTxMeta));
if (editGasMode === EDIT_GAS_MODES.SWAPS) {
dispatch(
updateSwapsUserFeeLevel(estimateUsed || PRIORITY_LEVELS.CUSTOM),
);
dispatch(updateCustomSwapsEIP1559GasParams(newGasSettings));
} else {
dispatch(updateTransactionFn(updatedTxMeta));
}
},
[defaultEstimateToUse, dispatch, gasLimitInTransaction, transaction],
[
defaultEstimateToUse,
dispatch,
editGasMode,
gasLimitInTransaction,
transaction,
],
);
const updateTransactionUsingGasFeeEstimates = useCallback(

View File

@ -269,8 +269,12 @@ export default function ConfirmApprove() {
transaction={transaction}
/>
)}
<EditGasFeePopover />
<AdvancedGasFeePopover />
{supportsEIP1559V2 && (
<>
<EditGasFeePopover />
<AdvancedGasFeePopover />
</>
)}
</TransactionModalContextProvider>
}
hideSenderToRecipient

View File

@ -439,7 +439,6 @@ export default class ConfirmTransactionBase extends Component {
key="gas_details"
hexMaximumTransactionFee={hexMaximumTransactionFee}
hexMinimumTransactionFee={hexMinimumTransactionFee}
isMainnet={isMainnet}
maxFeePerGas={maxFeePerGas}
maxPriorityFeePerGas={maxPriorityFeePerGas}
supportsEIP1559={supportsEIP1559}

View File

@ -0,0 +1,52 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { TYPOGRAPHY } from '../../../../helpers/constants/design-system';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { getIsMainnet } from '../../../../selectors';
import Box from '../../../../components/ui/box';
import I18nValue from '../../../../components/ui/i18n-value';
import InfoTooltip from '../../../../components/ui/info-tooltip/info-tooltip';
import Typography from '../../../../components/ui/typography/typography';
const GasDetailsItemTitle = () => {
const t = useI18nContext();
const isMainnet = useSelector(getIsMainnet);
return (
<Box display="flex">
<Box marginRight={1}>
<I18nValue messageKey="transactionDetailGasHeadingV2" />
</Box>
<span className="gas-details-item-title__estimate">
(<I18nValue messageKey="transactionDetailGasInfoV2" />)
</span>
<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="bottom"
/>
</Box>
);
};
export default GasDetailsItemTitle;

View File

@ -0,0 +1,9 @@
.gas-details-item-title {
&__estimate {
font-weight: 400;
font-style: italic;
font-size: 12px;
color: $Grey-500;
line-height: inherit;
}
}

View File

@ -0,0 +1,51 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { MAINNET_CHAIN_ID } from '../../../../../shared/constants/network';
import { GasFeeContextProvider } from '../../../../contexts/gasFee';
import { renderWithProvider } from '../../../../../test/jest';
import configureStore from '../../../../store/store';
import GasDetailsItemTitle from './gas-details-item-title';
jest.mock('../../../../store/actions', () => ({
disconnectGasFeeEstimatePoller: jest.fn(),
getGasFeeEstimatesAndStartPolling: jest
.fn()
.mockImplementation(() => Promise.resolve()),
addPollingTokenToAppState: jest.fn(),
getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()),
}));
const render = () => {
const store = configureStore({
metamask: {
provider: { chainId: MAINNET_CHAIN_ID },
cachedBalances: {},
accounts: {
'0xAddress': {
address: '0xAddress',
balance: '0x176e5b6f173ebe66',
},
},
selectedAddress: '0xAddress',
},
});
return renderWithProvider(
<GasFeeContextProvider transaction={{ txParams: {} }}>
<GasDetailsItemTitle userAcknowledgedGasMissing={false} />
</GasFeeContextProvider>,
store,
);
};
describe('GasDetailsItem', () => {
it('should render label', async () => {
render();
await waitFor(() => {
expect(screen.queryByText('Gas')).toBeInTheDocument();
expect(screen.queryByText('(estimated)')).toBeInTheDocument();
});
});
});

View File

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

View File

@ -2,31 +2,27 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system';
import { COLORS } from '../../../helpers/constants/design-system';
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common';
import { hexWEIToDecGWEI } from '../../../helpers/utils/conversions.util';
import { useI18nContext } from '../../../hooks/useI18nContext';
import Box from '../../../components/ui/box';
import Typography from '../../../components/ui/typography/typography';
import GasTiming from '../../../components/app/gas-timing/gas-timing.component';
import I18nValue from '../../../components/ui/i18n-value';
import InfoTooltip from '../../../components/ui/info-tooltip/info-tooltip';
import LoadingHeartBeat from '../../../components/ui/loading-heartbeat';
import TransactionDetailItem from '../../../components/app/transaction-detail-item/transaction-detail-item.component';
import UserPreferencedCurrencyDisplay from '../../../components/app/user-preferenced-currency-display';
import { useGasFeeContext } from '../../../contexts/gasFee';
import GasDetailsItemTitle from './gas-details-item-title';
const GasDetailsItem = ({
hexMaximumTransactionFee,
hexMinimumTransactionFee,
isMainnet,
maxFeePerGas,
maxPriorityFeePerGas,
userAcknowledgedGasMissing,
useNativeCurrencyAsPrimaryCurrency,
}) => {
const t = useI18nContext();
const { estimateUsed, hasSimulationError, transaction } = useGasFeeContext();
if (hasSimulationError && !userAcknowledgedGasMissing) return null;
@ -34,40 +30,7 @@ const GasDetailsItem = ({
return (
<TransactionDetailItem
key="gas-item"
detailTitle={
<Box display="flex">
<Box marginRight={1}>
<I18nValue messageKey="transactionDetailGasHeadingV2" />
</Box>
<span className="gas-details-item__estimate">
(<I18nValue messageKey="transactionDetailGasInfoV2" />)
</span>
<InfoTooltip
contentText={
<>
<Typography tag={TYPOGRAPHY.Paragraph} variant={TYPOGRAPHY.H7}>
{t('transactionDetailGasTooltipIntro', [
isMainnet ? t('networkNameEthereum') : '',
])}
</Typography>
<Typography tag={TYPOGRAPHY.Paragraph} variant={TYPOGRAPHY.H7}>
{t('transactionDetailGasTooltipExplanation')}
</Typography>
<Typography tag={TYPOGRAPHY.Paragraph} 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="bottom"
/>
</Box>
}
detailTitle={<GasDetailsItemTitle />}
detailTitleColor={COLORS.BLACK}
detailText={
<div className="gas-details-item__currency-container">
@ -137,7 +100,6 @@ const GasDetailsItem = ({
GasDetailsItem.propTypes = {
hexMaximumTransactionFee: PropTypes.string,
hexMinimumTransactionFee: PropTypes.string,
isMainnet: PropTypes.bool,
maxFeePerGas: PropTypes.string,
maxPriorityFeePerGas: PropTypes.string,
userAcknowledgedGasMissing: PropTypes.bool.isRequired,

View File

@ -1,12 +1,4 @@
.gas-details-item {
&__estimate {
font-weight: 400;
font-style: italic;
font-size: 12px;
color: $Grey-500;
line-height: inherit;
}
&__gas-fee-warning {
color: $secondary-1;
}

View File

@ -6,6 +6,7 @@
@import 'confirm-decrypt-message/confirm-decrypt-message';
@import 'confirm-encryption-public-key/confirm-encryption-public-key';
@import 'confirm-transaction-base/gas-details-item/gas-details-item';
@import 'confirm-transaction-base/gas-details-item/gas-details-item-title/gas-details-item-title';
@import 'confirm-transaction-base/transaction-alerts/transaction-alerts';
@import 'confirmation/confirmation';
@import 'connected-sites/index';

View File

@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FeeCard renders the component with EIP-1559 V2 enabled 1`] = `null`;
exports[`FeeCard renders the component with EIP-1559 enabled 1`] = `null`;
exports[`FeeCard renders the component with initial props 1`] = `null`;

View File

@ -18,6 +18,7 @@ import {
TYPOGRAPHY,
FONT_WEIGHT,
} from '../../../helpers/constants/design-system';
import GasDetailsItemTitle from '../../confirm-transaction-base/gas-details-item/gas-details-item-title';
const GAS_FEES_LEARN_MORE_URL =
'https://community.metamask.io/t/what-is-gas-why-do-transactions-take-so-long/3172';
@ -34,6 +35,7 @@ export default function FeeCard({
onQuotesClick,
chainId,
isBestQuote,
supportsEIP1559V2,
}) {
const t = useContext(I18nContext);
@ -73,42 +75,46 @@ export default function FeeCard({
<TransactionDetailItem
key="gas-item"
detailTitle={
<>
{t('transactionDetailGasHeading')}
<InfoTooltip
position="top"
contentText={
<>
<p className="fee-card__info-tooltip-paragraph">
{t('swapGasFeesSummary', [
getTranslatedNetworkName(),
])}
</p>
<p className="fee-card__info-tooltip-paragraph">
{t('swapGasFeesDetails')}
</p>
<p className="fee-card__info-tooltip-paragraph">
<a
className="fee-card__link"
onClick={() => {
gasFeesLearnMoreLinkClickedEvent();
global.platform.openTab({
url: GAS_FEES_LEARN_MORE_URL,
});
}}
target="_blank"
rel="noopener noreferrer"
>
{t('swapGasFeesLearnMore')}
</a>
</p>
</>
}
containerClassName="fee-card__info-tooltip-content-container"
wrapperClassName="fee-card__row-label fee-card__info-tooltip-container"
wide
/>
</>
supportsEIP1559V2 ? (
<GasDetailsItemTitle />
) : (
<>
{t('transactionDetailGasHeading')}
<InfoTooltip
position="top"
contentText={
<>
<p className="fee-card__info-tooltip-paragraph">
{t('swapGasFeesSummary', [
getTranslatedNetworkName(),
])}
</p>
<p className="fee-card__info-tooltip-paragraph">
{t('swapGasFeesDetails')}
</p>
<p className="fee-card__info-tooltip-paragraph">
<a
className="fee-card__link"
onClick={() => {
gasFeesLearnMoreLinkClickedEvent();
global.platform.openTab({
url: GAS_FEES_LEARN_MORE_URL,
});
}}
target="_blank"
rel="noopener noreferrer"
>
{t('swapGasFeesLearnMore')}
</a>
</p>
</>
}
containerClassName="fee-card__info-tooltip-content-container"
wrapperClassName="fee-card__row-label fee-card__info-tooltip-container"
wide
/>
</>
)
}
detailText={primaryFee.fee}
detailTotal={secondaryFee.fee}
@ -124,12 +130,14 @@ export default function FeeCard({
{t('maxFee')}
</Typography>
{`: ${secondaryFee.maxFee}`}
<span
className="fee-card__edit-link"
onClick={() => onFeeCardMaxRowClick()}
>
{t('edit')}
</span>
{!supportsEIP1559V2 && (
<span
className="fee-card__edit-link"
onClick={() => onFeeCardMaxRowClick()}
>
{t('edit')}
</span>
)}
</>
)
}
@ -203,4 +211,5 @@ FeeCard.propTypes = {
numberOfQuotes: PropTypes.number.isRequired,
chainId: PropTypes.string.isRequired,
isBestQuote: PropTypes.bool.isRequired,
supportsEIP1559V2: PropTypes.bool.isRequired,
};

View File

@ -3,25 +3,37 @@ import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { useSelector } from 'react-redux';
import { checkNetworkAndAccountSupports1559 } from '../../../selectors';
import {
getGasEstimateType,
getGasFeeEstimates,
getIsGasEstimatesLoading,
} from '../../../ducks/metamask/metamask';
import {
renderWithProvider,
createSwapsMockStore,
setBackgroundConnection,
MOCKS,
} from '../../../../test/jest';
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network';
import {
checkNetworkAndAccountSupports1559,
getPreferences,
getSelectedAccount,
} from '../../../selectors';
import {
getGasEstimateType,
getGasFeeEstimates,
getIsGasEstimatesLoading,
} from '../../../ducks/metamask/metamask';
import { GasFeeContextProvider } from '../../../contexts/gasFee';
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../helpers/constants/transactions';
import { useGasFeeEstimates } from '../../../hooks/useGasFeeEstimates';
import FeeCard from '.';
const middleware = [thunk];
jest.mock('../../../hooks/useGasFeeEstimates', () => ({
useGasFeeEstimates: jest.fn(),
}));
jest.mock('react-redux', () => {
const actual = jest.requireActual('react-redux');
@ -78,6 +90,7 @@ const createProps = (customProps = {}) => {
tokenConversionRate: 0.015,
chainId: MAINNET_CHAIN_ID,
networkAndAccountSupports1559: false,
supportsEIP1559V2: false,
...customProps,
};
};
@ -118,4 +131,55 @@ describe('FeeCard', () => {
document.querySelector('.fee-card__top-bordered-row'),
).toMatchSnapshot();
});
it('renders the component with EIP-1559 V2 enabled', () => {
process.env.EIP_1559_V2 = true;
useGasFeeEstimates.mockImplementation(() => ({ gasFeeEstimates: {} }));
useSelector.mockImplementation((selector) => {
if (selector === getPreferences) {
return {
useNativeCurrencyAsPrimaryCurrency: true,
};
}
if (selector === getSelectedAccount) {
return {
balance: '0x440aa47cc2556',
};
}
if (selector === checkNetworkAndAccountSupports1559) {
return true;
}
return undefined;
});
const store = configureMockStore(middleware)(createSwapsMockStore());
const props = createProps({
networkAndAccountSupports1559: true,
maxPriorityFeePerGasDecGWEI: '3',
maxFeePerGasDecGWEI: '4',
supportsEIP1559V2: true,
});
const { getByText } = renderWithProvider(
<GasFeeContextProvider
transaction={{ txParams: {}, userFeeLevel: 'high' }}
editGasMode={EDIT_GAS_MODES.SWAPS}
>
<FeeCard {...props} />
</GasFeeContextProvider>,
store,
);
expect(getByText('Best of 6 quotes.')).toBeInTheDocument();
expect(getByText('Gas')).toBeInTheDocument();
expect(getByText('(estimated)')).toBeInTheDocument();
expect(getByText('Swap suggested')).toBeInTheDocument();
expect(getByText('Max fee')).toBeInTheDocument();
expect(getByText(props.primaryFee.fee)).toBeInTheDocument();
expect(getByText(props.secondaryFee.fee)).toBeInTheDocument();
expect(getByText(`: ${props.secondaryFee.maxFee}`)).toBeInTheDocument();
expect(getByText('Includes a 0.875% MetaMask fee.')).toBeInTheDocument();
expect(
document.querySelector('.fee-card__top-bordered-row'),
).toMatchSnapshot();
process.env.EIP_1559_V2 = false;
});
});

View File

@ -81,6 +81,10 @@ import {
decGWEIToHexWEI,
addHexes,
} from '../../../helpers/utils/conversions.util';
import { GasFeeContextProvider } from '../../../contexts/gasFee';
import { TransactionModalContextProvider } from '../../../contexts/transaction-modal';
import AdvancedGasFeePopover from '../../../components/app/advanced-gas-fee-popover';
import EditGasFeePopover from '../../../components/app/edit-gas-fee-popover';
import MainQuoteSummary from '../main-quote-summary';
import { calcGasTotal } from '../../send/send.utils';
import { getCustomTxParamsData } from '../../confirm-approve/confirm-approve.util';
@ -99,6 +103,10 @@ import CountdownTimer from '../countdown-timer';
import SwapsFooter from '../swaps-footer';
import ViewQuotePriceDifference from './view-quote-price-difference';
// eslint-disable-next-line prefer-destructuring
const EIP_1559_V2_ENABLED =
process.env.EIP_1559_V2 === true || process.env.EIP_1559_V2 === 'true';
export default function ViewQuote() {
const history = useHistory();
const dispatch = useDispatch();
@ -672,93 +680,152 @@ export default function ViewQuote() {
}
}, [dispatch, viewQuotePageLoadedEvent, reviewSwapClickedTimestamp]);
const transaction = {
userFeeLevel: swapsUserFeeLevel || GAS_RECOMMENDATIONS.HIGH,
txParams: {
maxFeePerGas,
maxPriorityFeePerGas,
gas: maxGasLimit,
},
};
const supportsEIP1559V2 =
EIP_1559_V2_ENABLED && networkAndAccountSupports1559;
return (
<div className="view-quote">
<div
className={classnames('view-quote__content', {
'view-quote__content_modal': disableSubmissionDueToPriceWarning,
})}
>
{selectQuotePopoverShown && (
<SelectQuotePopover
quoteDataRows={renderablePopoverData}
onClose={() => setSelectQuotePopoverShown(false)}
onSubmit={(aggId) => dispatch(swapsQuoteSelected(aggId))}
swapToSymbol={destinationTokenSymbol}
initialAggId={usedQuote.aggregator}
onQuoteDetailsIsOpened={quoteDetailsOpened}
/>
)}
<GasFeeContextProvider
editGasMode={EDIT_GAS_MODES.SWAPS}
minimumGasLimit={usedGasLimit}
transaction={transaction}
>
<TransactionModalContextProvider captureEventEnabled={false}>
<div className="view-quote">
<div
className={classnames('view-quote__content', {
'view-quote__content_modal': disableSubmissionDueToPriceWarning,
})}
>
{selectQuotePopoverShown && (
<SelectQuotePopover
quoteDataRows={renderablePopoverData}
onClose={() => setSelectQuotePopoverShown(false)}
onSubmit={(aggId) => dispatch(swapsQuoteSelected(aggId))}
swapToSymbol={destinationTokenSymbol}
initialAggId={usedQuote.aggregator}
onQuoteDetailsIsOpened={quoteDetailsOpened}
/>
)}
{showEditGasPopover && networkAndAccountSupports1559 && (
<EditGasPopover
transaction={{
userFeeLevel: swapsUserFeeLevel || GAS_RECOMMENDATIONS.HIGH,
txParams: {
maxFeePerGas,
maxPriorityFeePerGas,
gas: maxGasLimit,
},
}}
minimumGasLimit={usedGasLimit}
defaultEstimateToUse={GAS_RECOMMENDATIONS.HIGH}
mode={EDIT_GAS_MODES.SWAPS}
confirmButtonText={t('submit')}
onClose={onCloseEditGasPopover}
/>
)}
{!supportsEIP1559V2 &&
showEditGasPopover &&
networkAndAccountSupports1559 && (
<EditGasPopover
transaction={transaction}
minimumGasLimit={usedGasLimit}
defaultEstimateToUse={GAS_RECOMMENDATIONS.HIGH}
mode={EDIT_GAS_MODES.SWAPS}
confirmButtonText={t('submit')}
onClose={onCloseEditGasPopover}
/>
)}
{supportsEIP1559V2 && (
<>
<EditGasFeePopover />
<AdvancedGasFeePopover />
</>
)}
<div
className={classnames('view-quote__warning-wrapper', {
'view-quote__warning-wrapper--thin': !isShowingWarning,
})}
>
{viewQuotePriceDifferenceComponent}
{(showInsufficientWarning || tokenBalanceUnavailable) && (
<ActionableMessage
message={actionableBalanceErrorMessage}
onClose={() => setWarningHidden(true)}
<div
className={classnames('view-quote__warning-wrapper', {
'view-quote__warning-wrapper--thin': !isShowingWarning,
})}
>
{viewQuotePriceDifferenceComponent}
{(showInsufficientWarning || tokenBalanceUnavailable) && (
<ActionableMessage
message={actionableBalanceErrorMessage}
onClose={() => setWarningHidden(true)}
/>
)}
</div>
<div className="view-quote__countdown-timer-container">
<CountdownTimer
timeStarted={quotesLastFetched}
warningTime="0:30"
labelKey="swapNewQuoteIn"
/>
</div>
<MainQuoteSummary
sourceValue={calcTokenValue(
sourceTokenValue,
sourceTokenDecimals,
)}
sourceDecimals={sourceTokenDecimals}
sourceSymbol={sourceTokenSymbol}
destinationValue={calcTokenValue(
destinationTokenValue,
destinationTokenDecimals,
)}
destinationDecimals={destinationTokenDecimals}
destinationSymbol={destinationTokenSymbol}
sourceIconUrl={sourceTokenIconUrl}
destinationIconUrl={destinationIconUrl}
/>
)}
</div>
<div className="view-quote__countdown-timer-container">
<CountdownTimer
timeStarted={quotesLastFetched}
warningTime="0:30"
labelKey="swapNewQuoteIn"
/>
</div>
<MainQuoteSummary
sourceValue={calcTokenValue(sourceTokenValue, sourceTokenDecimals)}
sourceDecimals={sourceTokenDecimals}
sourceSymbol={sourceTokenSymbol}
destinationValue={calcTokenValue(
destinationTokenValue,
destinationTokenDecimals,
)}
destinationDecimals={destinationTokenDecimals}
destinationSymbol={destinationTokenSymbol}
sourceIconUrl={sourceTokenIconUrl}
destinationIconUrl={destinationIconUrl}
/>
<div
className={classnames('view-quote__fee-card-container', {
'view-quote__fee-card-container--three-rows':
approveTxParams && (!balanceError || warningHidden),
})}
>
<FeeCard
primaryFee={{
fee: feeInEth,
maxFee: maxFeeInEth,
<div
className={classnames('view-quote__fee-card-container', {
'view-quote__fee-card-container--three-rows':
approveTxParams && (!balanceError || warningHidden),
})}
>
<FeeCard
primaryFee={{
fee: feeInEth,
maxFee: maxFeeInEth,
}}
secondaryFee={{
fee: feeInFiat,
maxFee: maxFeeInFiat,
}}
onFeeCardMaxRowClick={onFeeCardMaxRowClick}
hideTokenApprovalRow={
!approveTxParams || (balanceError && !warningHidden)
}
tokenApprovalSourceTokenSymbol={sourceTokenSymbol}
onTokenApprovalClick={onFeeCardTokenApprovalClick}
metaMaskFee={String(metaMaskFee)}
numberOfQuotes={Object.values(quotes).length}
onQuotesClick={() => {
allAvailableQuotesOpened();
setSelectQuotePopoverShown(true);
}}
chainId={chainId}
isBestQuote={isBestQuote}
supportsEIP1559V2={supportsEIP1559V2}
/>
</div>
</div>
<SwapsFooter
onSubmit={() => {
setSubmitClicked(true);
if (!balanceError) {
dispatch(signAndSendTransactions(history, metaMetricsEvent));
} else if (destinationToken.symbol === defaultSwapsToken.symbol) {
history.push(DEFAULT_ROUTE);
} else {
history.push(`${ASSET_ROUTE}/${destinationToken.address}`);
}
}}
secondaryFee={{
fee: feeInFiat,
maxFee: maxFeeInFiat,
}}
onFeeCardMaxRowClick={onFeeCardMaxRowClick}
hideTokenApprovalRow={
!approveTxParams || (balanceError && !warningHidden)
submitText={t('swap')}
hideCancel
disabled={
submitClicked ||
balanceError ||
tokenBalanceUnavailable ||
disableSubmissionDueToPriceWarning ||
(networkAndAccountSupports1559 &&
baseAndPriorityFeePerGas === undefined) ||
(!networkAndAccountSupports1559 &&
(gasPrice === null || gasPrice === undefined))
}
tokenApprovalSourceTokenSymbol={sourceTokenSymbol}
onTokenApprovalClick={onFeeCardTokenApprovalClick}
@ -772,33 +839,7 @@ export default function ViewQuote() {
isBestQuote={isBestQuote}
/>
</div>
</div>
<SwapsFooter
onSubmit={() => {
setSubmitClicked(true);
if (!balanceError) {
dispatch(signAndSendTransactions(history, metaMetricsEvent));
} else if (destinationToken.symbol === defaultSwapsToken.symbol) {
history.push(DEFAULT_ROUTE);
} else {
history.push(`${ASSET_ROUTE}/${destinationToken.address}`);
}
}}
submitText={t('swap')}
hideCancel
disabled={
submitClicked ||
balanceError ||
tokenBalanceUnavailable ||
disableSubmissionDueToPriceWarning ||
(networkAndAccountSupports1559 &&
baseAndPriorityFeePerGas === undefined) ||
(!networkAndAccountSupports1559 &&
(gasPrice === null || gasPrice === undefined))
}
className={isShowingWarning && 'view-quote__thin-swaps-footer'}
showTopBorder
/>
</div>
</TransactionModalContextProvider>
</GasFeeContextProvider>
);
}