Support for Layer 2 networks with transaction fees on both layers (#12658)

* Support for Layer 2 networks with transaction fees on both layers

* Use  variable name in transaction-breakdown

* Add comment on code source to ui/helpers/utils/optimism/fetchEstimatedL1Fee.js

* Fix unit tests

* Ensure values passed to  are defined

* Fix activity log
feature/default_network_editable
Dan J Miller 3 years ago committed by GitHub
parent 0a8f94af81
commit 9fa15dda6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      app/_locales/en/messages.json
  2. 7
      lavamoat/browserify/policy.json
  3. 5
      package.json
  4. 2
      shared/modules/conversion.utils.js
  5. 6
      ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js
  6. 4
      ui/components/app/advanced-gas-controls/advanced-gas-controls.test.js
  7. 12
      ui/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js
  8. 13
      ui/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js
  9. 2
      ui/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.test.js
  10. 4
      ui/components/app/gas-customization/advanced-gas-inputs/index.scss
  11. 1
      ui/components/app/multilayer-fee-message/index.js
  12. 70
      ui/components/app/multilayer-fee-message/multi-layer-fee-message.js
  13. 2
      ui/components/app/transaction-activity-log/transaction-activity-log.util.js
  14. 37
      ui/components/app/transaction-breakdown/transaction-breakdown.component.js
  15. 20
      ui/components/app/transaction-breakdown/transaction-breakdown.container.js
  16. 33
      ui/helpers/utils/optimism/buildUnserializedTransaction.js
  17. 28
      ui/helpers/utils/optimism/buildUnserializedTransaction.test.js
  18. 24
      ui/helpers/utils/optimism/fetchEstimatedL1Fee.js
  19. 101
      ui/pages/confirm-transaction-base/confirm-transaction-base.component.js
  20. 4
      ui/pages/confirm-transaction-base/confirm-transaction-base.container.js
  21. 17
      ui/selectors/selectors.js
  22. 822
      yarn.lock

@ -1265,6 +1265,9 @@
"lastConnected": { "lastConnected": {
"message": "Last Connected" "message": "Last Connected"
}, },
"layer1Fees": {
"message": "Layer 1 fees"
},
"learnMore": { "learnMore": {
"message": "Learn more" "message": "Learn more"
}, },
@ -2825,6 +2828,12 @@
"transactionDetailGasTotalSubtitle": { "transactionDetailGasTotalSubtitle": {
"message": "Amount + gas fee" "message": "Amount + gas fee"
}, },
"transactionDetailLayer2GasHeading": {
"message": "Layer 2 gas fee"
},
"transactionDetailMultiLayerTotalSubtitle": {
"message": "Amount + fees"
},
"transactionDropped": { "transactionDropped": {
"message": "Transaction dropped at $2." "message": "Transaction dropped at $2."
}, },
@ -2843,6 +2852,15 @@
"transactionHistoryBaseFee": { "transactionHistoryBaseFee": {
"message": "Base Fee (GWEI)" "message": "Base Fee (GWEI)"
}, },
"transactionHistoryL1GasLabel": {
"message": "Total L1 gas fee"
},
"transactionHistoryL2GasLimitLabel": {
"message": "L2 gas limit`"
},
"transactionHistoryL2GasPriceLabel": {
"message": "L2 gas price`"
},
"transactionHistoryMaxFeePerGas": { "transactionHistoryMaxFeePerGas": {
"message": "Max Fee Per Gas" "message": "Max Fee Per Gas"
}, },

@ -84,6 +84,12 @@
"multihashes": true "multihashes": true
} }
}, },
"@eth-optimism/contracts": {
"packages": {
"@ethersproject/abstract-provider": true,
"ethers": true
}
},
"@ethereumjs/common": { "@ethereumjs/common": {
"packages": { "packages": {
"buffer": true, "buffer": true,
@ -319,6 +325,7 @@
"@ethersproject/bignumber": true, "@ethersproject/bignumber": true,
"@ethersproject/bytes": true, "@ethersproject/bytes": true,
"@ethersproject/keccak256": true, "@ethersproject/keccak256": true,
"@ethersproject/logger": true,
"@ethersproject/sha2": true, "@ethersproject/sha2": true,
"@ethersproject/strings": true "@ethersproject/strings": true
} }

@ -98,6 +98,7 @@
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.5.5",
"@download/blockies": "^1.0.3", "@download/blockies": "^1.0.3",
"@ensdomains/content-hash": "^2.5.6", "@ensdomains/content-hash": "^2.5.6",
"@eth-optimism/contracts": "0.0.0-2021919175625",
"@ethereumjs/common": "^2.3.1", "@ethereumjs/common": "^2.3.1",
"@ethereumjs/tx": "^3.2.1", "@ethereumjs/tx": "^3.2.1",
"@formatjs/intl-relativetimeformat": "^5.2.6", "@formatjs/intl-relativetimeformat": "^5.2.6",
@ -367,7 +368,9 @@
"github:assemblyscript/assemblyscript": false, "github:assemblyscript/assemblyscript": false,
"tiny-secp256k1": false, "tiny-secp256k1": false,
"@lavamoat/preinstall-always-fail": false, "@lavamoat/preinstall-always-fail": false,
"fsevents": false "fsevents": false,
"node-hid": false,
"usb": false
} }
} }
} }

