diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c4c137922..db5224de9 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -166,6 +166,9 @@ "advanced": { "message": "Advanced" }, + "advancedBaseGasFeeToolTip": { + "message": "Any difference between your max base fee and the current base fee will be refunded after completion." + }, "advancedGasFeeModalTitle": { "message": "Advanced gas fee" }, @@ -175,6 +178,9 @@ "advancedOptions": { "message": "Advanced Options" }, + "advancedPriorityFeeToolTip": { + "message": "Priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction." + }, "advancedSettingsDescription": { "message": "Access developer features, download State Logs, Reset Account, setup test networks and custom RPC" }, @@ -623,6 +629,9 @@ "currentLanguage": { "message": "Current Language" }, + "currentTitle": { + "message": "Current:" + }, "currentlyUnavailable": { "message": "Unavailable on this network" }, @@ -827,18 +836,10 @@ "editGasPriceTooltip": { "message": "This network requires a “Gas price” field when submitting a transaction. Gas price is the amount you will pay pay per unit of gas." }, - "editGasSubTextAmount": { - "message": "$1 $2", - "description": "$1 will be passed the editGasSubTextAmountLabel and $2 will be passed the amount in either cryptocurrency or fiat" - }, "editGasSubTextAmountLabel": { "message": "Max amount:", "description": "This is meant to be used as the $1 substitution editGasSubTextAmount" }, - "editGasSubTextFee": { - "message": "$1 $2", - "description": "$1 will be passed the editGasSubTextFeeLabel and $2 will be passed the fee amount in either cryptocurrency or fiat" - }, "editGasSubTextFeeLabel": { "message": "Max fee:", "description": "$1 represents a dollar amount" @@ -855,6 +856,12 @@ "editGasTooLowWarningTooltip": { "message": "This lowers your maximum fee but if network traffic increases your transaction may be delayed or fail." }, + "editInGwei": { + "message": "Edit in GWEI" + }, + "editInMultiplier": { + "message": "Edit in multiplier" + }, "editNonceField": { "message": "Edit Nonce" }, @@ -1593,6 +1600,9 @@ "mobileSyncWarning": { "message": "The 'Sync with extension' feature is temporarily disabled. If you want to use your extension wallet on MetaMask mobile, then on your mobile app: go back to the wallet setup options and select the 'Import with Secret Recovery Phrase' option. Use your extension wallet's secret phrase to then import your wallet into mobile." }, + "multiplier": { + "message": "multiplier" + }, "mustSelectOne": { "message": "Must select at least 1 token." }, @@ -2021,6 +2031,9 @@ "primaryCurrencySettingDescription": { "message": "Select native to prioritize displaying values in the native currency of the chain (e.g. ETH). Select Fiat to prioritize displaying values in your selected fiat currency." }, + "priorityFee": { + "message": "Priority Fee" + }, "privacyMsg": { "message": "Privacy Policy" }, @@ -3099,6 +3112,9 @@ "turnOnTokenDetection": { "message": "Turn on enhanced token detection" }, + "twelveHrTitle": { + "message": "12hr:" + }, "typePassword": { "message": "Type your MetaMask password" }, diff --git a/app/images/high-arrow.svg b/app/images/high-arrow.svg new file mode 100644 index 000000000..76449fe0d --- /dev/null +++ b/app/images/high-arrow.svg @@ -0,0 +1 @@ + diff --git a/app/images/low-arrow.svg b/app/images/low-arrow.svg new file mode 100644 index 000000000..da51d636f --- /dev/null +++ b/app/images/low-arrow.svg @@ -0,0 +1 @@ + diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 0f1d15961..c2655a6d7 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -33,6 +33,7 @@ import { GAS_ESTIMATE_TYPES, GAS_RECOMMENDATIONS, CUSTOM_GAS_ESTIMATE, + PRIORITY_LEVELS, } from '../../../../shared/constants/gas'; import { decGWEIToHexWEI } from '../../../../shared/modules/conversion.utils'; import { @@ -438,7 +439,11 @@ export default class TransactionController extends EventEmitter { ) { txMeta.txParams.maxFeePerGas = txMeta.txParams.gasPrice; txMeta.txParams.maxPriorityFeePerGas = txMeta.txParams.gasPrice; - txMeta.userFeeLevel = CUSTOM_GAS_ESTIMATE; + if (process.env.EIP_1559_V2) { + txMeta.userFeeLevel = PRIORITY_LEVELS.DAPP_SUGGESTED; + } else { + txMeta.userFeeLevel = CUSTOM_GAS_ESTIMATE; + } } else { if ( (defaultMaxFeePerGas && @@ -448,6 +453,8 @@ export default class TransactionController extends EventEmitter { txMeta.origin === 'metamask' ) { txMeta.userFeeLevel = GAS_RECOMMENDATIONS.MEDIUM; + } else if (process.env.EIP_1559_V2) { + txMeta.userFeeLevel = PRIORITY_LEVELS.DAPP_SUGGESTED; } else { txMeta.userFeeLevel = CUSTOM_GAS_ESTIMATE; } diff --git a/shared/modules/conversion.utils.js b/shared/modules/conversion.utils.js index 5c139bbbc..eba33fc8e 100644 --- a/shared/modules/conversion.utils.js +++ b/shared/modules/conversion.utils.js @@ -229,6 +229,21 @@ const multiplyCurrencies = (a, b, options = {}) => { }); }; +const divideCurrencies = (a, b, options = {}) => { + const { dividendBase, divisorBase, ...conversionOptions } = options; + + if (!isValidBase(dividendBase) || !isValidBase(divisorBase)) { + throw new Error('Must specify valid dividendBase and divisorBase'); + } + + const value = getBigNumber(a, dividendBase).div(getBigNumber(b, divisorBase)); + + return converter({ + value, + ...conversionOptions, + }); +}; + const conversionGreaterThan = ({ ...firstProps }, { ...secondProps }) => { const firstValue = converter({ ...firstProps }); const secondValue = converter({ ...secondProps }); @@ -291,4 +306,5 @@ export { decGWEIToHexWEI, toBigNumber, toNormalizedDenomination, + divideCurrencies, }; diff --git a/shared/modules/conversion.utils.test.js b/shared/modules/conversion.utils.test.js index d483a9a78..735d0edf2 100644 --- a/shared/modules/conversion.utils.test.js +++ b/shared/modules/conversion.utils.test.js @@ -1,5 +1,9 @@ import BigNumber from 'bignumber.js'; -import { addCurrencies, conversionUtil } from './conversion.utils'; +import { + addCurrencies, + conversionUtil, + divideCurrencies, +} from './conversion.utils'; describe('conversion utils', () => { describe('addCurrencies()', () => { @@ -163,4 +167,39 @@ describe('conversion utils', () => { ).toStrictEqual('1.5'); }); }); + + describe('divideCurrencies()', () => { + it('should correctly divide decimal values', () => { + const result = divideCurrencies(9, 3, { + dividendBase: 10, + divisorBase: 10, + }); + expect(result.toNumber()).toStrictEqual(3); + }); + + it('should correctly divide hexadecimal values', () => { + const result = divideCurrencies(1000, 0xa, { + dividendBase: 16, + divisorBase: 16, + }); + expect(result.toNumber()).toStrictEqual(0x100); + }); + + it('should correctly divide hexadecimal value from decimal value', () => { + const result = divideCurrencies(0x3e8, 0xa, { + dividendBase: 16, + divisorBase: 16, + }); + expect(result.toNumber()).toStrictEqual(0x100); + }); + + it('should throw error for wrong base value', () => { + expect(() => { + divideCurrencies(0x3e8, 0xa, { + dividendBase: 10.5, + divisorBase: 7, + }); + }).toThrow('Must specify valid dividendBase and divisorBase'); + }); + }); }); diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.js new file mode 100644 index 000000000..015ed5f7e --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.js @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Box from '../../../ui/box'; +import I18nValue from '../../../ui/i18n-value'; + +const AdvancedGasFeeInputSubtext = ({ latest, historical }) => { + return ( + + + + + + {latest} + + + + + + + {historical} + + + ); +}; + +AdvancedGasFeeInputSubtext.propTypes = { + latest: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + historical: PropTypes.string, +}; + +export default AdvancedGasFeeInputSubtext; diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js new file mode 100644 index 000000000..f466a08f0 --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import AdvancedGasFeeInputSubtext from './advanced-gas-fee-input-subtext'; + +describe('AdvancedGasFeeInputSubtext', () => { + it('should renders latest and historical values passed', () => { + render( + , + ); + + expect(screen.queryByText('Latest Value')).toBeInTheDocument(); + expect(screen.queryByText('Historical value')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.js new file mode 100644 index 000000000..13e6bb844 --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.js @@ -0,0 +1 @@ +export { default } from './advanced-gas-fee-input-subtext'; diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.scss b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.scss new file mode 100644 index 000000000..17aba4ccd --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.scss @@ -0,0 +1,17 @@ +.advanced-gas-fee-input-subtext { + display: flex; + align-items: center; + margin-top: 2px; + color: $ui-4; + font-size: $font-size-h7; + + &__label { + font-weight: bold; + margin-right: 4px; + } + + img { + height: 16px; + margin-right: 8px; + } +} diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/advanced-gas-fee-inputs.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/advanced-gas-fee-inputs.js new file mode 100644 index 000000000..654a414c5 --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/advanced-gas-fee-inputs.js @@ -0,0 +1,17 @@ +import React from 'react'; + +import Box from '../../../ui/box'; +import BasefeeInput from './basefee-input'; +import PriorityFeeInput from './priorityfee-input'; + +const AdvancedGasFeeInputs = () => { + return ( + + +
+ + + ); +}; + +export default AdvancedGasFeeInputs; diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/basefee-input.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/basefee-input.js new file mode 100644 index 000000000..d448393a7 --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/basefee-input.js @@ -0,0 +1,144 @@ +import React, { useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { PRIORITY_LEVELS } from '../../../../../shared/constants/gas'; +import { + divideCurrencies, + multiplyCurrencies, +} from '../../../../../shared/modules/conversion.utils'; +import { PRIMARY, SECONDARY } from '../../../../helpers/constants/common'; +import { decGWEIToHexWEI } from '../../../../helpers/utils/conversions.util'; +import { useGasFeeContext } from '../../../../contexts/gasFee'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; +import { useCurrencyDisplay } from '../../../../hooks/useCurrencyDisplay'; +import Button from '../../../ui/button'; +import FormField from '../../../ui/form-field'; +import I18nValue from '../../../ui/i18n-value'; + +import AdvancedGasFeeInputSubtext from '../advanced-gas-fee-input-subtext'; +import { getAdvancedGasFeeValues } from '../../../../selectors'; + +const divideCurrencyValues = (value, baseFee) => { + if (baseFee === 0) { + return 0; + } + return divideCurrencies(value, baseFee, { + numberOfDecimals: 2, + dividendBase: 10, + divisorBase: 10, + }).toNumber(); +}; + +const multiplyCurrencyValues = (baseFee, value, numberOfDecimals) => + multiplyCurrencies(baseFee, value, { + numberOfDecimals, + multiplicandBase: 10, + multiplierBase: 10, + }).toNumber(); + +const BasefeeInput = () => { + const t = useI18nContext(); + const { gasFeeEstimates, estimateUsed, maxFeePerGas } = useGasFeeContext(); + const { estimatedBaseFee } = gasFeeEstimates; + const { + numberOfDecimals: numberOfDecimalsPrimary, + } = useUserPreferencedCurrency(PRIMARY); + const { + currency, + numberOfDecimals: numberOfDecimalsFiat, + } = useUserPreferencedCurrency(SECONDARY); + + const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues); + + const [editingInGwei, setEditingInGwei] = useState(false); + + const [maxBaseFeeGWEI, setMaxBaseFeeGWEI] = useState(() => { + if ( + estimateUsed !== PRIORITY_LEVELS.CUSTOM && + advancedGasFeeValues?.maxBaseFee + ) { + return multiplyCurrencyValues( + estimatedBaseFee, + advancedGasFeeValues.maxBaseFee, + numberOfDecimalsPrimary, + ); + } + return maxFeePerGas; + }); + + const [maxBaseFeeMultiplier, setMaxBaseFeeMultiplier] = useState(() => { + if ( + estimateUsed !== PRIORITY_LEVELS.CUSTOM && + advancedGasFeeValues?.maxBaseFee + ) { + return advancedGasFeeValues.maxBaseFee; + } + return divideCurrencyValues(maxFeePerGas, estimatedBaseFee); + }); + + const [, { value: baseFeeInFiat }] = useCurrencyDisplay( + decGWEIToHexWEI(maxBaseFeeGWEI), + { currency, numberOfDecimalsFiat }, + ); + + const updateBaseFee = useCallback( + (value) => { + if (editingInGwei) { + setMaxBaseFeeGWEI(value); + setMaxBaseFeeMultiplier(divideCurrencyValues(value, estimatedBaseFee)); + } else { + setMaxBaseFeeMultiplier(value); + setMaxBaseFeeGWEI( + multiplyCurrencyValues( + estimatedBaseFee, + value, + numberOfDecimalsPrimary, + ), + ); + } + }, + [ + editingInGwei, + estimatedBaseFee, + numberOfDecimalsPrimary, + setMaxBaseFeeGWEI, + setMaxBaseFeeMultiplier, + ], + ); + + return ( + setEditingInGwei(!editingInGwei)} + > + + + } + value={editingInGwei ? maxBaseFeeGWEI : maxBaseFeeMultiplier} + detailText={ + editingInGwei + ? `${maxBaseFeeMultiplier}x ${`≈ ${baseFeeInFiat}`}` + : `${maxBaseFeeGWEI} GWEI ${`≈ ${baseFeeInFiat}`}` + } + numeric + inputDetails={ + + } + /> + ); +}; + +export default BasefeeInput; diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/basefee-input.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/basefee-input.test.js new file mode 100644 index 000000000..1818f5a07 --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/basefee-input.test.js @@ -0,0 +1,114 @@ +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; + +import mockEstimates from '../../../../../test/data/mock-estimates.json'; +import mockState from '../../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../../test/lib/render-helpers'; +import configureStore from '../../../../store/store'; +import { GasFeeContextProvider } from '../../../../contexts/gasFee'; + +import { GAS_ESTIMATE_TYPES } from '../../../../../shared/constants/gas'; +import BasefeeInput from './basefee-input'; + +jest.mock('../../../../store/actions', () => ({ + disconnectGasFeeEstimatePoller: jest.fn(), + getGasFeeEstimatesAndStartPolling: jest + .fn() + .mockImplementation(() => Promise.resolve()), + addPollingTokenToAppState: jest.fn(), +})); + +const render = (txProps) => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + accounts: { + [mockState.metamask.selectedAddress]: { + address: mockState.metamask.selectedAddress, + balance: '0x1F4', + }, + }, + advancedGasFee: { maxBaseFee: 2 }, + featureFlags: { advancedInlineGas: true }, + gasFeeEstimates: + mockEstimates[GAS_ESTIMATE_TYPES.FEE_MARKET].gasFeeEstimates, + }, + }); + + return renderWithProvider( + + + , + store, + ); +}; + +describe('BasefeeInput', () => { + it('should renders advancedGasFee.baseFee value if current estimate used is not custom', () => { + render({ + userFeeLevel: 'high', + }); + expect(document.getElementsByTagName('input')[0]).toHaveValue(2); + }); + + it('should renders baseFee values from transaction if current estimate used is custom', () => { + render({ + txParams: { + maxFeePerGas: '0x174876E800', + }, + }); + expect(document.getElementsByTagName('input')[0]).toHaveValue(2); + }); + + it('should show GWEI value in input when Edit in GWEI link is clicked', () => { + render({ + txParams: { + maxFeePerGas: '0x174876E800', + }, + }); + fireEvent.click(screen.queryByText('Edit in GWEI')); + expect(document.getElementsByTagName('input')[0]).toHaveValue(100); + }); + + it('should correctly update GWEI value if multiplier is changed', () => { + render({ + txParams: { + maxFeePerGas: '0x174876E800', + }, + }); + fireEvent.change(document.getElementsByTagName('input')[0], { + target: { value: 4 }, + }); + fireEvent.click(screen.queryByText('Edit in GWEI')); + expect(document.getElementsByTagName('input')[0]).toHaveValue(200); + }); + + it('should correctly update multiplier value if GWEI is changed', () => { + render({ + txParams: { + maxFeePerGas: '0x174876E800', + }, + }); + expect(document.getElementsByTagName('input')[0]).toHaveValue(2); + fireEvent.click(screen.queryByText('Edit in GWEI')); + fireEvent.change(document.getElementsByTagName('input')[0], { + target: { value: 200 }, + }); + fireEvent.click(screen.queryByText('Edit in multiplier')); + expect(document.getElementsByTagName('input')[0]).toHaveValue(4); + }); + + it('should show current value of estimatedBaseFee in subtext', () => { + render({ + txParams: { + maxFeePerGas: '0x174876E800', + }, + }); + expect(screen.queryByText('50')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/index.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/index.js new file mode 100644 index 000000000..787f2a6d8 --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/index.js @@ -0,0 +1 @@ +export { default } from './advanced-gas-fee-inputs'; diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/index.scss b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/index.scss new file mode 100644 index 000000000..489722155 --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/index.scss @@ -0,0 +1,22 @@ +.advanced-gas-fee-input { + a.advanced-gas-fee-input__edit-link { + display: inline; + font-size: $font-size-h7; + padding: 0; + white-space: nowrap; + } + + .form-field__heading-title > h6 { + font-size: $font-size-h7; + } + + &__border { + padding-bottom: 16px; + border-bottom: 1px solid $Grey-100; + } + + &__separator { + border-top: 1px solid $ui-grey; + margin: 24px 0 16px 0; + } +} diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priorityfee-input.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priorityfee-input.js new file mode 100644 index 000000000..6e75f1b45 --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priorityfee-input.js @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { PRIORITY_LEVELS } from '../../../../../shared/constants/gas'; +import { SECONDARY } from '../../../../helpers/constants/common'; +import { decGWEIToHexWEI } from '../../../../helpers/utils/conversions.util'; +import { getAdvancedGasFeeValues } from '../../../../selectors'; +import { useCurrencyDisplay } from '../../../../hooks/useCurrencyDisplay'; +import { useGasFeeContext } from '../../../../contexts/gasFee'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { useUserPreferencedCurrency } from '../../../../hooks/useUserPreferencedCurrency'; +import FormField from '../../../ui/form-field'; + +import AdvancedGasFeeInputSubtext from '../advanced-gas-fee-input-subtext'; + +const PriorityFeeInput = () => { + const t = useI18nContext(); + const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues); + + const { estimateUsed, maxPriorityFeePerGas } = useGasFeeContext(); + + const [priorityFee, setPriorityFee] = useState(() => { + if ( + estimateUsed !== PRIORITY_LEVELS.CUSTOM && + advancedGasFeeValues?.priorityFee + ) + return advancedGasFeeValues.priorityFee; + return maxPriorityFeePerGas; + }); + + const { currency, numberOfDecimals } = useUserPreferencedCurrency(SECONDARY); + + const [, { value: priorityFeeInFiat }] = useCurrencyDisplay( + decGWEIToHexWEI(priorityFee), + { currency, numberOfDecimals }, + ); + + return ( + + } + /> + ); +}; + +export default PriorityFeeInput; diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priorityfee-input.test.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priorityfee-input.test.js new file mode 100644 index 000000000..9240b83b8 --- /dev/null +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priorityfee-input.test.js @@ -0,0 +1,66 @@ +import React from 'react'; + +import mockEstimates from '../../../../../test/data/mock-estimates.json'; +import mockState from '../../../../../test/data/mock-state.json'; +import { renderWithProvider } from '../../../../../test/lib/render-helpers'; +import configureStore from '../../../../store/store'; +import { GasFeeContextProvider } from '../../../../contexts/gasFee'; + +import { GAS_ESTIMATE_TYPES } from '../../../../../shared/constants/gas'; +import PriprityfeeInput from './priorityfee-input'; + +jest.mock('../../../../store/actions', () => ({ + disconnectGasFeeEstimatePoller: jest.fn(), + getGasFeeEstimatesAndStartPolling: jest + .fn() + .mockImplementation(() => Promise.resolve()), + addPollingTokenToAppState: jest.fn(), +})); + +const render = (txProps) => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + accounts: { + [mockState.metamask.selectedAddress]: { + address: mockState.metamask.selectedAddress, + balance: '0x1F4', + }, + }, + advancedGasFee: { priorityFee: 100 }, + featureFlags: { advancedInlineGas: true }, + gasFeeEstimates: + mockEstimates[GAS_ESTIMATE_TYPES.FEE_MARKET].gasFeeEstimates, + }, + }); + + return renderWithProvider( + + + , + store, + ); +}; + +describe('PriorityfeeInput', () => { + it('should renders advancedGasFee.priorityfee value if current estimate used is not custom', () => { + render({ + userFeeLevel: 'high', + }); + expect(document.getElementsByTagName('input')[0]).toHaveValue(100); + }); + + it('should renders priorityfee value from transaction if current estimate used is custom', () => { + render({ + txParams: { + maxPriorityFeePerGas: '0x77359400', + }, + }); + expect(document.getElementsByTagName('input')[0]).toHaveValue(2); + }); +}); diff --git a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.js b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.js index 41bfced9a..5a673afda 100644 --- a/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.js +++ b/ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.js @@ -8,26 +8,33 @@ import Button from '../../ui/button'; import I18nValue from '../../ui/i18n-value'; import Popover from '../../ui/popover'; +import AdvancedGasFeeInputs from './advanced-gas-fee-inputs'; + const AdvancedGasFeePopover = () => { const t = useI18nContext(); - const { closeModal, currentModal } = useTransactionModalContext(); + const { + closeModal, + closeAllModals, + currentModal, + } = useTransactionModalContext(); if (currentModal !== 'advancedGasFee') return null; - // todo: align styles to edit gas fee modal return ( closeModal('advancedGasFee')} - onClose={() => closeModal('advancedGasFee')} + onClose={closeAllModals} footer={ } > - + + + ); }; diff --git a/ui/components/app/advanced-gas-fee-popover/index.scss b/ui/components/app/advanced-gas-fee-popover/index.scss index 1e4959be6..604a983e5 100644 --- a/ui/components/app/advanced-gas-fee-popover/index.scss +++ b/ui/components/app/advanced-gas-fee-popover/index.scss @@ -1,8 +1,10 @@ .advanced-gas-fee-popover { - .popover-header { - border-radius: 0; - border-top-left-radius: 10px; - border-top-right-radius: 10px; - border-bottom: 1px solid $Grey-200; + &__wrapper { + border-top: 1px solid $ui-grey; + } + + &__separator { + border-top: 1px solid $ui-grey; + margin: 24px 0 16px 0; } } diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index da57af878..961ed1c84 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -52,3 +52,5 @@ @import 'whats-new-popup/index'; @import 'loading-network-screen/index'; @import 'advanced-gas-fee-popover/index'; +@import 'advanced-gas-fee-popover/advanced-gas-fee-inputs/index'; +@import 'advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index'; diff --git a/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js b/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js index 3a1b541b3..cd95b654f 100644 --- a/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js +++ b/ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js @@ -46,7 +46,7 @@ const DAPP_SUGGESTED_ESTIMATE = { maxPriorityFeePerGas: '0x59682f00', }; -const renderComponent = (props, transactionProps, gasFeeContextProps) => { +const renderComponent = (componentProps, transactionProps) => { const store = configureStore({ metamask: { nativeCurrency: ETH, @@ -71,9 +71,8 @@ const renderComponent = (props, transactionProps, gasFeeContextProps) => { return renderWithProvider( - + , store, ); @@ -137,7 +136,7 @@ describe('EditGasItem', () => { }); it('should renders advance gas estimate option for priorityLevel custom', () => { - renderComponent({ priorityLevel: 'custom' }); + renderComponent({ priorityLevel: 'custom' }, { userFeeLevel: 'high' }); expect( screen.queryByRole('button', { name: 'custom' }), ).toBeInTheDocument(); diff --git a/ui/components/app/transaction-detail/transaction-detail.component.test.js b/ui/components/app/transaction-detail/transaction-detail.component.test.js index ee74034ec..a1f899413 100644 --- a/ui/components/app/transaction-detail/transaction-detail.component.test.js +++ b/ui/components/app/transaction-detail/transaction-detail.component.test.js @@ -1,7 +1,10 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas'; +import { + GAS_ESTIMATE_TYPES, + PRIORITY_LEVELS, +} from '../../../../shared/constants/gas'; import { TRANSACTION_ENVELOPE_TYPES } from '../../../../shared/constants/transaction'; import { GasFeeContextProvider } from '../../../contexts/gasFee'; @@ -79,6 +82,7 @@ describe('TransactionDetail', () => { render({ contextProps: { transaction: { + userFeeLevel: PRIORITY_LEVELS.DAPP_SUGGESTED, dappSuggestedGasFees: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 }, txParams: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 }, }, diff --git a/ui/components/ui/form-field/form-field.js b/ui/components/ui/form-field/form-field.js index 5cd1fd5b6..79386680c 100644 --- a/ui/components/ui/form-field/form-field.js +++ b/ui/components/ui/form-field/form-field.js @@ -30,6 +30,7 @@ export default function FormField({ password, allowDecimals, disabled, + inputDetails, }) { return (
)} + {inputDetails}
); @@ -127,6 +129,7 @@ FormField.propTypes = { password: PropTypes.bool, allowDecimals: PropTypes.bool, disabled: PropTypes.bool, + inputDetails: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), }; FormField.defaultProps = { @@ -143,4 +146,5 @@ FormField.defaultProps = { password: false, allowDecimals: true, disabled: false, + inputDetails: '', }; diff --git a/ui/components/ui/form-field/index.scss b/ui/components/ui/form-field/index.scss index d7d4cb0b7..ed8859141 100644 --- a/ui/components/ui/form-field/index.scss +++ b/ui/components/ui/form-field/index.scss @@ -3,6 +3,7 @@ &__heading { display: flex; + align-items: center; margin-top: 4px; } diff --git a/ui/contexts/transaction-modal.js b/ui/contexts/transaction-modal.js index 18735d8b2..bf16ecf09 100644 --- a/ui/contexts/transaction-modal.js +++ b/ui/contexts/transaction-modal.js @@ -43,6 +43,10 @@ export const TransactionModalContextProvider = ({ setOpenModals(modals); }; + const closeAllModals = () => { + setOpenModals([]); + }; + const openModal = (modalName) => { if (openModals.includes(modalName)) return; captureEvent(); @@ -55,6 +59,7 @@ export const TransactionModalContextProvider = ({ { - if (areDappSuggestedAndTxParamGasFeesTheSame(transaction)) { - return 'dappSuggested'; + if (estimateToUse) { + return estimateToUse; } - return estimateToUse; + return PRIORITY_LEVELS.CUSTOM; }); /** @@ -118,9 +118,7 @@ export function useGasFeeInputs( * so that transaction is source of truth whenever possible. */ useEffect(() => { - if (areDappSuggestedAndTxParamGasFeesTheSame(transaction)) { - setEstimateUsed('dappSuggested'); - } else if (transaction?.userFeeLevel) { + if (transaction?.userFeeLevel) { setEstimateUsed(transaction?.userFeeLevel); } }, [setEstimateUsed, transaction]); @@ -219,11 +217,6 @@ export function useGasFeeInputs( const { updateTransactionUsingGasFeeEstimates } = useTransactionFunctions({ defaultEstimateToUse, gasLimit, - gasPrice, - maxFeePerGas, - maxPriorityFeePerGas, - gasFeeEstimates, - supportsEIP1559, transaction, }); diff --git a/ui/hooks/gasFeeInput/useTransactionFunctions.js b/ui/hooks/gasFeeInput/useTransactionFunctions.js index ea891869a..d4f094240 100644 --- a/ui/hooks/gasFeeInput/useTransactionFunctions.js +++ b/ui/hooks/gasFeeInput/useTransactionFunctions.js @@ -2,16 +2,12 @@ import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { PRIORITY_LEVELS } from '../../../shared/constants/gas'; -import { - decGWEIToHexWEI, - decimalToHex, -} from '../../helpers/utils/conversions.util'; +import { decimalToHex } from '../../helpers/utils/conversions.util'; import { updateTransaction as updateTransactionFn } from '../../store/actions'; export const useTransactionFunctions = ({ defaultEstimateToUse, gasLimit, - gasFeeEstimates, transaction, }) => { const dispatch = useDispatch(); @@ -23,9 +19,13 @@ export const useTransactionFunctions = ({ gasLimit: decimalToHex(gasLimit), estimateSuggested: defaultEstimateToUse, estimateUsed, - maxFeePerGas, - maxPriorityFeePerGas, }; + if (maxFeePerGas) { + newGasSettings.maxFeePerGas = maxFeePerGas; + } + if (maxPriorityFeePerGas) { + newGasSettings.maxPriorityFeePerGas = maxPriorityFeePerGas; + } const updatedTxMeta = { ...transaction, @@ -49,23 +49,15 @@ export const useTransactionFunctions = ({ maxPriorityFeePerGas, } = transaction?.dappSuggestedGasFees; updateTransaction( - PRIORITY_LEVELS.CUSTOM, + PRIORITY_LEVELS.DAPP_SUGGESTED, maxFeePerGas, maxPriorityFeePerGas, ); } else { - const { - suggestedMaxFeePerGas, - suggestedMaxPriorityFeePerGas, - } = gasFeeEstimates[gasFeeEstimateToUse]; - updateTransaction( - gasFeeEstimateToUse, - decGWEIToHexWEI(suggestedMaxFeePerGas), - decGWEIToHexWEI(suggestedMaxPriorityFeePerGas), - ); + updateTransaction(gasFeeEstimateToUse); } }, - [gasFeeEstimates, transaction?.dappSuggestedGasFees, updateTransaction], + [transaction?.dappSuggestedGasFees, updateTransaction], ); return { updateTransactionUsingGasFeeEstimates }; diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js index 6de9074c9..8f611e870 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -514,22 +514,26 @@ export default class ConfirmTransactionBase extends Component {
} subText={ - !isMultiLayerFeeNetwork && - t('editGasSubTextFee', [ - {t('editGasSubTextFeeLabel')}, -
- {renderHeartBeatIfNotInTest()} - -
, - ]) + !isMultiLayerFeeNetwork && ( + <> + + {t('editGasSubTextFeeLabel')} + + , +
+ {renderHeartBeatIfNotInTest()} + +
+ + ) } subTitle={ <> @@ -606,12 +610,14 @@ export default class ConfirmTransactionBase extends Component { detailText={renderTotalDetailText()} detailTotal={renderTotalDetailTotal()} subTitle={t('transactionDetailGasTotalSubtitle')} - subText={t('editGasSubTextAmount', [ - - {t('editGasSubTextAmountLabel')} - , - renderTotalMaxAmount(), - ])} + subText={ + <> + + {t('editGasSubTextAmountLabel')} + + {renderTotalMaxAmount()} + + } /> ), ]} diff --git a/ui/pages/confirm-transaction-base/gas-details-item/gas-details-item.js b/ui/pages/confirm-transaction-base/gas-details-item/gas-details-item.js index 91280266e..0c00be4e9 100644 --- a/ui/pages/confirm-transaction-base/gas-details-item/gas-details-item.js +++ b/ui/pages/confirm-transaction-base/gas-details-item/gas-details-item.js @@ -92,34 +92,36 @@ const GasDetailsItem = ({ /> } - subText={t('editGasSubTextFee', [ - - - - {estimateUsed === 'high' && '⚠ '} - - - -
+ - - -
-
, - ])} + + + {estimateUsed === 'high' && '⚠ '} + + + +
+ + +
+
+ + } subTitle={