EIP-1559 V2 : Advanced gas fee modal - Max base fee and Priority fee inputs (#12619)

feature/default_network_editable
Niranjana Binoy 3 years ago committed by GitHub
parent c2ea04c775
commit 4b975adc85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      app/_locales/en/messages.json
  2. 1
      app/images/high-arrow.svg
  3. 1
      app/images/low-arrow.svg
  4. 9
      app/scripts/controllers/transactions/index.js
  5. 16
      shared/modules/conversion.utils.js
  6. 41
      shared/modules/conversion.utils.test.js
  7. 32
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.js
  8. 18
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/advanced-gas-fee-input-subtext.test.js
  9. 1
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.js
  10. 17
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-input-subtext/index.scss
  11. 17
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/advanced-gas-fee-inputs.js
  12. 144
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/basefee-input.js
  13. 114
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/basefee-input.test.js
  14. 1
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/index.js
  15. 22
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/index.scss
  16. 57
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priorityfee-input.js
  17. 66
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-inputs/priorityfee-input.test.js
  18. 15
      ui/components/app/advanced-gas-fee-popover/advanced-gas-fee-popover.js
  19. 12
      ui/components/app/advanced-gas-fee-popover/index.scss
  20. 2
      ui/components/app/app-components.scss
  21. 7
      ui/components/app/edit-gas-fee-popover/edit-gas-item/edit-gas-item.test.js
  22. 6
      ui/components/app/transaction-detail/transaction-detail.component.test.js
  23. 4
      ui/components/ui/form-field/form-field.js
  24. 1
      ui/components/ui/form-field/index.scss
  25. 5
      ui/contexts/transaction-modal.js
  26. 17
      ui/hooks/gasFeeInput/useGasFeeInputs.js
  27. 28
      ui/hooks/gasFeeInput/useTransactionFunctions.js
  28. 50
      ui/pages/confirm-transaction-base/confirm-transaction-base.component.js
  29. 56
      ui/pages/confirm-transaction-base/gas-details-item/gas-details-item.js

@ -166,6 +166,9 @@
"advanced": { "advanced": {
"message": "Advanced" "message": "Advanced"
}, },
"advancedBaseGasFeeToolTip": {
"message": "Any difference between your max base fee and the current base fee will be refunded after completion."
},
"advancedGasFeeModalTitle": { "advancedGasFeeModalTitle": {
"message": "Advanced gas fee" "message": "Advanced gas fee"
}, },
@ -175,6 +178,9 @@
"advancedOptions": { "advancedOptions": {
"message": "Advanced Options" "message": "Advanced Options"
}, },
"advancedPriorityFeeToolTip": {
"message": "Priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction."
},
"advancedSettingsDescription": { "advancedSettingsDescription": {
"message": "Access developer features, download State Logs, Reset Account, setup test networks and custom RPC" "message": "Access developer features, download State Logs, Reset Account, setup test networks and custom RPC"
}, },
@ -623,6 +629,9 @@
"currentLanguage": { "currentLanguage": {
"message": "Current Language" "message": "Current Language"
}, },
"currentTitle": {
"message": "Current:"
},
"currentlyUnavailable": { "currentlyUnavailable": {
"message": "Unavailable on this network" "message": "Unavailable on this network"
}, },
@ -827,18 +836,10 @@
"editGasPriceTooltip": { "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." "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": { "editGasSubTextAmountLabel": {
"message": "Max amount:", "message": "Max amount:",
"description": "This is meant to be used as the $1 substitution editGasSubTextAmount" "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": { "editGasSubTextFeeLabel": {
"message": "Max fee:", "message": "Max fee:",
"description": "$1 represents a dollar amount" "description": "$1 represents a dollar amount"
@ -855,6 +856,12 @@
"editGasTooLowWarningTooltip": { "editGasTooLowWarningTooltip": {
"message": "This lowers your maximum fee but if network traffic increases your transaction may be delayed or fail." "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": { "editNonceField": {
"message": "Edit Nonce" "message": "Edit Nonce"
}, },
@ -1593,6 +1600,9 @@
"mobileSyncWarning": { "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." "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": { "mustSelectOne": {
"message": "Must select at least 1 token." "message": "Must select at least 1 token."
}, },
@ -2021,6 +2031,9 @@
"primaryCurrencySettingDescription": { "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." "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": { "privacyMsg": {
"message": "Privacy Policy" "message": "Privacy Policy"
}, },
@ -3099,6 +3112,9 @@
"turnOnTokenDetection": { "turnOnTokenDetection": {
"message": "Turn on enhanced token detection" "message": "Turn on enhanced token detection"
}, },
"twelveHrTitle": {
"message": "12hr:"
},
"typePassword": { "typePassword": {
"message": "Type your MetaMask password" "message": "Type your MetaMask password"
}, },

@ -0,0 +1 @@
<svg width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.022 4.82c0 .275.22.469.483.482l3.26-.082L3.8 9.183a.451.451 0 0 0 0 .663l.442.442c.18.18.47.193.663 0l3.963-3.964-.082 3.232a.49.49 0 0 0 .47.497h.607a.484.484 0 0 0 .47-.47V4.199a.46.46 0 0 0-.456-.456H4.49a.484.484 0 0 0-.47.47v.607Z" fill="#219E37"/></svg>

After

Width:  |  Height:  |  Size: 347 B

@ -0,0 +1 @@
<svg width="13" height="13" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.296 8.42c0-.276-.22-.47-.483-.483l-3.26.083 3.964-3.964a.451.451 0 0 0 0-.663l-.442-.442a.463.463 0 0 0-.662 0L4.449 6.915l.083-3.232a.49.49 0 0 0-.47-.497h-.607a.484.484 0 0 0-.47.47v5.386a.46.46 0 0 0 .456.456h5.386a.484.484 0 0 0 .47-.47V8.42Z" fill="#D73A49"/></svg>

After

Width:  |  Height:  |  Size: 358 B

@ -33,6 +33,7 @@ import {
GAS_ESTIMATE_TYPES, GAS_ESTIMATE_TYPES,
GAS_RECOMMENDATIONS, GAS_RECOMMENDATIONS,
CUSTOM_GAS_ESTIMATE, CUSTOM_GAS_ESTIMATE,
PRIORITY_LEVELS,
} from '../../../../shared/constants/gas'; } from '../../../../shared/constants/gas';
import { decGWEIToHexWEI } from '../../../../shared/modules/conversion.utils'; import { decGWEIToHexWEI } from '../../../../shared/modules/conversion.utils';
import { import {
@ -438,7 +439,11 @@ export default class TransactionController extends EventEmitter {
) { ) {
txMeta.txParams.maxFeePerGas = txMeta.txParams.gasPrice; txMeta.txParams.maxFeePerGas = txMeta.txParams.gasPrice;
txMeta.txParams.maxPriorityFeePerGas = 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 { } else {
if ( if (
(defaultMaxFeePerGas && (defaultMaxFeePerGas &&
@ -448,6 +453,8 @@ export default class TransactionController extends EventEmitter {
txMeta.origin === 'metamask' txMeta.origin === 'metamask'
) { ) {
txMeta.userFeeLevel = GAS_RECOMMENDATIONS.MEDIUM; txMeta.userFeeLevel = GAS_RECOMMENDATIONS.MEDIUM;
} else if (process.env.EIP_1559_V2) {
txMeta.userFeeLevel = PRIORITY_LEVELS.DAPP_SUGGESTED;
} else { } else {
txMeta.userFeeLevel = CUSTOM_GAS_ESTIMATE; txMeta.userFeeLevel = CUSTOM_GAS_ESTIMATE;
} }

@ -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 conversionGreaterThan = ({ ...firstProps }, { ...secondProps }) => {
const firstValue = converter({ ...firstProps }); const firstValue = converter({ ...firstProps });
const secondValue = converter({ ...secondProps }); const secondValue = converter({ ...secondProps });
@ -291,4 +306,5 @@ export {
decGWEIToHexWEI, decGWEIToHexWEI,
toBigNumber, toBigNumber,
toNormalizedDenomination, toNormalizedDenomination,
divideCurrencies,
}; };

@ -1,5 +1,9 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { addCurrencies, conversionUtil } from './conversion.utils'; import {
addCurrencies,
conversionUtil,
divideCurrencies,
} from './conversion.utils';
describe('conversion utils', () => { describe('conversion utils', () => {
describe('addCurrencies()', () => { describe('addCurrencies()', () => {
@ -163,4 +167,39 @@ describe('conversion utils', () => {
).toStrictEqual('1.5'); ).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');
});
});
}); });

@ -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 (
<Box className="advanced-gas-fee-input-subtext">
<Box display="flex" alignItems="center">
<span className="advanced-gas-fee-input-subtext__label">
<I18nValue messageKey="currentTitle" />
</span>
<span>{latest}</span>
<img src="./images/high-arrow.svg" alt="" />
</Box>
<Box>
<span className="advanced-gas-fee-input-subtext__label">
<I18nValue messageKey="twelveHrTitle" />
</span>
<span>{historical}</span>
</Box>
</Box>
);
};
AdvancedGasFeeInputSubtext.propTypes = {
latest: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
historical: PropTypes.string,
};
export default AdvancedGasFeeInputSubtext;

@ -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(
<AdvancedGasFeeInputSubtext
latest="Latest Value"
historical="Historical value"
/>,
);
expect(screen.queryByText('Latest Value')).toBeInTheDocument();
expect(screen.queryByText('Historical value')).toBeInTheDocument();
});
});

@ -0,0 +1 @@
export { default } from './advanced-gas-fee-input-subtext';

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

@ -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 (
<Box className="advanced-gas-fee-input" margin={4}>
<BasefeeInput />
<div className="advanced-gas-fee-input__separator" />
<PriorityFeeInput />
</Box>
);
};
export default AdvancedGasFeeInputs;

@ -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 (
<FormField
onChange={updateBaseFee}
titleText={t('maxBaseFee')}
titleUnit={editingInGwei ? 'GWEI' : `(${t('multiplier')})`}
tooltipText={t('advancedBaseGasFeeToolTip')}
titleDetail={
<Button
className="advanced-gas-fee-input__edit-link"
type="link"
onClick={() => setEditingInGwei(!editingInGwei)}
>
<I18nValue
messageKey={editingInGwei ? 'editInMultiplier' : 'editInGwei'}
/>
</Button>
}
value={editingInGwei ? maxBaseFeeGWEI : maxBaseFeeMultiplier}
detailText={
editingInGwei
? `${maxBaseFeeMultiplier}x ${`${baseFeeInFiat}`}`
: `${maxBaseFeeGWEI} GWEI ${`${baseFeeInFiat}`}`
}
numeric
inputDetails={
<AdvancedGasFeeInputSubtext
latest={estimatedBaseFee}
historical="23-359 GWEI"
/>
}
/>
);
};
export default BasefeeInput;

@ -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(
<GasFeeContextProvider
transaction={{
userFeeLevel: 'custom',
...txProps,
}}
>
<BasefeeInput />
</GasFeeContextProvider>,
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();
});
});

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

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

@ -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 (
<FormField
onChange={setPriorityFee}
titleText={t('priorityFee')}
titleUnit="(GWEI)"
tooltipText={t('advancedPriorityFeeToolTip')}
value={priorityFee}
detailText={`${priorityFeeInFiat}`}
numeric
inputDetails={
<AdvancedGasFeeInputSubtext
latest="1-18 GWEI"
historical="23-359 GWEI"
/>
}
/>
);
};
export default PriorityFeeInput;

@ -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(
<GasFeeContextProvider
transaction={{
userFeeLevel: 'custom',
...txProps,
}}
>
<PriprityfeeInput />
</GasFeeContextProvider>,
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);
});
});

@ -8,26 +8,33 @@ import Button from '../../ui/button';
import I18nValue from '../../ui/i18n-value'; import I18nValue from '../../ui/i18n-value';
import Popover from '../../ui/popover'; import Popover from '../../ui/popover';
import AdvancedGasFeeInputs from './advanced-gas-fee-inputs';
const AdvancedGasFeePopover = () => { const AdvancedGasFeePopover = () => {
const t = useI18nContext(); const t = useI18nContext();
const { closeModal, currentModal } = useTransactionModalContext(); const {
closeModal,
closeAllModals,
currentModal,
} = useTransactionModalContext();
if (currentModal !== 'advancedGasFee') return null; if (currentModal !== 'advancedGasFee') return null;
// todo: align styles to edit gas fee modal
return ( return (
<Popover <Popover
className="advanced-gas-fee-popover" className="advanced-gas-fee-popover"
title={t('advancedGasFeeModalTitle')} title={t('advancedGasFeeModalTitle')}
onBack={() => closeModal('advancedGasFee')} onBack={() => closeModal('advancedGasFee')}
onClose={() => closeModal('advancedGasFee')} onClose={closeAllModals}
footer={ footer={
<Button type="primary"> <Button type="primary">
<I18nValue messageKey="save" /> <I18nValue messageKey="save" />
</Button> </Button>
} }
> >
<Box className="advanced-gas-fee-popover" margin={4}></Box> <Box className="advanced-gas-fee-popover__wrapper">
<AdvancedGasFeeInputs />
</Box>
</Popover> </Popover>
); );
}; };

@ -1,8 +1,10 @@
.advanced-gas-fee-popover { .advanced-gas-fee-popover {
.popover-header { &__wrapper {
border-radius: 0; border-top: 1px solid $ui-grey;
border-top-left-radius: 10px; }
border-top-right-radius: 10px;
border-bottom: 1px solid $Grey-200; &__separator {
border-top: 1px solid $ui-grey;
margin: 24px 0 16px 0;
} }
} }

@ -52,3 +52,5 @@
@import 'whats-new-popup/index'; @import 'whats-new-popup/index';
@import 'loading-network-screen/index'; @import 'loading-network-screen/index';
@import 'advanced-gas-fee-popover/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';

@ -46,7 +46,7 @@ const DAPP_SUGGESTED_ESTIMATE = {
maxPriorityFeePerGas: '0x59682f00', maxPriorityFeePerGas: '0x59682f00',
}; };
const renderComponent = (props, transactionProps, gasFeeContextProps) => { const renderComponent = (componentProps, transactionProps) => {
const store = configureStore({ const store = configureStore({
metamask: { metamask: {
nativeCurrency: ETH, nativeCurrency: ETH,
@ -71,9 +71,8 @@ const renderComponent = (props, transactionProps, gasFeeContextProps) => {
return renderWithProvider( return renderWithProvider(
<GasFeeContextProvider <GasFeeContextProvider
transaction={{ txParams: { gas: '0x5208' }, ...transactionProps }} transaction={{ txParams: { gas: '0x5208' }, ...transactionProps }}
{...gasFeeContextProps}
> >
<EditGasItem priorityLevel="low" {...props} /> <EditGasItem priorityLevel="low" {...componentProps} />
</GasFeeContextProvider>, </GasFeeContextProvider>,
store, store,
); );
@ -137,7 +136,7 @@ describe('EditGasItem', () => {
}); });
it('should renders advance gas estimate option for priorityLevel custom', () => { it('should renders advance gas estimate option for priorityLevel custom', () => {
renderComponent({ priorityLevel: 'custom' }); renderComponent({ priorityLevel: 'custom' }, { userFeeLevel: 'high' });
expect( expect(
screen.queryByRole('button', { name: 'custom' }), screen.queryByRole('button', { name: 'custom' }),
).toBeInTheDocument(); ).toBeInTheDocument();

@ -1,7 +1,10 @@
import React from 'react'; import React from 'react';
import { screen } from '@testing-library/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 { TRANSACTION_ENVELOPE_TYPES } from '../../../../shared/constants/transaction';
import { GasFeeContextProvider } from '../../../contexts/gasFee'; import { GasFeeContextProvider } from '../../../contexts/gasFee';
@ -79,6 +82,7 @@ describe('TransactionDetail', () => {
render({ render({
contextProps: { contextProps: {
transaction: { transaction: {
userFeeLevel: PRIORITY_LEVELS.DAPP_SUGGESTED,
dappSuggestedGasFees: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 }, dappSuggestedGasFees: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 },
txParams: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 }, txParams: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 },
}, },

@ -30,6 +30,7 @@ export default function FormField({
password, password,
allowDecimals, allowDecimals,
disabled, disabled,
inputDetails,
}) { }) {
return ( return (
<div <div
@ -107,6 +108,7 @@ export default function FormField({
{error} {error}
</Typography> </Typography>
)} )}
{inputDetails}
</label> </label>
</div> </div>
); );
@ -127,6 +129,7 @@ FormField.propTypes = {
password: PropTypes.bool, password: PropTypes.bool,
allowDecimals: PropTypes.bool, allowDecimals: PropTypes.bool,
disabled: PropTypes.bool, disabled: PropTypes.bool,
inputDetails: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
}; };
FormField.defaultProps = { FormField.defaultProps = {
@ -143,4 +146,5 @@ FormField.defaultProps = {
password: false, password: false,
allowDecimals: true, allowDecimals: true,
disabled: false, disabled: false,
inputDetails: '',
}; };

@ -3,6 +3,7 @@
&__heading { &__heading {
display: flex; display: flex;
align-items: center;
margin-top: 4px; margin-top: 4px;
} }

@ -43,6 +43,10 @@ export const TransactionModalContextProvider = ({
setOpenModals(modals); setOpenModals(modals);
}; };
const closeAllModals = () => {
setOpenModals([]);
};
const openModal = (modalName) => { const openModal = (modalName) => {
if (openModals.includes(modalName)) return; if (openModals.includes(modalName)) return;
captureEvent(); captureEvent();
@ -55,6 +59,7 @@ export const TransactionModalContextProvider = ({
<TransactionModalContext.Provider <TransactionModalContext.Provider
value={{ value={{
closeModal, closeModal,
closeAllModals,
currentModal: openModals[openModals.length - 1], currentModal: openModals[openModals.length - 1],
openModal, openModal,
}} }}

@ -5,9 +5,9 @@ import {
CUSTOM_GAS_ESTIMATE, CUSTOM_GAS_ESTIMATE,
GAS_RECOMMENDATIONS, GAS_RECOMMENDATIONS,
EDIT_GAS_MODES, EDIT_GAS_MODES,
PRIORITY_LEVELS,
} from '../../../shared/constants/gas'; } from '../../../shared/constants/gas';
import { GAS_FORM_ERRORS } from '../../helpers/constants/gas'; import { GAS_FORM_ERRORS } from '../../helpers/constants/gas';
import { areDappSuggestedAndTxParamGasFeesTheSame } from '../../helpers/utils/confirm-tx.util';
import { import {
checkNetworkAndAccountSupports1559, checkNetworkAndAccountSupports1559,
getAdvancedInlineGasShown, getAdvancedInlineGasShown,
@ -106,10 +106,10 @@ export function useGasFeeInputs(
}); });
const [estimateUsed, setEstimateUsed] = useState(() => { const [estimateUsed, setEstimateUsed] = useState(() => {
if (areDappSuggestedAndTxParamGasFeesTheSame(transaction)) { if (estimateToUse) {
return 'dappSuggested'; return estimateToUse;
} }
return estimateToUse; return PRIORITY_LEVELS.CUSTOM;
}); });
/** /**
@ -118,9 +118,7 @@ export function useGasFeeInputs(
* so that transaction is source of truth whenever possible. * so that transaction is source of truth whenever possible.
*/ */
useEffect(() => { useEffect(() => {
if (areDappSuggestedAndTxParamGasFeesTheSame(transaction)) { if (transaction?.userFeeLevel) {
setEstimateUsed('dappSuggested');
} else if (transaction?.userFeeLevel) {
setEstimateUsed(transaction?.userFeeLevel); setEstimateUsed(transaction?.userFeeLevel);
} }
}, [setEstimateUsed, transaction]); }, [setEstimateUsed, transaction]);
@ -219,11 +217,6 @@ export function useGasFeeInputs(
const { updateTransactionUsingGasFeeEstimates } = useTransactionFunctions({ const { updateTransactionUsingGasFeeEstimates } = useTransactionFunctions({
defaultEstimateToUse, defaultEstimateToUse,
gasLimit, gasLimit,
gasPrice,
maxFeePerGas,
maxPriorityFeePerGas,
gasFeeEstimates,
supportsEIP1559,
transaction, transaction,
}); });

@ -2,16 +2,12 @@ import { useCallback } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { PRIORITY_LEVELS } from '../../../shared/constants/gas'; import { PRIORITY_LEVELS } from '../../../shared/constants/gas';
import { import { decimalToHex } from '../../helpers/utils/conversions.util';
decGWEIToHexWEI,
decimalToHex,
} from '../../helpers/utils/conversions.util';
import { updateTransaction as updateTransactionFn } from '../../store/actions'; import { updateTransaction as updateTransactionFn } from '../../store/actions';
export const useTransactionFunctions = ({ export const useTransactionFunctions = ({
defaultEstimateToUse, defaultEstimateToUse,
gasLimit, gasLimit,
gasFeeEstimates,
transaction, transaction,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -23,9 +19,13 @@ export const useTransactionFunctions = ({
gasLimit: decimalToHex(gasLimit), gasLimit: decimalToHex(gasLimit),
estimateSuggested: defaultEstimateToUse, estimateSuggested: defaultEstimateToUse,
estimateUsed, estimateUsed,
maxFeePerGas,
maxPriorityFeePerGas,
}; };
if (maxFeePerGas) {
newGasSettings.maxFeePerGas = maxFeePerGas;
}
if (maxPriorityFeePerGas) {
newGasSettings.maxPriorityFeePerGas = maxPriorityFeePerGas;
}
const updatedTxMeta = { const updatedTxMeta = {
...transaction, ...transaction,
@ -49,23 +49,15 @@ export const useTransactionFunctions = ({
maxPriorityFeePerGas, maxPriorityFeePerGas,
} = transaction?.dappSuggestedGasFees; } = transaction?.dappSuggestedGasFees;
updateTransaction( updateTransaction(
PRIORITY_LEVELS.CUSTOM, PRIORITY_LEVELS.DAPP_SUGGESTED,
maxFeePerGas, maxFeePerGas,
maxPriorityFeePerGas, maxPriorityFeePerGas,
); );
} else { } else {
const { updateTransaction(gasFeeEstimateToUse);
suggestedMaxFeePerGas,
suggestedMaxPriorityFeePerGas,
} = gasFeeEstimates[gasFeeEstimateToUse];
updateTransaction(
gasFeeEstimateToUse,
decGWEIToHexWEI(suggestedMaxFeePerGas),
decGWEIToHexWEI(suggestedMaxPriorityFeePerGas),
);
} }
}, },
[gasFeeEstimates, transaction?.dappSuggestedGasFees, updateTransaction], [transaction?.dappSuggestedGasFees, updateTransaction],
); );
return { updateTransactionUsingGasFeeEstimates }; return { updateTransactionUsingGasFeeEstimates };

@ -514,22 +514,26 @@ export default class ConfirmTransactionBase extends Component {
</div> </div>
} }
subText={ subText={
!isMultiLayerFeeNetwork && !isMultiLayerFeeNetwork && (
t('editGasSubTextFee', [ <>
<b key="editGasSubTextFeeLabel">{t('editGasSubTextFeeLabel')}</b>, <b key="editGasSubTextFeeLabel">
<div {t('editGasSubTextFeeLabel')}
key="editGasSubTextFeeValue" </b>
className="confirm-page-container-content__currency-container" ,
> <div
{renderHeartBeatIfNotInTest()} key="editGasSubTextFeeValue"
<UserPreferencedCurrencyDisplay className="confirm-page-container-content__currency-container"
key="editGasSubTextFeeAmount" >
type={PRIMARY} {renderHeartBeatIfNotInTest()}
value={hexMaximumTransactionFee} <UserPreferencedCurrencyDisplay
hideLabel={!useNativeCurrencyAsPrimaryCurrency} key="editGasSubTextFeeAmount"
/> type={PRIMARY}
</div>, value={hexMaximumTransactionFee}
]) hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</div>
</>
)
} }
subTitle={ subTitle={
<> <>
@ -606,12 +610,14 @@ export default class ConfirmTransactionBase extends Component {
detailText={renderTotalDetailText()} detailText={renderTotalDetailText()}
detailTotal={renderTotalDetailTotal()} detailTotal={renderTotalDetailTotal()}
subTitle={t('transactionDetailGasTotalSubtitle')} subTitle={t('transactionDetailGasTotalSubtitle')}
subText={t('editGasSubTextAmount', [ subText={
<b key="editGasSubTextAmountLabel"> <>
{t('editGasSubTextAmountLabel')} <b key="editGasSubTextAmountLabel">
</b>, {t('editGasSubTextAmountLabel')}
renderTotalMaxAmount(), </b>
])} {renderTotalMaxAmount()}
</>
}
/> />
), ),
]} ]}

@ -92,34 +92,36 @@ const GasDetailsItem = ({
/> />
</div> </div>
} }
subText={t('editGasSubTextFee', [ subText={
<Box <>
key="editGasSubTextFeeLabel" <Box
display="inline-flex" key="editGasSubTextFeeLabel"
className={classNames('gas-details-item__gasfee-label', { display="inline-flex"
'gas-details-item__gas-fee-warning': estimateUsed === 'high', className={classNames('gas-details-item__gasfee-label', {
})} 'gas-details-item__gas-fee-warning': estimateUsed === 'high',
> })}
<Box marginRight={1}>
<b>
{estimateUsed === 'high' && '⚠ '}
<I18nValue messageKey="editGasSubTextFeeLabel" />
</b>
</Box>
<div
key="editGasSubTextFeeValue"
className="gas-details-item__currency-container"
> >
<HeartBeat /> <Box marginRight={1}>
<UserPreferencedCurrencyDisplay <b>
key="editGasSubTextFeeAmount" {estimateUsed === 'high' && '⚠ '}
type={PRIMARY} <I18nValue messageKey="editGasSubTextFeeLabel" />
value={hexMaximumTransactionFee} </b>
hideLabel={!useNativeCurrencyAsPrimaryCurrency} </Box>
/> <div
</div> key="editGasSubTextFeeValue"
</Box>, className="gas-details-item__currency-container"
])} >
<HeartBeat />
<UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount"
type={PRIMARY}
value={hexMaximumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</div>
</Box>
</>
}
subTitle={ subTitle={
<GasTiming <GasTiming
maxPriorityFeePerGas={hexWEIToDecGWEI( maxPriorityFeePerGas={hexWEIToDecGWEI(

Loading…
Cancel
Save