@ -289,4 +289,6 @@ export {
toNegative, toNegative,
subtractCurrencies, subtractCurrencies,
decGWEIToHexWEI, decGWEIToHexWEI,
toBigNumber,
toNormalizedDenomination,
}; };

@ -7,6 +7,7 @@ import FormField from '../../ui/form-field';
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas'; import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas';
import { getGasFormErrorText } from '../../../helpers/constants/gas'; import { getGasFormErrorText } from '../../../helpers/constants/gas';
import { getIsGasEstimatesLoading } from '../../../ducks/metamask/metamask'; import { getIsGasEstimatesLoading } from '../../../ducks/metamask/metamask';
import { getNetworkSupportsSettingGasPrice } from '../../../selectors/selectors';
export default function AdvancedGasControls({ export default function AdvancedGasControls({
gasEstimateType, gasEstimateType,
@ -34,6 +35,10 @@ export default function AdvancedGasControls({
gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE || gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE ||
isGasEstimatesLoading); isGasEstimatesLoading);
const networkSupportsSettingGasPrice = useSelector(
getNetworkSupportsSettingGasPrice,
);
return ( return (
<div className="advanced-gas-controls"> <div className="advanced-gas-controls">
<FormField <FormField
@ -106,6 +111,7 @@ export default function AdvancedGasControls({
? getGasFormErrorText(gasErrors.gasPrice, t) ? getGasFormErrorText(gasErrors.gasPrice, t)
: null : null
} }
disabled={!networkSupportsSettingGasPrice}
/> />
</> </>
)} )}

@ -7,7 +7,9 @@ import { renderWithProvider } from '../../../../test/jest/rendering';
import AdvancedGasControls from './advanced-gas-controls.component'; import AdvancedGasControls from './advanced-gas-controls.component';
const renderComponent = (props) => { const renderComponent = (props) => {
const store = configureMockStore([])({ metamask: { identities: [] } }); const store = configureMockStore([])({
metamask: { identities: [], provider: {} },
});
return renderWithProvider(<AdvancedGasControls {...props} />, store); return renderWithProvider(<AdvancedGasControls {...props} />, store);
}; };

@ -20,10 +20,12 @@ export default class AdvancedGasInputs extends Component {
customGasLimitMessage: PropTypes.string, customGasLimitMessage: PropTypes.string,
minimumGasLimit: PropTypes.number, minimumGasLimit: PropTypes.number,
customPriceIsExcessive: PropTypes.bool, customPriceIsExcessive: PropTypes.bool,
networkSupportsSettingGasPrice: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
customPriceIsExcessive: false, customPriceIsExcessive: false,
networkSupportsSettingGasPrice: true,
}; };
constructor(props) { constructor(props) {
@ -131,6 +133,7 @@ export default class AdvancedGasInputs extends Component {
testId, testId,
customMessageComponent, customMessageComponent,
tooltipTitle, tooltipTitle,
disabled,
}) { }) {
return ( return (
<div className="advanced-gas-inputs__gas-edit-row"> <div className="advanced-gas-inputs__gas-edit-row">
@ -152,6 +155,7 @@ export default class AdvancedGasInputs extends Component {
min="0" min="0"
value={value} value={value}
onChange={onChange} onChange={onChange}
disabled={disabled}
data-testid={testId} data-testid={testId}
/> />
<div <div
@ -162,18 +166,22 @@ export default class AdvancedGasInputs extends Component {
errorType === 'error', errorType === 'error',
'advanced-gas-inputs__gas-edit-row__input--warning': 'advanced-gas-inputs__gas-edit-row__input--warning':
errorType === 'warning', errorType === 'warning',
'advanced-gas-inputs__gas-edit-row__input-arrows--hidden': disabled,
}, },
)} )}
> >
<div <div
className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap" className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap"
onClick={() => onChange({ target: { value: value + 1 } })} onClick={() =>
!disabled && onChange({ target: { value: value + 1 } })
}
> >
<i className="fa fa-sm fa-angle-up" /> <i className="fa fa-sm fa-angle-up" />
</div> </div>
<div <div
className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap" className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap"
onClick={() => onClick={() =>
!disabled &&
onChange({ target: { value: Math.max(value - 1, 0) } }) onChange({ target: { value: Math.max(value - 1, 0) } })
} }
> >
@ -194,6 +202,7 @@ export default class AdvancedGasInputs extends Component {
customGasLimitMessage, customGasLimitMessage,
minimumGasLimit, minimumGasLimit,
customPriceIsExcessive, customPriceIsExcessive,
networkSupportsSettingGasPrice,
} = this.props; } = this.props;
const { gasPrice, gasLimit } = this.state; const { gasPrice, gasLimit } = this.state;
@ -243,6 +252,7 @@ export default class AdvancedGasInputs extends Component {
onChange: this.onChangeGasPrice, onChange: this.onChangeGasPrice,
errorComponent: gasPriceErrorComponent, errorComponent: gasPriceErrorComponent,
errorType: gasPriceErrorType, errorType: gasPriceErrorType,
disabled: !networkSupportsSettingGasPrice,
})} })}
{this.renderGasInput({ {this.renderGasInput({
label: this.context.t('gasLimit'), label: this.context.t('gasLimit'),

@ -4,6 +4,7 @@ import {
decimalToHex, decimalToHex,
hexWEIToDecGWEI, hexWEIToDecGWEI,
} from '../../../../helpers/utils/conversions.util'; } from '../../../../helpers/utils/conversions.util';
import { getNetworkSupportsSettingGasPrice } from '../../../../selectors/selectors';
import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants'; import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants';
import AdvancedGasInputs from './advanced-gas-inputs.component'; import AdvancedGasInputs from './advanced-gas-inputs.component';
@ -19,7 +20,13 @@ function convertMinimumGasLimitForInputs(minimumGasLimit = MIN_GAS_LIMIT_DEC) {
return parseInt(minimumGasLimit, 10); return parseInt(minimumGasLimit, 10);
} }
const mergeProps = (stateProps, dispatchProps, ownProps) => { function mapStateToProps(state) {
return {
networkSupportsSettingGasPrice: getNetworkSupportsSettingGasPrice(state),
};
}
function mergeProps(stateProps, dispatchProps, ownProps) {
const { const {
customGasPrice, customGasPrice,
customGasLimit, customGasLimit,
@ -38,6 +45,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
updateCustomGasPrice(decGWEIToHexWEI(price)), updateCustomGasPrice(decGWEIToHexWEI(price)),
updateCustomGasLimit: (limit) => updateCustomGasLimit(decimalToHex(limit)), updateCustomGasLimit: (limit) => updateCustomGasLimit(decimalToHex(limit)),
}; };
}; }
export default connect(null, null, mergeProps)(AdvancedGasInputs); export default connect(mapStateToProps, null, mergeProps)(AdvancedGasInputs);

@ -20,7 +20,7 @@ describe('AdvancedGasInputs', () => {
minimumGasLimit: 21000, minimumGasLimit: 21000,
}; };
const store = configureStore({}); const store = configureStore({ metamask: { provider: {} } });
beforeEach(() => { beforeEach(() => {
clock = sinon.useFakeTimers(); clock = sinon.useFakeTimers();

@ -109,6 +109,10 @@
i { i {
font-size: $font-size-h8; font-size: $font-size-h8;
} }
&--hidden {
display: none;
}
} }
&__input-arrows--error { &__input-arrows--error {

@ -0,0 +1 @@
export { default } from './multi-layer-fee-message';

@ -0,0 +1,70 @@
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 fetchEstimatedL1Fee from '../../../helpers/utils/optimism/fetchEstimatedL1Fee';
import { I18nContext } from '../../../contexts/i18n';
import { sumHexes } from '../../../helpers/utils/transactions.util';
import {
toBigNumber,
toNormalizedDenomination,
} from '../../../../shared/modules/conversion.utils';
export default function MultilayerFeeMessage({ transaction, layer2fee }) {
const t = useContext(I18nContext);
const [fetchedLayer1Total, setLayer1Total] = useState(null);
let layer1Total = 'unknown';
if (fetchedLayer1Total !== null) {
const layer1TotalBN = toBigNumber.hex(fetchedLayer1Total);
layer1Total = `${toNormalizedDenomination
.WEI(layer1TotalBN)
.toString(10)} ETH`;
}
const totalInWeiHex = sumHexes(
layer2fee || '0x0',
fetchedLayer1Total || '0x0',
transaction.txParams.value || '0x0',
);
const totalBN = toBigNumber.hex(totalInWeiHex);
const totalInEth = `${toNormalizedDenomination
.WEI(totalBN)
.toString(10)} ETH`;
useEffect(() => {
const getEstimatedL1Fee = async () => {
try {
const result = await fetchEstimatedL1Fee(global.eth, transaction);
setLayer1Total(result);
} catch (e) {
captureException(e);
setLayer1Total(null);
}
};
getEstimatedL1Fee();
}, [transaction]);
return (
<>
<TransactionDetailItem
key="total-item"
detailTitle={t('layer1Fees')}
detailTotal={layer1Total}
/>
<TransactionDetailItem
key="total-item"
detailTitle={t('total')}
detailTotal={totalInEth}
subTitle={t('transactionDetailMultiLayerTotalSubtitle')}
/>
</>
);
}
MultilayerFeeMessage.propTypes = {
transaction: PropTypes.object,
layer2fee: PropTypes.string,
};

@ -89,7 +89,7 @@ export function getActivities(transaction, isFirstTransaction = false) {
// need to cache these values because the status update history events don't provide us with // need to cache these values because the status update history events don't provide us with
// the latest gas limit and gas price. // the latest gas limit and gas price.
cachedGasLimit = gas; cachedGasLimit = gas;
cachedGasPrice = eip1559Price || gasPrice || '0x0'; cachedGasPrice = eip1559Price || gasPrice || paramsGasPrice || '0x0';
if (isFirstTransaction) { if (isFirstTransaction) {
return acc.concat({ return acc.concat({

@ -33,6 +33,8 @@ export default class TransactionBreakdown extends PureComponent {
priorityFee: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), priorityFee: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
hexGasTotal: PropTypes.string, hexGasTotal: PropTypes.string,
isEIP1559Transaction: PropTypes.bool, isEIP1559Transaction: PropTypes.bool,
isMultiLayerFeeNetwork: PropTypes.bool,
l1HexGasTotal: PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
@ -57,6 +59,8 @@ export default class TransactionBreakdown extends PureComponent {
priorityFee, priorityFee,
hexGasTotal, hexGasTotal,
isEIP1559Transaction, isEIP1559Transaction,
isMultiLayerFeeNetwork,
l1HexGasTotal,
} = this.props; } = this.props;
return ( return (
<div className={classnames('transaction-breakdown', className)}> <div className={classnames('transaction-breakdown', className)}>
@ -77,7 +81,11 @@ export default class TransactionBreakdown extends PureComponent {
</span> </span>
</TransactionBreakdownRow> </TransactionBreakdownRow>
<TransactionBreakdownRow <TransactionBreakdownRow
title={`${t('gasLimit')} (${t('units')})`} title={
isMultiLayerFeeNetwork
? t('transactionHistoryL2GasLimitLabel')
: `${t('gasLimit')} (${t('units')})`
}
className="transaction-breakdown__row-title" className="transaction-breakdown__row-title"
> >
{typeof gas === 'undefined' ? ( {typeof gas === 'undefined' ? (
@ -127,7 +135,13 @@ export default class TransactionBreakdown extends PureComponent {
</TransactionBreakdownRow> </TransactionBreakdownRow>
) : null} ) : null}
{!isEIP1559Transaction && ( {!isEIP1559Transaction && (
<TransactionBreakdownRow title={t('advancedGasPriceTitle')}> <TransactionBreakdownRow
title={
isMultiLayerFeeNetwork
? t('transactionHistoryL2GasPriceLabel')
: t('advancedGasPriceTitle')
}
>
{typeof gasPrice === 'undefined' ? ( {typeof gasPrice === 'undefined' ? (
'?' '?'
) : ( ) : (
@ -182,11 +196,30 @@ export default class TransactionBreakdown extends PureComponent {
)} )}
</TransactionBreakdownRow> </TransactionBreakdownRow>
)} )}
{isMultiLayerFeeNetwork && (
<TransactionBreakdownRow title={t('transactionHistoryL1GasLabel')}>
<UserPreferencedCurrencyDisplay
className="transaction-breakdown__value"
data-testid="transaction-breakdown__l1-gas-total"
numberOfDecimals={18}
value={l1HexGasTotal}
type={PRIMARY}
/>
{showFiat && (
<UserPreferencedCurrencyDisplay
className="transaction-breakdown__value"
type={SECONDARY}
value={l1HexGasTotal}
/>
)}
</TransactionBreakdownRow>
)}
<TransactionBreakdownRow title={t('total')}> <TransactionBreakdownRow title={t('total')}>
<UserPreferencedCurrencyDisplay <UserPreferencedCurrencyDisplay
className="transaction-breakdown__value transaction-breakdown__value--eth-total" className="transaction-breakdown__value transaction-breakdown__value--eth-total"
type={PRIMARY} type={PRIMARY}
value={totalInHex} value={totalInHex}
numberOfDecimals={isMultiLayerFeeNetwork ? 18 : null}
/> />
{showFiat && ( {showFiat && (
<UserPreferencedCurrencyDisplay <UserPreferencedCurrencyDisplay

@ -1,17 +1,21 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getShouldShowFiat } from '../../../selectors'; import {
getShouldShowFiat,
getIsMultiLayerFeeNetwork,
} from '../../../selectors';
import { getNativeCurrency } from '../../../ducks/metamask/metamask'; import { getNativeCurrency } from '../../../ducks/metamask/metamask';
import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util'; import { getHexGasTotal } from '../../../helpers/utils/confirm-tx.util';
import { subtractHexes } from '../../../helpers/utils/conversions.util'; import { subtractHexes } from '../../../helpers/utils/conversions.util';
import { sumHexes } from '../../../helpers/utils/transactions.util'; import { sumHexes } from '../../../helpers/utils/transactions.util';
import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils'; import { isEIP1559Transaction } from '../../../../shared/modules/transaction.utils';
import TransactionBreakdown from './transaction-breakdown.component'; import TransactionBreakdown from './transaction-breakdown.component';
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
const { transaction, isTokenApprove } = ownProps; const { transaction, isTokenApprove } = ownProps;
const { const {
txParams: { gas, gasPrice, maxFeePerGas, value } = {}, txParams: { gas, gasPrice, maxFeePerGas, value } = {},
txReceipt: { gasUsed, effectiveGasPrice } = {}, txReceipt: { gasUsed, effectiveGasPrice, l1Fee: l1HexGasTotal } = {},
baseFeePerGas, baseFeePerGas,
} = transaction; } = transaction;
@ -33,7 +37,15 @@ const mapStateToProps = (state, ownProps) => {
usedGasPrice && usedGasPrice &&
getHexGasTotal({ gasLimit, gasPrice: usedGasPrice })) || getHexGasTotal({ gasLimit, gasPrice: usedGasPrice })) ||
'0x0'; '0x0';
const totalInHex = sumHexes(hexGasTotal, value);
let totalInHex = sumHexes(hexGasTotal, value);
const isMultiLayerFeeNetwork =
getIsMultiLayerFeeNetwork(state) && l1HexGasTotal !== undefined;
if (isMultiLayerFeeNetwork) {
totalInHex = sumHexes(totalInHex, l1HexGasTotal);
}
return { return {
nativeCurrency: getNativeCurrency(state), nativeCurrency: getNativeCurrency(state),
@ -48,6 +60,8 @@ const mapStateToProps = (state, ownProps) => {
priorityFee, priorityFee,
baseFee: baseFeePerGas, baseFee: baseFeePerGas,
isEIP1559Transaction: isEIP1559Transaction(transaction), isEIP1559Transaction: isEIP1559Transaction(transaction),
isMultiLayerFeeNetwork,
l1HexGasTotal,
}; };
}; };

@ -0,0 +1,33 @@
import { omit } from 'lodash';
import { BN, stripHexPrefix } from 'ethereumjs-util';
import Common, { Chain, Hardfork } from '@ethereumjs/common';
import { TransactionFactory } from '@ethereumjs/tx';
function buildTxParams(txMeta) {
return {
...omit(txMeta.txParams, 'gas'),
gasLimit: txMeta.txParams.gas,
};
}
function buildTransactionCommon(txMeta) {
// This produces a transaction whose information does not completely match an
// Optimism transaction — for instance, DEFAULT_CHAIN is still 'mainnet' and
// genesis points to the mainnet genesis, not the Optimism genesis — but
// considering that all we want to do is serialize a transaction, this works
// fine for our use case.
return Common.forCustomChain(Chain.Mainnet, {
chainId: new BN(stripHexPrefix(txMeta.chainId), 16),
networkId: new BN(txMeta.metamaskNetworkId, 10),
// Optimism only supports type-0 transactions; it does not support any of
// the newer EIPs since EIP-155. Source:
// <https://github.com/ethereum-optimism/optimism/blob/develop/specs/l2geth/transaction-types.md>
defaultHardfork: Hardfork.SpuriousDragon,
});
}
export default function buildUnserializedTransaction(txMeta) {
const txParams = buildTxParams(txMeta);
const common = buildTransactionCommon(txMeta);
return TransactionFactory.fromTxData(txParams, { common });
}

@ -0,0 +1,28 @@
import { BN } from 'ethereumjs-util';
import { times } from 'lodash';
import buildUnserializedTransaction from './buildUnserializedTransaction';
describe('buildUnserializedTransaction', () => {
it('returns a transaction that can be serialized and fed to an Optimism smart contract', () => {
const unserializedTransaction = buildUnserializedTransaction({
txParams: {
nonce: '0x0',
gasPrice: `0x${new BN('100').toString(16)}`,
gas: `0x${new BN('21000').toString(16)}`,
to: '0x0000000000000000000000000000000000000000',
value: `0x${new BN('10000000000000').toString(16)}`,
data: '0x0',
},
});
expect(unserializedTransaction).toMatchObject({
nonce: new BN('00', 16),
gasPrice: new BN('64', 16),
gasLimit: new BN('5208', 16),
to: expect.objectContaining({
buf: Buffer.from(times(20, 0)),
}),
value: new BN('09184e72a000', 16),
data: Buffer.from([0]),
});
});
});

@ -0,0 +1,24 @@
import * as ethers from 'ethers';
import * as optimismContracts from '@eth-optimism/contracts';
import buildUnserializedTransaction from './buildUnserializedTransaction';
// The code in this file is largely drawn from https://community.optimism.io/docs/developers/l2/new-fees.html#for-frontend-and-wallet-developers
function buildOVMGasPriceOracleContract(eth) {
const OVMGasPriceOracle = optimismContracts
.getContractFactory('OVM_GasPriceOracle')
.attach(optimismContracts.predeploys.OVM_GasPriceOracle);
const abi = JSON.parse(
OVMGasPriceOracle.interface.format(ethers.utils.FormatTypes.json),
);
return eth.contract(abi).at(OVMGasPriceOracle.address);
}
export default async function fetchEstimatedL1Fee(eth, txMeta) {
const contract = buildOVMGasPriceOracleContract(eth);
const serializedTransaction = buildUnserializedTransaction(
txMeta,
).serialize();
const result = await contract.getL1Fee(serializedTransaction);
return result?.[0]?.toString(16);
}

@ -36,6 +36,7 @@ import InfoTooltip from '../../components/ui/info-tooltip/info-tooltip';
import LoadingHeartBeat from '../../components/ui/loading-heartbeat'; import LoadingHeartBeat from '../../components/ui/loading-heartbeat';
import GasTiming from '../../components/app/gas-timing/gas-timing.component'; import GasTiming from '../../components/app/gas-timing/gas-timing.component';
import LedgerInstructionField from '../../components/app/ledger-instruction-field'; import LedgerInstructionField from '../../components/app/ledger-instruction-field';
import MultiLayerFeeMessage from '../../components/app/multilayer-fee-message';
import { import {
COLORS, COLORS,
@ -134,6 +135,7 @@ export default class ConfirmTransactionBase extends Component {
nativeCurrency: PropTypes.string, nativeCurrency: PropTypes.string,
supportsEIP1559: PropTypes.bool, supportsEIP1559: PropTypes.bool,
hardwareWalletRequiresConnection: PropTypes.bool, hardwareWalletRequiresConnection: PropTypes.bool,
isMultiLayerFeeNetwork: PropTypes.bool,
}; };
state = { state = {
@ -315,6 +317,7 @@ export default class ConfirmTransactionBase extends Component {
isMainnet, isMainnet,
showLedgerSteps, showLedgerSteps,
supportsEIP1559, supportsEIP1559,
isMultiLayerFeeNetwork,
} = this.props; } = this.props;
const { t } = this.context; const { t } = this.context;
@ -433,7 +436,9 @@ export default class ConfirmTransactionBase extends Component {
detailTitle={ detailTitle={
txData.dappSuggestedGasFees ? ( txData.dappSuggestedGasFees ? (
<> <>
{t('transactionDetailGasHeading')} {isMultiLayerFeeNetwork
? t('transactionDetailLayer2GasHeading')
: t('transactionDetailGasHeading')}
<InfoTooltip <InfoTooltip
contentText={t('transactionDetailDappGasTooltip')} contentText={t('transactionDetailDappGasTooltip')}
position="top" position="top"
@ -443,7 +448,9 @@ export default class ConfirmTransactionBase extends Component {
</> </>
) : ( ) : (
<> <>
{t('transactionDetailGasHeading')} {isMultiLayerFeeNetwork
? t('transactionDetailLayer2GasHeading')
: t('transactionDetailGasHeading')}
<InfoTooltip <InfoTooltip
contentText={ contentText={
<> <>
@ -473,14 +480,16 @@ export default class ConfirmTransactionBase extends Component {
} }
detailTitleColor={COLORS.BLACK} detailTitleColor={COLORS.BLACK}
detailText={ detailText={
<div className="confirm-page-container-content__currency-container"> !isMultiLayerFeeNetwork && (
{renderHeartBeatIfNotInTest()} <div className="confirm-page-container-content__currency-container">
<UserPreferencedCurrencyDisplay {renderHeartBeatIfNotInTest()}
type={SECONDARY} <UserPreferencedCurrencyDisplay
value={hexMinimumTransactionFee} type={SECONDARY}
hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)} value={hexMinimumTransactionFee}
/> hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)}
</div> />
</div>
)
} }
detailTotal={ detailTotal={
<div className="confirm-page-container-content__currency-container"> <div className="confirm-page-container-content__currency-container">
@ -489,26 +498,30 @@ export default class ConfirmTransactionBase extends Component {
type={PRIMARY} type={PRIMARY}
value={hexMinimumTransactionFee} value={hexMinimumTransactionFee}
hideLabel={!useNativeCurrencyAsPrimaryCurrency} hideLabel={!useNativeCurrencyAsPrimaryCurrency}
numberOfDecimals={isMultiLayerFeeNetwork ? 18 : 6}
/> />
</div> </div>
} }
subText={t('editGasSubTextFee', [ subText={
<b key="editGasSubTextFeeLabel"> !isMultiLayerFeeNetwork &&
{t('editGasSubTextFeeLabel')} t('editGasSubTextFee', [
</b>, <b key="editGasSubTextFeeLabel">
<div {t('editGasSubTextFeeLabel')}
key="editGasSubTextFeeValue" </b>,
className="confirm-page-container-content__currency-container" <div
> key="editGasSubTextFeeValue"
{renderHeartBeatIfNotInTest()} className="confirm-page-container-content__currency-container"
<UserPreferencedCurrencyDisplay >
key="editGasSubTextFeeAmount" {renderHeartBeatIfNotInTest()}
type={PRIMARY} <UserPreferencedCurrencyDisplay
value={hexMaximumTransactionFee} key="editGasSubTextFeeAmount"
hideLabel={!useNativeCurrencyAsPrimaryCurrency} type={PRIMARY}
/> value={hexMaximumTransactionFee}
</div>, hideLabel={!useNativeCurrencyAsPrimaryCurrency}
])} />
</div>,
])
}
subTitle={ subTitle={
<> <>
{txData.dappSuggestedGasFees ? ( {txData.dappSuggestedGasFees ? (
@ -537,19 +550,27 @@ export default class ConfirmTransactionBase extends Component {
} }
/> />
), ),
<TransactionDetailItem isMultiLayerFeeNetwork && (
key="total-item" <MultiLayerFeeMessage
detailTitle={t('total')} transaction={txData}
detailText={renderTotalDetailText()} layer2fee={hexMinimumTransactionFee}
detailTotal={renderTotalDetailTotal()} />
subTitle={t('transactionDetailGasTotalSubtitle')} ),
subText={t('editGasSubTextAmount', [ !isMultiLayerFeeNetwork && (
<b key="editGasSubTextAmountLabel"> <TransactionDetailItem
{t('editGasSubTextAmountLabel')} key="total-item"
</b>, detailTitle={t('total')}
renderTotalMaxAmount(), detailText={renderTotalDetailText()}
])} detailTotal={renderTotalDetailTotal()}
/>, subTitle={t('transactionDetailGasTotalSubtitle')}
subText={t('editGasSubTextAmount', [
<b key="editGasSubTextAmountLabel">
{t('editGasSubTextAmountLabel')}
</b>,
renderTotalMaxAmount(),
])}
/>
),
]} ]}
/> />
{nonceField} {nonceField}

@ -31,6 +31,7 @@ import {
doesAddressRequireLedgerHidConnection, doesAddressRequireLedgerHidConnection,
getUseTokenDetection, getUseTokenDetection,
getTokenList, getTokenList,
getIsMultiLayerFeeNetwork,
} from '../../selectors'; } from '../../selectors';
import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { import {
@ -179,6 +180,8 @@ const mapStateToProps = (state, ownProps) => {
fromAddress, fromAddress,
); );
const isMultiLayerFeeNetwork = getIsMultiLayerFeeNetwork(state);
return { return {
balance, balance,
fromAddress, fromAddress,
@ -227,6 +230,7 @@ const mapStateToProps = (state, ownProps) => {
showLedgerSteps: fromAddressIsLedger, showLedgerSteps: fromAddressIsLedger,
nativeCurrency, nativeCurrency,
hardwareWalletRequiresConnection, hardwareWalletRequiresConnection,
isMultiLayerFeeNetwork,
}; };
}; };

@ -5,6 +5,8 @@ import {
TEST_CHAINS, TEST_CHAINS,
NETWORK_TYPE_RPC, NETWORK_TYPE_RPC,
NATIVE_CURRENCY_TOKEN_IMAGE_MAP, NATIVE_CURRENCY_TOKEN_IMAGE_MAP,
OPTIMISM_CHAIN_ID,
OPTIMISM_TESTNET_CHAIN_ID,
} from '../../shared/constants/network'; } from '../../shared/constants/network';
import { import {
KEYRING_TYPES, KEYRING_TYPES,
@ -701,3 +703,18 @@ export function getProvider(state) {
export function getFrequentRpcListDetail(state) { export function getFrequentRpcListDetail(state) {
return state.metamask.frequentRpcListDetail; return state.metamask.frequentRpcListDetail;
} }
export function getIsOptimism(state) {
return (
getCurrentChainId(state) === OPTIMISM_CHAIN_ID ||
getCurrentChainId(state) === OPTIMISM_TESTNET_CHAIN_ID
);
}
export function getNetworkSupportsSettingGasPrice(state) {
return !getIsOptimism(state);
}
export function getIsMultiLayerFeeNetwork(state) {
return getIsOptimism(state);
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save