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

Improvements for multi-layer fee UX (#13547)

* Show fiat on confirm screen on multilayer-fee network

* Disable gas editing on optimism

* Fix send max mode on optimism

* Represent layer 2 gas fee as a single value

* Hide gas fee edit UI on optimism

* Improvement multilayer-fee-message styling

* Lint fix

* Fix locales

* Remove unnecessary code change

Co-authored-by: David Walsh <davidwalsh83@gmail.com>
This commit is contained in:
Dan J Miller 2022-03-29 16:51:45 -02:30 committed by GitHub
parent 7cb22bd883
commit 09512c7148
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 156 additions and 107 deletions

View File

@ -1408,9 +1408,6 @@
"lastConnected": {
"message": "Zuletzt verbunden"
},
"layer1Fees": {
"message": "Ebene 1 Gebühren"
},
"learmMoreAboutGas": {
"message": "Wollen Sie 1 $ über Gas?"
},

View File

@ -1405,9 +1405,6 @@
"lastConnected": {
"message": "Τελευταία Σύνδεση"
},
"layer1Fees": {
"message": "Τέλη επιπέδου 1"
},
"learmMoreAboutGas": {
"message": "Θέλετε να $1 για το τέλος συναλλαγής;"
},

View File

@ -1277,6 +1277,9 @@
"gasEstimatesUnavailableWarning": {
"message": "Our low, medium and high estimates are not available."
},
"gasFee": {
"message": "Gas Fee"
},
"gasLimit": {
"message": "Gas Limit"
},
@ -1629,9 +1632,6 @@
"lastConnected": {
"message": "Last Connected"
},
"layer1Fees": {
"message": "Layer 1 fees"
},
"learmMoreAboutGas": {
"message": "Want to $1 about gas?"
},

View File

@ -1454,9 +1454,6 @@
"lastConnected": {
"message": "Última conexión"
},
"layer1Fees": {
"message": "Cargos de capa 1"
},
"learmMoreAboutGas": {
"message": "¿Quiere $1 sobre el gas?"
},

View File

@ -1405,9 +1405,6 @@
"lastConnected": {
"message": "Dernière connexion"
},
"layer1Fees": {
"message": "Frais de couche 1 (L1)"
},
"learmMoreAboutGas": {
"message": "Souhaitez-vous $1 sur le carburant?"
},

View File

@ -1405,9 +1405,6 @@
"lastConnected": {
"message": "अंतिम बार कनेक्ट किया गया"
},
"layer1Fees": {
"message": "लेयर 1 शुल्क"
},
"learmMoreAboutGas": {
"message": "गैस के बारे में $1 चाहते हैं?"
},

View File

@ -1405,9 +1405,6 @@
"lastConnected": {
"message": "Terakhir Terhubung"
},
"layer1Fees": {
"message": "Biaya lapis 1"
},
"learmMoreAboutGas": {
"message": "Ingin $1 seputar gas?"
},

View File

@ -1405,9 +1405,6 @@
"lastConnected": {
"message": "前回の接続"
},
"layer1Fees": {
"message": "レイヤー1手数料"
},
"learmMoreAboutGas": {
"message": "ガスについて$1しますか?"
},

View File

@ -1405,9 +1405,6 @@
"lastConnected": {
"message": "마지막 연결"
},
"layer1Fees": {
"message": "레이어 1 요금"
},
"learmMoreAboutGas": {
"message": "가스에 대해 $1하시겠습니까?"
},

View File

@ -1438,9 +1438,6 @@
"lastConnected": {
"message": "Conectado pela última vez em"
},
"layer1Fees": {
"message": "Taxas de camada 1"
},
"learmMoreAboutGas": {
"message": "Quer $1 sobre o gás?"
},

View File

@ -1405,9 +1405,6 @@
"lastConnected": {
"message": "Последнее подключение"
},
"layer1Fees": {
"message": "Комиссии 1-го уровня"
},
"learmMoreAboutGas": {
"message": "Хотите $1 о газе?"
},

View File

@ -1405,9 +1405,6 @@
"lastConnected": {
"message": "Huling Kumonekta"
},
"layer1Fees": {
"message": "Layer 1 fees"
},
"learmMoreAboutGas": {
"message": "Gusto mo bang $1 ang tungkol sa gas?"
},

View File

@ -1405,9 +1405,6 @@
"lastConnected": {
"message": "Son Bağlanma"
},
"layer1Fees": {
"message": "Aşama 1 ücretleri"
},
"learmMoreAboutGas": {
"message": "Gaz hakkında $1 istiyor musunuz?"
},

View File

@ -1405,9 +1405,6 @@
"lastConnected": {
"message": "Đã kết nối lần cuối"
},
"layer1Fees": {
"message": "Phí Lớp 1"
},
"learmMoreAboutGas": {
"message": "Muốn $1 về gas?"
},

View File

@ -1405,9 +1405,6 @@
"lastConnected": {
"message": "最后连接"
},
"layer1Fees": {
"message": "1层费用"
},
"learmMoreAboutGas": {
"message": "想要有关燃料$1"
},

View File

@ -7,7 +7,7 @@ import FormField from '../../ui/form-field';
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas';
import { getGasFormErrorText } from '../../../helpers/constants/gas';
import { getIsGasEstimatesLoading } from '../../../ducks/metamask/metamask';
import { getNetworkSupportsSettingGasPrice } from '../../../selectors';
import { getNetworkSupportsSettingGasFees } from '../../../selectors';
export default function AdvancedGasControls({
gasEstimateType,
@ -35,8 +35,8 @@ export default function AdvancedGasControls({
gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE ||
isGasEstimatesLoading);
const networkSupportsSettingGasPrice = useSelector(
getNetworkSupportsSettingGasPrice,
const networkSupportsSettingGasFees = useSelector(
getNetworkSupportsSettingGasFees,
);
return (
@ -55,6 +55,7 @@ export default function AdvancedGasControls({
tooltipText={t('editGasLimitTooltip')}
value={gasLimit}
allowDecimals={false}
disabled={!networkSupportsSettingGasFees}
numeric
/>
{showFeeMarketFields ? (
@ -111,7 +112,7 @@ export default function AdvancedGasControls({
? getGasFormErrorText(gasErrors.gasPrice, t)
: null
}
disabled={!networkSupportsSettingGasPrice}
disabled={!networkSupportsSettingGasFees}
/>
</>
)}

View File

@ -45,6 +45,7 @@
@import 'menu-bar/index';
@import 'modal/index';
@import 'modals/index';
@import 'multilayer-fee-message/index';
@import 'multiple-notifications/index';
@import 'network-display/index';
@import 'permission-page-container/index';

View File

@ -20,12 +20,12 @@ export default class AdvancedGasInputs extends Component {
customGasLimitMessage: PropTypes.string,
minimumGasLimit: PropTypes.number,
customPriceIsExcessive: PropTypes.bool,
networkSupportsSettingGasPrice: PropTypes.bool,
networkSupportsSettingGasFees: PropTypes.bool,
};
static defaultProps = {
customPriceIsExcessive: false,
networkSupportsSettingGasPrice: true,
networkSupportsSettingGasFees: true,
};
constructor(props) {
@ -202,10 +202,14 @@ export default class AdvancedGasInputs extends Component {
customGasLimitMessage,
minimumGasLimit,
customPriceIsExcessive,
networkSupportsSettingGasPrice,
networkSupportsSettingGasFees,
} = this.props;
const { gasPrice, gasLimit } = this.state;
if (!networkSupportsSettingGasFees) {
return null;
}
const {
errorText: gasPriceErrorText,
errorType: gasPriceErrorType,
@ -252,7 +256,7 @@ export default class AdvancedGasInputs extends Component {
onChange: this.onChangeGasPrice,
errorComponent: gasPriceErrorComponent,
errorType: gasPriceErrorType,
disabled: !networkSupportsSettingGasPrice,
disabled: !networkSupportsSettingGasFees,
})}
{this.renderGasInput({
label: this.context.t('gasLimit'),
@ -263,6 +267,7 @@ export default class AdvancedGasInputs extends Component {
errorComponent: gasLimitErrorComponent,
customMessageComponent: gasLimitCustomMessageComponent,
errorType: gasLimitErrorType,
disabled: !networkSupportsSettingGasFees,
})}
</div>
);

View File

@ -4,7 +4,7 @@ import {
decimalToHex,
hexWEIToDecGWEI,
} from '../../../../helpers/utils/conversions.util';
import { getNetworkSupportsSettingGasPrice } from '../../../../selectors/selectors';
import { getNetworkSupportsSettingGasFees } from '../../../../selectors/selectors';
import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants';
import AdvancedGasInputs from './advanced-gas-inputs.component';
@ -22,7 +22,7 @@ function convertMinimumGasLimitForInputs(minimumGasLimit = MIN_GAS_LIMIT_DEC) {
function mapStateToProps(state) {
return {
networkSupportsSettingGasPrice: getNetworkSupportsSettingGasPrice(state),
networkSupportsSettingGasFees: getNetworkSupportsSettingGasFees(state),
};
}

View File

@ -0,0 +1,3 @@
.multi-layer-fee-message {
padding-top: 24px;
}

View File

@ -2,7 +2,9 @@ import React, { useContext, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { captureException } from '@sentry/browser';
import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component';
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display';
import fetchEstimatedL1Fee from '../../../helpers/utils/optimism/fetchEstimatedL1Fee';
import { SECONDARY } from '../../../helpers/constants/common';
import { I18nContext } from '../../../contexts/i18n';
import { sumHexes } from '../../../helpers/utils/transactions.util';
import {
@ -21,23 +23,26 @@ export default function MultilayerFeeMessage({
const [fetchedLayer1Total, setLayer1Total] = useState(null);
let layer1Total = 'unknown';
let layer1TotalBN;
if (fetchedLayer1Total !== null) {
const layer1TotalBN = toBigNumber.hex(fetchedLayer1Total);
layer1TotalBN = toBigNumber.hex(fetchedLayer1Total);
layer1Total = `${toNormalizedDenomination
.WEI(layer1TotalBN)
.toString(10)} ${nativeCurrency}`;
.toFixed(12)} ${nativeCurrency}`;
}
const feeTotal = sumHexes(layer2fee || '0x0', fetchedLayer1Total || '0x0');
const totalInWeiHex = sumHexes(
layer2fee || '0x0',
fetchedLayer1Total || '0x0',
feeTotal || '0x0',
transaction.txParams.value || '0x0',
);
const totalBN = toBigNumber.hex(totalInWeiHex);
const totalInEth = `${toNormalizedDenomination
.WEI(totalBN)
.toString(10)} ${nativeCurrency}`;
.toFixed(12)} ${nativeCurrency}`;
useEffect(() => {
const getEstimatedL1Fee = async () => {
@ -52,12 +57,31 @@ export default function MultilayerFeeMessage({
getEstimatedL1Fee();
}, [transaction]);
const feeTotalInFiat = (
<UserPreferencedCurrencyDisplay
type={SECONDARY}
value={feeTotal}
showFiat
hideLabel
/>
);
const totalInFiat = (
<UserPreferencedCurrencyDisplay
type={SECONDARY}
value={totalInWeiHex}
showFiat
hideLabel
/>
);
return (
<>
<div className="multi-layer-fee-message">
<TransactionDetailItem
key="total-item"
detailTitle={t('layer1Fees')}
detailTitle={t('gasFee')}
detailTotal={layer1Total}
detailText={feeTotalInFiat}
noBold={plainStyle}
flexWidthValues={plainStyle}
/>
@ -65,11 +89,12 @@ export default function MultilayerFeeMessage({
key="total-item"
detailTitle={t('total')}
detailTotal={totalInEth}
detailText={totalInFiat}
subTitle={t('transactionDetailMultiLayerTotalSubtitle')}
noBold={plainStyle}
flexWidthValues={plainStyle}
/>
</>
</div>
);
}

View File

@ -12,12 +12,14 @@ export default function UserPreferencedCurrencyDisplay({
numberOfDecimals: propsNumberOfDecimals,
showEthLogo,
type,
showFiat,
...restProps
}) {
const { currency, numberOfDecimals } = useUserPreferencedCurrency(type, {
ethNumberOfDecimals,
fiatNumberOfDecimals,
numberOfDecimals: propsNumberOfDecimals,
showFiatOverride: showFiat,
});
const prefixComponent = useMemo(() => {
return (
@ -65,4 +67,5 @@ UserPreferencedCurrencyDisplay.propTypes = {
PropTypes.string,
PropTypes.number,
]),
showFiat: PropTypes.boolean,
};

View File

@ -44,6 +44,7 @@ import {
getUseTokenDetection,
getTokenList,
getAddressBookEntryOrAccountName,
getIsMultiLayerFeeNetwork,
} from '../../selectors';
import {
disconnectGasFeeEstimatePoller,
@ -91,6 +92,9 @@ import {
isBurnAddress,
isValidHexAddress,
} from '../../../shared/modules/hexstring-utils';
import { sumHexes } from '../../helpers/utils/transactions.util';
import fetchEstimatedL1Fee from '../../helpers/utils/optimism/fetchEstimatedL1Fee';
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
import {
ERC20,
@ -192,6 +196,7 @@ async function estimateGasLimitForSend({
data,
isNonStandardEthChain,
chainId,
gasLimit,
...options
}) {
let isSimpleSendOnNonStandardNetwork = false;
@ -313,12 +318,14 @@ async function estimateGasLimitForSend({
error.message.includes('Transaction execution error.') ||
error.message.includes(
'gas required exceeds allowance or always failing transaction',
);
) ||
(CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId] &&
error.message.includes('gas required exceeds allowance'));
if (simulationFailed) {
const estimateWithBuffer = addGasBuffer(
paramsForGasEstimate.gas,
paramsForGasEstimate?.gas ?? gasLimit,
blockGasLimit,
1.5,
bufferMultiplier,
);
return addHexPrefix(estimateWithBuffer);
}
@ -362,9 +369,29 @@ export const computeEstimatedGasLimit = createAsyncThunk(
const state = thunkApi.getState();
const { send, metamask } = state;
const unapprovedTxs = getUnapprovedTxs(state);
const isMultiLayerFeeNetwork = getIsMultiLayerFeeNetwork(state);
const transaction = unapprovedTxs[send.draftTransaction.id];
const isNonStandardEthChain = getIsNonStandardEthChain(state);
const chainId = getCurrentChainId(state);
let layer1GasTotal;
if (isMultiLayerFeeNetwork) {
layer1GasTotal = await fetchEstimatedL1Fee(global.eth, {
txParams: {
gasPrice: send.gas.gasPrice,
gas: send.gas.gasLimit,
to: send.recipient.address?.toLowerCase(),
value:
send.amount.mode === 'MAX'
? send.account.balance
: send.amount.value,
from: send.account.address,
data: send.draftTransaction.userInputHexData,
type: '0x0',
},
});
}
if (
send.stage !== SEND_STAGES.EDIT ||
!transaction.dappSuggestedGasFees?.gas ||
@ -380,10 +407,12 @@ export const computeEstimatedGasLimit = createAsyncThunk(
data: send.draftTransaction.userInputHexData,
isNonStandardEthChain,
chainId,
gasLimit: send.gas.gasLimit,
});
await thunkApi.dispatch(setCustomGasLimit(gasLimit));
return {
gasLimit,
layer1GasTotal,
};
}
return null;
@ -651,6 +680,10 @@ export const initialState = {
// Warning to display on the address field
warning: null,
},
multiLayerFees: {
// Layer 1 gas fee total on multi-layer fee networks
layer1GasTotal: '0x0',
},
};
const slice = createSlice({
@ -697,9 +730,13 @@ const slice = createSlice({
multiplierBase: 10,
});
} else {
const _gasTotal = sumHexes(
state.gas.gasTotal || '0x0',
state.multiLayerFees?.layer1GasTotal || '0x0',
);
amount = subtractCurrencies(
addHexPrefix(state.asset.balance),
addHexPrefix(state.gas.gasTotal),
addHexPrefix(_gasTotal),
{
toNumericBase: 'hex',
aBase: 16,
@ -881,6 +918,21 @@ const slice = createSlice({
// Record the latest gasPriceEstimate for future comparisons
state.gas.gasPriceEstimate = addHexPrefix(gasPriceEstimate);
},
/**
* sets the layer 1 fees total (for a multi-layer fee network)
*
* @param state
* @param action
*/
updateLayer1Fees: (state, action) => {
state.multiLayerFees.layer1GasTotal = action.payload;
if (
state.amount.mode === AMOUNT_MODES.MAX &&
state.asset.type === ASSET_TYPES.NATIVE
) {
slice.caseReducers.updateAmountToMax(state);
}
},
/**
* sets the amount mode to the provided value as long as it is one of the
* supported modes (MAX|INPUT)
@ -1319,6 +1371,11 @@ const slice = createSlice({
payload: action.payload.gasLimit,
});
}
if (action.payload?.layer1GasTotal) {
slice.caseReducers.updateLayer1Fees(state, {
payload: action.payload.layer1GasTotal,
});
}
})
.addCase(computeEstimatedGasLimit.rejected, (state) => {
// If gas estimation fails, we should set the loading state to false,

View File

@ -44,7 +44,7 @@ export function useUserPreferencedCurrency(type, opts = {}) {
getPreferences,
shallowEqual,
);
const showFiat = useSelector(getShouldShowFiat);
const showFiat = useSelector(getShouldShowFiat) || opts.showFiatOverride;
const currentCurrency = useSelector(getCurrentCurrency);
let currency, numberOfDecimals;

View File

@ -449,9 +449,7 @@ export default class ConfirmTransactionBase extends Component {
detailTitle={
txData.dappSuggestedGasFees ? (
<>
{isMultiLayerFeeNetwork
? t('transactionDetailLayer2GasHeading')
: t('transactionDetailGasHeading')}
{t('transactionDetailGasHeading')}
<InfoTooltip
contentText={t('transactionDetailDappGasTooltip')}
position="top"
@ -461,9 +459,7 @@ export default class ConfirmTransactionBase extends Component {
</>
) : (
<>
{isMultiLayerFeeNetwork
? t('transactionDetailLayer2GasHeading')
: t('transactionDetailGasHeading')}
{t('transactionDetailGasHeading')}
<InfoTooltip
contentText={
<>
@ -492,16 +488,14 @@ export default class ConfirmTransactionBase extends Component {
)
}
detailText={
!isMultiLayerFeeNetwork && (
<div className="confirm-page-container-content__currency-container">
{renderHeartBeatIfNotInTest()}
<UserPreferencedCurrencyDisplay
type={SECONDARY}
value={hexMinimumTransactionFee}
hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)}
/>
</div>
)
<div className="confirm-page-container-content__currency-container test">
{renderHeartBeatIfNotInTest()}
<UserPreferencedCurrencyDisplay
type={SECONDARY}
value={hexMinimumTransactionFee}
hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)}
/>
</div>
}
detailTotal={
<div className="confirm-page-container-content__currency-container">
@ -510,30 +504,28 @@ export default class ConfirmTransactionBase extends Component {
type={PRIMARY}
value={hexMinimumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
numberOfDecimals={isMultiLayerFeeNetwork ? 18 : 6}
numberOfDecimals={6}
/>
</div>
}
subText={
!isMultiLayerFeeNetwork && (
<>
<strong key="editGasSubTextFeeLabel">
{t('editGasSubTextFeeLabel')}
</strong>
<div
key="editGasSubTextFeeValue"
className="confirm-page-container-content__currency-container"
>
{renderHeartBeatIfNotInTest()}
<UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount"
type={PRIMARY}
value={hexMaximumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</div>
</>
)
<>
<strong key="editGasSubTextFeeLabel">
{t('editGasSubTextFeeLabel')}
</strong>
<div
key="editGasSubTextFeeValue"
className="confirm-page-container-content__currency-container"
>
{renderHeartBeatIfNotInTest()}
<UserPreferencedCurrencyDisplay
key="editGasSubTextFeeAmount"
type={PRIMARY}
value={hexMaximumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
/>
</div>
</>
}
subTitle={
<>
@ -596,11 +588,15 @@ export default class ConfirmTransactionBase extends Component {
disabled={isDisabled()}
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
onEdit={
renderSimulationFailureWarning ? null : () => this.handleEditGas()
renderSimulationFailureWarning || isMultiLayerFeeNetwork
? null
: () => this.handleEditGas()
}
rows={[
renderSimulationFailureWarning && simulationFailureWarning(),
!renderSimulationFailureWarning && renderGasDetailsItem(),
!renderSimulationFailureWarning &&
!isMultiLayerFeeNetwork &&
renderGasDetailsItem(),
!renderSimulationFailureWarning && isMultiLayerFeeNetwork && (
<MultiLayerFeeMessage
transaction={txData}

View File

@ -866,7 +866,7 @@ export function getIsOptimism(state) {
);
}
export function getNetworkSupportsSettingGasPrice(state) {
export function getNetworkSupportsSettingGasFees(state) {
return !getIsOptimism(state);
}