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

Switch gas price estimation in swaps to metaswap-api /gasPrices (#9599)

Adds swaps-gas-customization-modal and utilize in swaps

Remove swaps specific code from gas-modal-page-container/

Remove slow estimate data from swaps-gas-customization-modal.container

Use average as lower safe price limit in swaps-gas-customization-modal

Lint fix

Fix up unit tests

Update ui/app/ducks/swaps/swaps.js

Co-authored-by: Mark Stacey <markjstacey@gmail.com>

Remove stale properties from gas-modal-page-container.component.js

Replace use of isCustomPrice safe with isCustomSwapsGasPriceSafe, in swaps-gas-customization-modal

Remove use of averageIsSafe in isCustomPriceSafe function

Stop calling resetCustomGasState in swaps

Refactor 'setter' type actions and creators to 'event based', for swaps slice custom gas logic

Replace use of advanced-tab-content.component with advanceGasInputs in swaps gas customization component

Add validation for the gasPrices endpoint

swaps custom gas price should be considered safe if >= to average

Update renderDataSummary unit test

Lint fix

Remove customOnHideOpts for swapsGasCustomizationModal in modal.js

Better handling for swaps gas price loading and failure states

Improve semantics: isCustomSwapsGasPriceSafe renamed to isCustomSwapsGasPriceUnSafe

Mutate state directly in swaps gas slice reducer

Remove unused params

More reliable tracking of speed setting for Gas Fees Changed metrics event

Lint fix

Throw error when fetchSwapsGasPrices response is invalid

add disableSave and customTotalSupplement to swaps-gas-customization container return

Update ui/app/ducks/swaps/swaps.js

Co-authored-by: Mark Stacey <markjstacey@gmail.com>

Improve error handling in fetchMetaSwapsGasPriceEstimates

Remove metricsEvent from swaps-gas-customization-modal context

Base check of gas speed type in swaps-gas-customization-modal on gasEstimateType

Improve naming of variable and functions use to set customPriceIsSafe prop of AdvancedGasInputs in swaps-gas-customization-modal

Simplify sinon spy/stub code in gas-price-button-group-component.test.js

Remove unnecessary getSwapsFallbackGasPrice call in swaps-gas-customization-modal

Remove use of getSwapsTradeTxParams and clean up related gas price logic in swaps

Improve validator of SWAP_GAS_PRICE_VALIDATOR

Ensure default tradeValue
This commit is contained in:
Dan J Miller 2020-11-04 12:44:08 -03:30 committed by GitHub
parent ad838df3e6
commit a0d7c71011
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 788 additions and 292 deletions

View File

@ -25,7 +25,7 @@ export default class AdvancedTabContent extends Component {
isSpeedUp: PropTypes.bool, isSpeedUp: PropTypes.bool,
isEthereumNetwork: PropTypes.bool, isEthereumNetwork: PropTypes.bool,
customGasLimitMessage: PropTypes.string, customGasLimitMessage: PropTypes.string,
minimumGasLimit: PropTypes.number.isRequired, minimumGasLimit: PropTypes.number,
} }
renderDataSummary(transactionFee, timeRemaining) { renderDataSummary(transactionFee, timeRemaining) {

View File

@ -103,7 +103,7 @@ describe('AdvancedTabContent Component', function () {
const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall( const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall(
0, 0,
).args ).args
assert.deepEqual(renderDataSummaryArgs, ['$0.25', 21500]) assert.deepEqual(renderDataSummaryArgs, ['$0.25', '21500'])
}) })
}) })

View File

@ -2,8 +2,6 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import PageContainer from '../../../ui/page-container' import PageContainer from '../../../ui/page-container'
import { Tabs, Tab } from '../../../ui/tabs' import { Tabs, Tab } from '../../../ui/tabs'
import { calcGasTotal } from '../../../../pages/send/send.utils'
import { sumHexWEIsToRenderableFiat } from '../../../../helpers/utils/conversions.util'
import AdvancedTabContent from './advanced-tab-content' import AdvancedTabContent from './advanced-tab-content'
import BasicTabContent from './basic-tab-content' import BasicTabContent from './basic-tab-content'
@ -32,10 +30,6 @@ export default class GasModalPageContainer extends Component {
newTotalEth: PropTypes.string, newTotalEth: PropTypes.string,
sendAmount: PropTypes.string, sendAmount: PropTypes.string,
transactionFee: PropTypes.string, transactionFee: PropTypes.string,
extraInfoRow: PropTypes.shape({
label: PropTypes.string,
value: PropTypes.string,
}),
}), }),
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
customModalGasPriceInHex: PropTypes.string, customModalGasPriceInHex: PropTypes.string,
@ -47,16 +41,6 @@ export default class GasModalPageContainer extends Component {
isRetry: PropTypes.bool, isRetry: PropTypes.bool,
disableSave: PropTypes.bool, disableSave: PropTypes.bool,
isEthereumNetwork: PropTypes.bool, isEthereumNetwork: PropTypes.bool,
customGasLimitMessage: PropTypes.string,
customTotalSupplement: PropTypes.string,
isSwap: PropTypes.bool,
value: PropTypes.string,
conversionRate: PropTypes.number,
minimumGasLimit: PropTypes.number.isRequired,
}
state = {
selectedTab: 'Basic',
} }
componentDidMount() { componentDidMount() {
@ -92,8 +76,6 @@ export default class GasModalPageContainer extends Component {
isRetry, isRetry,
infoRowProps: { transactionFee }, infoRowProps: { transactionFee },
isEthereumNetwork, isEthereumNetwork,
customGasLimitMessage,
minimumGasLimit,
} = this.props } = this.props
return ( return (
@ -102,7 +84,6 @@ export default class GasModalPageContainer extends Component {
updateCustomGasLimit={updateCustomGasLimit} updateCustomGasLimit={updateCustomGasLimit}
customModalGasPriceInHex={customModalGasPriceInHex} customModalGasPriceInHex={customModalGasPriceInHex}
customModalGasLimitInHex={customModalGasLimitInHex} customModalGasLimitInHex={customModalGasLimitInHex}
customGasLimitMessage={customGasLimitMessage}
timeRemaining={currentTimeEstimate} timeRemaining={currentTimeEstimate}
transactionFee={transactionFee} transactionFee={transactionFee}
gasChartProps={gasChartProps} gasChartProps={gasChartProps}
@ -112,18 +93,11 @@ export default class GasModalPageContainer extends Component {
isSpeedUp={isSpeedUp} isSpeedUp={isSpeedUp}
isRetry={isRetry} isRetry={isRetry}
isEthereumNetwork={isEthereumNetwork} isEthereumNetwork={isEthereumNetwork}
minimumGasLimit={minimumGasLimit}
/> />
) )
} }
renderInfoRows( renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) {
newTotalFiat,
newTotalEth,
sendAmount,
transactionFee,
extraInfoRow,
) {
return ( return (
<div className="gas-modal-content__info-row-wrapper"> <div className="gas-modal-content__info-row-wrapper">
<div className="gas-modal-content__info-row"> <div className="gas-modal-content__info-row">
@ -143,16 +117,6 @@ export default class GasModalPageContainer extends Component {
{transactionFee} {transactionFee}
</span> </span>
</div> </div>
{extraInfoRow && (
<div className="gas-modal-content__info-row__transaction-info">
<span className="gas-modal-content__info-row__transaction-info__label">
{extraInfoRow.label}
</span>
<span className="gas-modal-content__info-row__transaction-info__value">
{extraInfoRow.value}
</span>
</div>
)}
<div className="gas-modal-content__info-row__total-info"> <div className="gas-modal-content__info-row__total-info">
<span className="gas-modal-content__info-row__total-info__label"> <span className="gas-modal-content__info-row__total-info__label">
{this.context.t('newTotal')} {this.context.t('newTotal')}
@ -175,13 +139,7 @@ export default class GasModalPageContainer extends Component {
const { const {
gasPriceButtonGroupProps, gasPriceButtonGroupProps,
hideBasic, hideBasic,
infoRowProps: { infoRowProps: { newTotalFiat, newTotalEth, sendAmount, transactionFee },
newTotalFiat,
newTotalEth,
sendAmount,
transactionFee,
extraInfoRow,
},
} = this.props } = this.props
let tabsToRender = [ let tabsToRender = [
@ -200,7 +158,7 @@ export default class GasModalPageContainer extends Component {
} }
return ( return (
<Tabs onTabClick={(tabName) => this.setState({ selectedTab: tabName })}> <Tabs>
{tabsToRender.map(({ name, content }, i) => ( {tabsToRender.map(({ name, content }, i) => (
<Tab name={name} key={`gas-modal-tab-${i}`}> <Tab name={name} key={`gas-modal-tab-${i}`}>
<div className="gas-modal-content"> <div className="gas-modal-content">
@ -210,7 +168,6 @@ export default class GasModalPageContainer extends Component {
newTotalEth, newTotalEth,
sendAmount, sendAmount,
transactionFee, transactionFee,
extraInfoRow,
)} )}
</div> </div>
</Tab> </Tab>
@ -248,45 +205,7 @@ export default class GasModalPageContainer extends Component {
}, },
}) })
} }
if (this.props.isSwap) { onSubmit(customModalGasLimitInHex, customModalGasPriceInHex)
const newSwapGasTotal = calcGasTotal(
customModalGasLimitInHex,
customModalGasPriceInHex,
)
let speedSet = ''
if (this.state.selectedTab === 'Basic') {
const { gasButtonInfo } = this.props.gasPriceButtonGroupProps
const selectedGasButtonInfo = gasButtonInfo.find(
({ priceInHexWei }) =>
priceInHexWei === customModalGasPriceInHex,
)
speedSet = selectedGasButtonInfo?.gasEstimateType || ''
}
this.context.trackEvent({
event: 'Gas Fees Changed',
category: 'swaps',
properties: {
speed_set: speedSet,
gas_mode: this.state.selectedTab,
gas_fees: sumHexWEIsToRenderableFiat(
[
this.props.value,
newSwapGasTotal,
this.props.customTotalSupplement,
],
'usd',
this.props.conversionRate,
)?.slice(1),
},
})
}
onSubmit(
customModalGasLimitInHex,
customModalGasPriceInHex,
this.state.selectedTab,
this.context.mixPanelTrack,
)
}} }}
submitText={this.context.t('save')} submitText={this.context.t('save')}
headerCloseText={this.context.t('close')} headerCloseText={this.context.t('close')}

View File

@ -11,7 +11,6 @@ import {
updateSendAmount, updateSendAmount,
setGasTotal, setGasTotal,
updateTransaction, updateTransaction,
setSwapsTxGasParams,
} from '../../../../store/actions' } from '../../../../store/actions'
import { import {
setCustomGasPrice, setCustomGasPrice,
@ -67,21 +66,11 @@ import GasModalPageContainer from './gas-modal-page-container.component'
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
const { currentNetworkTxList, send } = state.metamask const { currentNetworkTxList, send } = state.metamask
const { modalState: { props: modalProps } = {} } = state.appState.modal || {} const { modalState: { props: modalProps } = {} } = state.appState.modal || {}
const { const { txData = {} } = modalProps || {}
txData = {},
isSwap = false,
customGasLimitMessage = '',
customTotalSupplement = '',
extraInfoRow = null,
useFastestButtons = false,
minimumGasLimit = Number(MIN_GAS_LIMIT_DEC),
} = modalProps || {}
const { transaction = {} } = ownProps const { transaction = {} } = ownProps
const selectedTransaction = isSwap const selectedTransaction = currentNetworkTxList.find(
? txData ({ id }) => id === (transaction.id || txData.id),
: currentNetworkTxList.find( )
({ id }) => id === (transaction.id || txData.id),
)
const buttonDataLoading = getBasicGasEstimateLoadingStatus(state) const buttonDataLoading = getBasicGasEstimateLoadingStatus(state)
const gasEstimatesLoading = getGasEstimatesLoadingStatus(state) const gasEstimatesLoading = getGasEstimatesLoadingStatus(state)
const sendToken = getSendToken(state) const sendToken = getSendToken(state)
@ -107,13 +96,12 @@ const mapStateToProps = (state, ownProps) => {
const gasButtonInfo = getRenderableBasicEstimateData( const gasButtonInfo = getRenderableBasicEstimateData(
state, state,
customModalGasLimitInHex, customModalGasLimitInHex,
useFastestButtons,
) )
const currentCurrency = getCurrentCurrency(state) const currentCurrency = getCurrentCurrency(state)
const conversionRate = getConversionRate(state) const conversionRate = getConversionRate(state)
const newTotalFiat = sumHexWEIsToRenderableFiat( const newTotalFiat = sumHexWEIsToRenderableFiat(
[value, customGasTotal, customTotalSupplement], [value, customGasTotal],
currentCurrency, currentCurrency,
conversionRate, conversionRate,
) )
@ -137,11 +125,7 @@ const mapStateToProps = (state, ownProps) => {
const newTotalEth = const newTotalEth =
maxModeOn && !isSendTokenSet maxModeOn && !isSendTokenSet
? sumHexWEIsToRenderableEth([balance, '0x0']) ? sumHexWEIsToRenderableEth([balance, '0x0'])
: sumHexWEIsToRenderableEth([ : sumHexWEIsToRenderableEth([value, customGasTotal])
value,
customGasTotal,
customTotalSupplement,
])
const sendAmount = const sendAmount =
maxModeOn && !isSendTokenSet maxModeOn && !isSendTokenSet
@ -171,7 +155,6 @@ const mapStateToProps = (state, ownProps) => {
return { return {
hideBasic, hideBasic,
isConfirm: isConfirm(state), isConfirm: isConfirm(state),
isSwap,
customModalGasPriceInHex, customModalGasPriceInHex,
customModalGasLimitInHex, customModalGasLimitInHex,
customGasPrice, customGasPrice,
@ -180,7 +163,7 @@ const mapStateToProps = (state, ownProps) => {
newTotalFiat, newTotalFiat,
currentTimeEstimate, currentTimeEstimate,
blockTime: getBasicGasEstimateBlockTime(state), blockTime: getBasicGasEstimateBlockTime(state),
customPriceIsSafe: isCustomPriceSafe(state, isSwap), customPriceIsSafe: isCustomPriceSafe(state),
maxModeOn, maxModeOn,
gasPriceButtonGroupProps: { gasPriceButtonGroupProps: {
buttonDataLoading, buttonDataLoading,
@ -199,20 +182,15 @@ const mapStateToProps = (state, ownProps) => {
}, },
infoRowProps: { infoRowProps: {
originalTotalFiat: sumHexWEIsToRenderableFiat( originalTotalFiat: sumHexWEIsToRenderableFiat(
[value, customGasTotal, customTotalSupplement], [value, customGasTotal],
currentCurrency, currentCurrency,
conversionRate, conversionRate,
), ),
originalTotalEth: sumHexWEIsToRenderableEth([ originalTotalEth: sumHexWEIsToRenderableEth([value, customGasTotal]),
value,
customGasTotal,
customTotalSupplement,
]),
newTotalFiat: showFiat ? newTotalFiat : '', newTotalFiat: showFiat ? newTotalFiat : '',
newTotalEth, newTotalEth,
transactionFee: sumHexWEIsToRenderableEth(['0x0', customGasTotal]), transactionFee: sumHexWEIsToRenderableEth(['0x0', customGasTotal]),
sendAmount, sendAmount,
extraInfoRow,
}, },
transaction: txData || transaction, transaction: txData || transaction,
isSpeedUp: transaction.status === 'submitted', isSpeedUp: transaction.status === 'submitted',
@ -225,11 +203,8 @@ const mapStateToProps = (state, ownProps) => {
sendToken, sendToken,
balance, balance,
tokenBalance: getTokenBalance(state), tokenBalance: getTokenBalance(state),
customGasLimitMessage,
conversionRate, conversionRate,
value, value,
customTotalSupplement,
minimumGasLimit,
} }
} }
@ -271,9 +246,6 @@ const mapDispatchToProps = (dispatch) => {
dispatch(updateSendErrors({ amount: null })) dispatch(updateSendErrors({ amount: null }))
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject))) dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
}, },
updateSwapTxGas: (gasLimit, gasPrice) => {
dispatch(setSwapsTxGasParams(gasLimit, gasPrice))
},
} }
} }
@ -282,7 +254,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
gasPriceButtonGroupProps, gasPriceButtonGroupProps,
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
isConfirm, isConfirm,
isSwap,
txId, txId,
isSpeedUp, isSpeedUp,
isRetry, isRetry,
@ -295,7 +266,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
tokenBalance, tokenBalance,
customGasLimit, customGasLimit,
transaction, transaction,
minimumGasLimit,
} = stateProps } = stateProps
const { const {
hideGasButtonGroup: dispatchHideGasButtonGroup, hideGasButtonGroup: dispatchHideGasButtonGroup,
@ -307,7 +277,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
cancelAndClose: dispatchCancelAndClose, cancelAndClose: dispatchCancelAndClose,
hideModal: dispatchHideModal, hideModal: dispatchHideModal,
setAmountToMax: dispatchSetAmountToMax, setAmountToMax: dispatchSetAmountToMax,
updateSwapTxGas: dispatchUpdateSwapTxGas,
...otherDispatchProps ...otherDispatchProps
} = dispatchProps } = dispatchProps
@ -316,10 +285,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
...otherDispatchProps, ...otherDispatchProps,
...ownProps, ...ownProps,
onSubmit: (gasLimit, gasPrice) => { onSubmit: (gasLimit, gasPrice) => {
if (isSwap) { if (isConfirm) {
dispatchUpdateSwapTxGas(gasLimit, gasPrice)
dispatchHideModal()
} else if (isConfirm) {
const updatedTx = { const updatedTx = {
...transaction, ...transaction,
txParams: { txParams: {
@ -365,7 +331,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
disableSave: disableSave:
insufficientBalance || insufficientBalance ||
(isSpeedUp && customGasPrice === 0) || (isSpeedUp && customGasPrice === 0) ||
customGasLimit < minimumGasLimit, customGasLimit < Number(MIN_GAS_LIMIT_DEC),
} }
} }

View File

@ -194,14 +194,12 @@ describe('GasModalPageContainer Component', function () {
'mockNewTotalEth', 'mockNewTotalEth',
'mockSendAmount', 'mockSendAmount',
'mockTransactionFee', 'mockTransactionFee',
{ label: 'mockLabel', value: 'mockValue' },
]) ])
assert.deepEqual(GP.renderInfoRows.getCall(1).args, [ assert.deepEqual(GP.renderInfoRows.getCall(1).args, [
'mockNewTotalFiat', 'mockNewTotalFiat',
'mockNewTotalEth', 'mockNewTotalEth',
'mockSendAmount', 'mockSendAmount',
'mockTransactionFee', 'mockTransactionFee',
{ label: 'mockLabel', value: 'mockValue' },
]) ])
}) })

View File

@ -63,8 +63,6 @@ describe('gas-modal-page-container container', function () {
txData: { txData: {
id: 34, id: 34,
}, },
extraInfoRow: { label: 'mockLabel', value: 'mockValue' },
minimumGasLimit: 21000,
}, },
}, },
}, },
@ -131,8 +129,6 @@ describe('gas-modal-page-container container', function () {
newTotalFiat: '637.41', newTotalFiat: '637.41',
blockTime: 12, blockTime: 12,
conversionRate: 50, conversionRate: 50,
customGasLimitMessage: '',
customTotalSupplement: '',
customModalGasLimitInHex: 'aaaaaaaa', customModalGasLimitInHex: 'aaaaaaaa',
customModalGasPriceInHex: 'ffffffff', customModalGasPriceInHex: 'ffffffff',
customGasTotal: 'aaaaaaa955555556', customGasTotal: 'aaaaaaa955555556',
@ -152,7 +148,6 @@ describe('gas-modal-page-container container', function () {
gasEstimatesLoading: false, gasEstimatesLoading: false,
hideBasic: true, hideBasic: true,
infoRowProps: { infoRowProps: {
extraInfoRow: { label: 'mockLabel', value: 'mockValue' },
originalTotalFiat: '637.41', originalTotalFiat: '637.41',
originalTotalEth: '12.748189 ETH', originalTotalEth: '12.748189 ETH',
newTotalFiat: '637.41', newTotalFiat: '637.41',
@ -163,7 +158,6 @@ describe('gas-modal-page-container container', function () {
insufficientBalance: true, insufficientBalance: true,
isSpeedUp: false, isSpeedUp: false,
isRetry: false, isRetry: false,
isSwap: false,
txId: 34, txId: 34,
isEthereumNetwork: true, isEthereumNetwork: true,
isMainnet: true, isMainnet: true,
@ -174,7 +168,6 @@ describe('gas-modal-page-container container', function () {
id: 34, id: 34,
}, },
value: '0x640000000000000', value: '0x640000000000000',
minimumGasLimit: 21000,
} }
const baseMockOwnProps = { transaction: { id: 34 } } const baseMockOwnProps = { transaction: { id: 34 } }
const tests = [ const tests = [

View File

@ -93,7 +93,12 @@ export default class GasPriceButtonGroup extends Component {
) { ) {
return ( return (
<Button <Button
onClick={() => handleGasPriceSelection(priceInHexWei)} onClick={() =>
handleGasPriceSelection(
priceInHexWei,
renderableGasInfo.gasEstimateType,
)
}
key={`gas-price-button-${index}`} key={`gas-price-button-${index}`}
> >
{this.renderButtonContent( {this.renderButtonContent(

View File

@ -3,6 +3,7 @@ import React from 'react'
import sinon from 'sinon' import sinon from 'sinon'
import shallow from '../../../../../../lib/shallow-with-context' import shallow from '../../../../../../lib/shallow-with-context'
import GasPriceButtonGroup from '../gas-price-button-group.component' import GasPriceButtonGroup from '../gas-price-button-group.component'
import { GAS_ESTIMATE_TYPES } from '../../../../../helpers/constants/common'
import ButtonGroup from '../../../../ui/button-group' import ButtonGroup from '../../../../ui/button-group'
@ -17,18 +18,21 @@ describe('GasPriceButtonGroup Component', function () {
className: 'gas-price-button-group', className: 'gas-price-button-group',
gasButtonInfo: [ gasButtonInfo: [
{ {
gasEstimateType: GAS_ESTIMATE_TYPES.SLOW,
feeInPrimaryCurrency: '$0.52', feeInPrimaryCurrency: '$0.52',
feeInSecondaryCurrency: '0.0048 ETH', feeInSecondaryCurrency: '0.0048 ETH',
timeEstimate: '~ 1 min 0 sec', timeEstimate: '~ 1 min 0 sec',
priceInHexWei: '0xa1b2c3f', priceInHexWei: '0xa1b2c3f',
}, },
{ {
gasEstimateType: GAS_ESTIMATE_TYPES.AVERAGE,
feeInPrimaryCurrency: '$0.39', feeInPrimaryCurrency: '$0.39',
feeInSecondaryCurrency: '0.004 ETH', feeInSecondaryCurrency: '0.004 ETH',
timeEstimate: '~ 1 min 30 sec', timeEstimate: '~ 1 min 30 sec',
priceInHexWei: '0xa1b2c39', priceInHexWei: '0xa1b2c39',
}, },
{ {
gasEstimateType: GAS_ESTIMATE_TYPES.FAST,
feeInPrimaryCurrency: '$0.30', feeInPrimaryCurrency: '$0.30',
feeInSecondaryCurrency: '0.00354 ETH', feeInSecondaryCurrency: '0.00354 ETH',
timeEstimate: '~ 2 min 1 sec', timeEstimate: '~ 2 min 1 sec',
@ -105,10 +109,12 @@ describe('GasPriceButtonGroup Component', function () {
beforeEach(function () { beforeEach(function () {
GasPriceButtonGroup.prototype.renderButtonContent.resetHistory() GasPriceButtonGroup.prototype.renderButtonContent.resetHistory()
const renderButtonResult = GasPriceButtonGroup.prototype.renderButton( const renderButtonResult = wrapper
{ ...mockGasPriceButtonGroupProps.gasButtonInfo[0] }, .instance()
mockButtonPropsAndFlags, .renderButton(
) { ...mockGasPriceButtonGroupProps.gasButtonInfo[0] },
mockButtonPropsAndFlags,
)
wrappedRenderButtonResult = shallow(renderButtonResult) wrappedRenderButtonResult = shallow(renderButtonResult)
}) })
@ -128,7 +134,10 @@ describe('GasPriceButtonGroup Component', function () {
) )
assert.deepEqual( assert.deepEqual(
mockGasPriceButtonGroupProps.handleGasPriceSelection.getCall(0).args, mockGasPriceButtonGroupProps.handleGasPriceSelection.getCall(0).args,
[mockGasPriceButtonGroupProps.gasButtonInfo[0].priceInHexWei], [
mockGasPriceButtonGroupProps.gasButtonInfo[0].priceInHexWei,
mockGasPriceButtonGroupProps.gasButtonInfo[0].gasEstimateType,
],
) )
}) })
@ -141,12 +150,14 @@ describe('GasPriceButtonGroup Component', function () {
feeInPrimaryCurrency, feeInPrimaryCurrency,
feeInSecondaryCurrency, feeInSecondaryCurrency,
timeEstimate, timeEstimate,
gasEstimateType,
} = mockGasPriceButtonGroupProps.gasButtonInfo[0] } = mockGasPriceButtonGroupProps.gasButtonInfo[0]
const { showCheck, className } = mockGasPriceButtonGroupProps const { showCheck, className } = mockGasPriceButtonGroupProps
assert.deepEqual( assert.deepEqual(
GasPriceButtonGroup.prototype.renderButtonContent.getCall(0).args, GasPriceButtonGroup.prototype.renderButtonContent.getCall(0).args,
[ [
{ {
gasEstimateType,
feeInPrimaryCurrency, feeInPrimaryCurrency,
feeInSecondaryCurrency, feeInSecondaryCurrency,
timeEstimate, timeEstimate,

View File

@ -10,6 +10,7 @@ import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
// Modal Components // Modal Components
import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container' import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container'
import SwapsGasCustomizationModal from '../../../pages/swaps/swaps-gas-customization-modal'
import DepositEtherModal from './deposit-ether-modal' import DepositEtherModal from './deposit-ether-modal'
import AccountDetailsModal from './account-details-modal' import AccountDetailsModal from './account-details-modal'
import ExportPrivateKeyModal from './export-private-key-modal' import ExportPrivateKeyModal from './export-private-key-modal'
@ -272,6 +273,31 @@ const MODALS = {
}, },
}, },
CUSTOMIZE_METASWAP_GAS: {
contents: <SwapsGasCustomizationModal />,
mobileModalStyle: {
width: '100vw',
height: '100vh',
top: '0',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
laptopModalStyle: {
width: 'auto',
height: '0px',
top: '80px',
left: '0px',
transform: 'none',
margin: '0 auto',
position: 'relative',
},
contentStyle: {
borderRadius: '8px',
},
},
EDIT_APPROVAL_PERMISSION: { EDIT_APPROVAL_PERMISSION: {
contents: <EditApprovalPermission />, contents: <EditApprovalPermission />,
mobileModalStyle: { mobileModalStyle: {

View File

@ -2,6 +2,10 @@ import { createSlice } from '@reduxjs/toolkit'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import log from 'loglevel' import log from 'loglevel'
import {
loadLocalStorageData,
saveLocalStorageData,
} from '../../../lib/local-storage-helpers'
import { import {
addToken, addToken,
addUnapprovedTransaction, addUnapprovedTransaction,
@ -27,7 +31,10 @@ import {
SWAPS_ERROR_ROUTE, SWAPS_ERROR_ROUTE,
SWAPS_MAINTENANCE_ROUTE, SWAPS_MAINTENANCE_ROUTE,
} from '../../helpers/constants/routes' } from '../../helpers/constants/routes'
import { fetchSwapsFeatureLiveness } from '../../pages/swaps/swaps.util' import {
fetchSwapsFeatureLiveness,
fetchSwapsGasPrices,
} from '../../pages/swaps/swaps.util'
import { calcGasTotal } from '../../pages/send/send.utils' import { calcGasTotal } from '../../pages/send/send.utils'
import { import {
decimalToHex, decimalToHex,
@ -37,9 +44,9 @@ import {
hexToDecimal, hexToDecimal,
hexWEIToDecGWEI, hexWEIToDecGWEI,
} from '../../helpers/utils/conversions.util' } from '../../helpers/utils/conversions.util'
import { conversionLessThan } from '../../helpers/utils/conversion-util'
import { calcTokenAmount } from '../../helpers/utils/token-util' import { calcTokenAmount } from '../../helpers/utils/token-util'
import { import {
getFastPriceEstimateInHexWEI,
getSelectedAccount, getSelectedAccount,
getTokenExchangeRates, getTokenExchangeRates,
conversionRateSelector as getConversionRate, conversionRateSelector as getConversionRate,
@ -51,14 +58,16 @@ import {
SWAP_FAILED_ERROR, SWAP_FAILED_ERROR,
SWAPS_FETCH_ORDER_CONFLICT, SWAPS_FETCH_ORDER_CONFLICT,
} from '../../helpers/constants/swaps' } from '../../helpers/constants/swaps'
import {
fetchBasicGasAndTimeEstimates,
fetchGasEstimates,
resetCustomGasState,
} from '../gas/gas.duck'
import { formatCurrency } from '../../helpers/utils/confirm-tx.util' import { formatCurrency } from '../../helpers/utils/confirm-tx.util'
import { TRANSACTION_CATEGORIES } from '../../../../shared/constants/transaction' import { TRANSACTION_CATEGORIES } from '../../../../shared/constants/transaction'
const GAS_PRICES_LOADING_STATES = {
INITIAL: 'INITIAL',
LOADING: 'LOADING',
FAILED: 'FAILED',
COMPLETED: 'COMPLETED',
}
const initialState = { const initialState = {
aggregatorMetadata: null, aggregatorMetadata: null,
approveTxId: null, approveTxId: null,
@ -68,6 +77,14 @@ const initialState = {
quotesFetchStartTime: null, quotesFetchStartTime: null,
topAssets: {}, topAssets: {},
toToken: null, toToken: null,
customGas: {
price: null,
limit: null,
loading: GAS_PRICES_LOADING_STATES.INITIAL,
priceEstimates: {},
priceEstimatesLastRetrieved: 0,
fallBackPrice: null,
},
} }
const slice = createSlice({ const slice = createSlice({
@ -106,6 +123,27 @@ const slice = createSlice({
setToToken: (state, action) => { setToToken: (state, action) => {
state.toToken = action.payload state.toToken = action.payload
}, },
swapCustomGasModalPriceEdited: (state, action) => {
state.customGas.price = action.payload
},
swapCustomGasModalLimitEdited: (state, action) => {
state.customGas.limit = action.payload
},
swapGasPriceEstimatesFetchStarted: (state) => {
state.customGas.loading = GAS_PRICES_LOADING_STATES.LOADING
},
swapGasPriceEstimatesFetchFailed: (state) => {
state.customGas.loading = GAS_PRICES_LOADING_STATES.FAILED
},
swapGasPriceEstimatesFetchCompleted: (state, action) => {
state.customGas.priceEstimates = action.payload.priceEstimates
state.customGas.loading = GAS_PRICES_LOADING_STATES.COMPLETED
state.customGas.priceEstimatesLastRetrieved =
action.payload.priceEstimatesLastRetrieved
},
retrievedFallbackSwapsGasPrice: (state, action) => {
state.customGas.fallBackPrice = action.payload
},
}, },
}) })
@ -130,6 +168,49 @@ export const getFetchingQuotes = (state) => state.swaps.fetchingQuotes
export const getQuotesFetchStartTime = (state) => export const getQuotesFetchStartTime = (state) =>
state.swaps.quotesFetchStartTime state.swaps.quotesFetchStartTime
export const getSwapsCustomizationModalPrice = (state) =>
state.swaps.customGas.price
export const getSwapsCustomizationModalLimit = (state) =>
state.swaps.customGas.limit
export const swapGasPriceEstimateIsLoading = (state) =>
state.swaps.customGas.loading === GAS_PRICES_LOADING_STATES.LOADING
export const swapGasEstimateLoadingHasFailed = (state) =>
state.swaps.customGas.loading === GAS_PRICES_LOADING_STATES.INITIAL
export const getSwapGasPriceEstimateData = (state) =>
state.swaps.customGas.priceEstimates
export const getSwapsPriceEstimatesLastRetrieved = (state) =>
state.swaps.customGas.priceEstimatesLastRetrieved
export const getSwapsFallbackGasPrice = (state) =>
state.swaps.customGas.fallBackPrice
export function shouldShowCustomPriceTooLowWarning(state) {
const { average } = getSwapGasPriceEstimateData(state)
const customGasPrice = getSwapsCustomizationModalPrice(state)
if (!customGasPrice || average === undefined) {
return false
}
const customPriceRisksSwapFailure = conversionLessThan(
{
value: customGasPrice,
fromNumericBase: 'hex',
fromDenomination: 'WEI',
toDenomination: 'GWEI',
},
{ value: average, fromNumericBase: 'dec' },
)
return customPriceRisksSwapFailure
}
// Background selectors // Background selectors
const getSwapsState = (state) => state.metamask.swapsState const getSwapsState = (state) => state.metamask.swapsState
@ -185,17 +266,8 @@ export const getUsedQuote = (state) =>
export const getDestinationTokenInfo = (state) => export const getDestinationTokenInfo = (state) =>
getFetchParams(state)?.metaData?.destinationTokenInfo getFetchParams(state)?.metaData?.destinationTokenInfo
export const getSwapsTradeTxParams = (state) => { export const getUsedSwapsGasPrice = (state) =>
const { selectedAggId, topAggId, quotes } = getSwapsState(state) getCustomSwapsGasPrice(state) || getSwapsFallbackGasPrice(state)
const usedQuote = selectedAggId ? quotes[selectedAggId] : quotes[topAggId]
if (!usedQuote) {
return null
}
const { trade } = usedQuote
const gas = getCustomSwapsGas(state) || trade.gas
const gasPrice = getCustomSwapsGasPrice(state) || trade.gasPrice
return { ...trade, gas, gasPrice }
}
export const getApproveTxParams = (state) => { export const getApproveTxParams = (state) => {
const { approvalNeeded } = getSelectedQuote(state) || getTopQuote(state) || {} const { approvalNeeded } = getSelectedQuote(state) || getTopQuote(state) || {}
@ -215,6 +287,9 @@ const {
clearSwapsState, clearSwapsState,
navigatedBackToBuildQuote, navigatedBackToBuildQuote,
retriedGetQuotes, retriedGetQuotes,
swapGasPriceEstimatesFetchCompleted,
swapGasPriceEstimatesFetchStarted,
swapGasPriceEstimatesFetchFailed,
setAggregatorMetadata, setAggregatorMetadata,
setBalanceError, setBalanceError,
setFetchingQuotes, setFetchingQuotes,
@ -222,6 +297,9 @@ const {
setQuotesFetchStartTime, setQuotesFetchStartTime,
setTopAssets, setTopAssets,
setToToken, setToToken,
swapCustomGasModalPriceEdited,
swapCustomGasModalLimitEdited,
retrievedFallbackSwapsGasPrice,
} = actions } = actions
export { export {
@ -233,6 +311,8 @@ export {
setQuotesFetchStartTime as setSwapQuotesFetchStartTime, setQuotesFetchStartTime as setSwapQuotesFetchStartTime,
setTopAssets, setTopAssets,
setToToken as setSwapToToken, setToToken as setSwapToToken,
swapCustomGasModalPriceEdited,
swapCustomGasModalLimitEdited,
} }
export const navigateBackToBuildQuote = (history) => { export const navigateBackToBuildQuote = (history) => {
@ -255,7 +335,6 @@ export const prepareForRetryGetQuotes = () => {
export const prepareToLeaveSwaps = () => { export const prepareToLeaveSwaps = () => {
return async (dispatch) => { return async (dispatch) => {
dispatch(resetCustomGasState())
dispatch(clearSwapsState()) dispatch(clearSwapsState())
await dispatch(resetBackgroundSwapsState()) await dispatch(resetBackgroundSwapsState())
} }
@ -263,9 +342,11 @@ export const prepareToLeaveSwaps = () => {
export const fetchAndSetSwapsGasPriceInfo = () => { export const fetchAndSetSwapsGasPriceInfo = () => {
return async (dispatch) => { return async (dispatch) => {
const basicEstimates = await dispatch(fetchBasicGasAndTimeEstimates()) const basicEstimates = await dispatch(fetchMetaSwapsGasPriceEstimates())
dispatch(setSwapsTxGasPrice(decGWEIToHexWEI(basicEstimates.fastest)))
await dispatch(fetchGasEstimates(basicEstimates.blockTime)) if (basicEstimates?.fast) {
dispatch(setSwapsTxGasPrice(decGWEIToHexWEI(basicEstimates.fast)))
}
} }
} }
@ -476,6 +557,7 @@ export const fetchQuotesAndSetQuoteState = (
} }
// TODO: Check for any errors we should expect to occur in production, and report others to Sentry // TODO: Check for any errors we should expect to occur in production, and report others to Sentry
log.error(`Error fetching quotes: `, e) log.error(`Error fetching quotes: `, e)
dispatch(setSwapsErrorKey(ERROR_FETCHING_QUOTES)) dispatch(setSwapsErrorKey(ERROR_FETCHING_QUOTES))
} }
@ -507,6 +589,8 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
await dispatch(stopPollingForQuotes()) await dispatch(stopPollingForQuotes())
history.push(AWAITING_SWAP_ROUTE) history.push(AWAITING_SWAP_ROUTE)
const { fast: fastGasEstimate } = getSwapGasPriceEstimateData(state)
const usedQuote = getUsedQuote(state) const usedQuote = getUsedQuote(state)
const usedTradeTxParams = usedQuote.trade const usedTradeTxParams = usedQuote.trade
@ -524,13 +608,9 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
`0x${decimalToHex(usedQuote?.maxGas || 0)}`, `0x${decimalToHex(usedQuote?.maxGas || 0)}`,
estimatedGasLimitWithMultiplier, estimatedGasLimitWithMultiplier,
) )
usedTradeTxParams.gas = maxGasLimit
const customConvertGasPrice = getCustomSwapsGasPrice(state) const usedGasPrice = getUsedSwapsGasPrice(state)
const tradeTxParams = getSwapsTradeTxParams(state) usedTradeTxParams.gas = maxGasLimit
const fastGasEstimate = getFastPriceEstimateInHexWEI(state)
const usedGasPrice =
customConvertGasPrice || tradeTxParams?.gasPrice || fastGasEstimate
usedTradeTxParams.gasPrice = usedGasPrice usedTradeTxParams.gasPrice = usedGasPrice
const conversionRate = getConversionRate(state) const conversionRate = getConversionRate(state)
@ -568,8 +648,8 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
: usedQuote.aggregator, : usedQuote.aggregator,
gas_fees: formatCurrency(gasEstimateTotalInEth, 'usd')?.slice(1), gas_fees: formatCurrency(gasEstimateTotalInEth, 'usd')?.slice(1),
estimated_gas: estimatedGasLimit.toString(10), estimated_gas: estimatedGasLimit.toString(10),
suggested_gas_price: hexWEIToDecGWEI(usedGasPrice), suggested_gas_price: fastGasEstimate,
used_gas_price: hexWEIToDecGWEI(fastGasEstimate), used_gas_price: hexWEIToDecGWEI(usedGasPrice),
average_savings: usedQuote.savings?.performance, average_savings: usedQuote.savings?.performance,
} }
@ -645,3 +725,68 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
await forceUpdateMetamaskState(dispatch) await forceUpdateMetamaskState(dispatch)
} }
} }
export function fetchMetaSwapsGasPriceEstimates() {
return async (dispatch, getState) => {
const state = getState()
const priceEstimatesLastRetrieved = getSwapsPriceEstimatesLastRetrieved(
state,
)
const timeLastRetrieved =
priceEstimatesLastRetrieved ||
loadLocalStorageData('METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED') ||
0
dispatch(swapGasPriceEstimatesFetchStarted())
let priceEstimates
try {
if (Date.now() - timeLastRetrieved > 30000) {
priceEstimates = await fetchSwapsGasPrices()
} else {
const cachedPriceEstimates = loadLocalStorageData(
'METASWAP_GAS_PRICE_ESTIMATES',
)
priceEstimates = cachedPriceEstimates || (await fetchSwapsGasPrices())
}
} catch (e) {
log.warn('Fetching swaps gas prices failed:', e)
if (!e.message?.match(/NetworkError|Fetch failed with status:/u)) {
throw e
}
dispatch(swapGasPriceEstimatesFetchFailed())
try {
const gasPrice = await global.ethQuery.gasPrice()
const gasPriceInDecGWEI = hexWEIToDecGWEI(gasPrice.toString(10))
dispatch(retrievedFallbackSwapsGasPrice(gasPriceInDecGWEI))
return null
} catch (networkGasPriceError) {
console.error(
`Failed to retrieve fallback gas price: `,
networkGasPriceError,
)
return null
}
}
const timeRetrieved = Date.now()
saveLocalStorageData(priceEstimates, 'METASWAP_GAS_PRICE_ESTIMATES')
saveLocalStorageData(
timeRetrieved,
'METASWAP_GAS_PRICE_ESTIMATES_LAST_RETRIEVED',
)
dispatch(
swapGasPriceEstimatesFetchCompleted({
priceEstimates,
priceEstimatesLastRetrieved: timeRetrieved,
}),
)
return priceEstimates
}
}

View File

@ -12,7 +12,7 @@ import {
getUsedQuote, getUsedQuote,
getFetchParams, getFetchParams,
getApproveTxParams, getApproveTxParams,
getSwapsTradeTxParams, getUsedSwapsGasPrice,
fetchQuotesAndSetQuoteState, fetchQuotesAndSetQuoteState,
navigateBackToBuildQuote, navigateBackToBuildQuote,
prepareForRetryGetQuotes, prepareForRetryGetQuotes,
@ -67,7 +67,7 @@ export default function AwaitingSwap({
const { destinationTokenInfo, sourceTokenInfo } = fetchParams?.metaData || {} const { destinationTokenInfo, sourceTokenInfo } = fetchParams?.metaData || {}
const usedQuote = useSelector(getUsedQuote) const usedQuote = useSelector(getUsedQuote)
const approveTxParams = useSelector(getApproveTxParams) const approveTxParams = useSelector(getApproveTxParams)
const tradeTxParams = useSelector(getSwapsTradeTxParams) const swapsGasPrice = useSelector(getUsedSwapsGasPrice)
const currentCurrency = useSelector(getCurrentCurrency) const currentCurrency = useSelector(getCurrentCurrency)
const conversionRate = useSelector(conversionRateSelector) const conversionRate = useSelector(conversionRateSelector)
@ -77,14 +77,15 @@ export default function AwaitingSwap({
) )
let feeinFiat let feeinFiat
if (usedQuote && tradeTxParams) {
if (usedQuote && swapsGasPrice) {
const renderableNetworkFees = getRenderableNetworkFeesForQuote( const renderableNetworkFees = getRenderableNetworkFeesForQuote(
usedQuote.gasEstimateWithRefund || usedQuote.averageGas, usedQuote.gasEstimateWithRefund || usedQuote.averageGas,
approveTxParams?.gas || '0x0', approveTxParams?.gas || '0x0',
tradeTxParams.gasPrice, swapsGasPrice,
currentCurrency, currentCurrency,
conversionRate, conversionRate,
tradeTxParams.value, usedQuote?.trade?.value,
sourceTokenInfo?.symbol, sourceTokenInfo?.symbol,
usedQuote.sourceAmount, usedQuote.sourceAmount,
) )

View File

@ -21,9 +21,8 @@ import {
getApproveTxId, getApproveTxId,
getFetchingQuotes, getFetchingQuotes,
setBalanceError, setBalanceError,
getCustomSwapsGasPrice,
setTopAssets, setTopAssets,
getSwapsTradeTxParams, getUsedSwapsGasPrice,
getFetchParams, getFetchParams,
setAggregatorMetadata, setAggregatorMetadata,
getAggregatorMetadata, getAggregatorMetadata,
@ -33,7 +32,6 @@ import {
prepareToLeaveSwaps, prepareToLeaveSwaps,
fetchAndSetSwapsGasPriceInfo, fetchAndSetSwapsGasPriceInfo,
} from '../../ducks/swaps/swaps' } from '../../ducks/swaps/swaps'
import { resetCustomGasState } from '../../ducks/gas/gas.duck'
import { import {
AWAITING_SWAP_ROUTE, AWAITING_SWAP_ROUTE,
BUILD_QUOTE_ROUTE, BUILD_QUOTE_ROUTE,
@ -59,7 +57,6 @@ import {
setSwapsErrorKey, setSwapsErrorKey,
} from '../../store/actions' } from '../../store/actions'
import { import {
getFastPriceEstimateInHexWEI,
currentNetworkTxListSelector, currentNetworkTxListSelector,
getRpcPrefsForCurrentProvider, getRpcPrefsForCurrentProvider,
} from '../../selectors' } from '../../selectors'
@ -96,11 +93,9 @@ export default function Swap() {
const [maxSlippage, setMaxSlippage] = useState(fetchParams?.slippage || 2) const [maxSlippage, setMaxSlippage] = useState(fetchParams?.slippage || 2)
const routeState = useSelector(getBackgroundSwapRouteState) const routeState = useSelector(getBackgroundSwapRouteState)
const tradeTxParams = useSelector(getSwapsTradeTxParams) const usedGasPrice = useSelector(getUsedSwapsGasPrice)
const selectedAccount = useSelector(getSelectedAccount) const selectedAccount = useSelector(getSelectedAccount)
const quotes = useSelector(getQuotes) const quotes = useSelector(getQuotes)
const fastGasEstimate = useSelector(getFastPriceEstimateInHexWEI)
const customConvertGasPrice = useSelector(getCustomSwapsGasPrice)
const txList = useSelector(currentNetworkTxListSelector) const txList = useSelector(currentNetworkTxListSelector)
const tradeTxId = useSelector(getTradeTxId) const tradeTxId = useSelector(getTradeTxId)
const approveTxId = useSelector(getApproveTxId) const approveTxId = useSelector(getApproveTxId)
@ -131,8 +126,6 @@ export default function Swap() {
useSelector(getFromToken) || fetchParamsFromToken || {} useSelector(getFromToken) || fetchParamsFromToken || {}
const { destinationTokenAddedForSwap } = fetchParams || {} const { destinationTokenAddedForSwap } = fetchParams || {}
const usedGasPrice =
customConvertGasPrice || tradeTxParams?.gasPrice || fastGasEstimate
const approveTxData = const approveTxData =
approveTxId && txList.find(({ id }) => approveTxId === id) approveTxId && txList.find(({ id }) => approveTxId === id)
const tradeTxData = tradeTxId && txList.find(({ id }) => tradeTxId === id) const tradeTxData = tradeTxId && txList.find(({ id }) => tradeTxId === id)
@ -197,7 +190,6 @@ export default function Swap() {
dispatch(setAggregatorMetadata(newAggregatorMetadata)) dispatch(setAggregatorMetadata(newAggregatorMetadata))
}) })
dispatch(resetCustomGasState())
dispatch(fetchAndSetSwapsGasPriceInfo()) dispatch(fetchAndSetSwapsGasPriceInfo())
return () => { return () => {

View File

@ -0,0 +1 @@
export { default } from './swaps-gas-customization-modal.container'

View File

@ -0,0 +1,269 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import PageContainer from '../../../components/ui/page-container'
import { Tabs, Tab } from '../../../components/ui/tabs'
import { calcGasTotal } from '../../send/send.utils'
import { sumHexWEIsToRenderableFiat } from '../../../helpers/utils/conversions.util'
import AdvancedGasInputs from '../../../components/app/gas-customization/advanced-gas-inputs'
import BasicTabContent from '../../../components/app/gas-customization/gas-modal-page-container/basic-tab-content'
import { GAS_ESTIMATE_TYPES } from '../../../helpers/constants/common'
export default class GasModalPageContainer extends Component {
static contextTypes = {
t: PropTypes.func,
trackEvent: PropTypes.func,
}
static propTypes = {
insufficientBalance: PropTypes.bool,
gasPriceButtonGroupProps: PropTypes.object,
infoRowProps: PropTypes.shape({
originalTotalFiat: PropTypes.string,
originalTotalEth: PropTypes.string,
newTotalFiat: PropTypes.string,
newTotalEth: PropTypes.string,
sendAmount: PropTypes.string,
transactionFee: PropTypes.string,
extraInfoRow: PropTypes.shape({
label: PropTypes.string,
value: PropTypes.string,
}),
}),
onSubmit: PropTypes.func,
cancelAndClose: PropTypes.func,
showCustomPriceTooLowWarning: PropTypes.bool,
disableSave: PropTypes.bool,
customGasLimitMessage: PropTypes.string,
customTotalSupplement: PropTypes.string,
value: PropTypes.string,
conversionRate: PropTypes.string,
customGasPrice: PropTypes.string,
customGasLimit: PropTypes.string,
setSwapsCustomizationModalPrice: PropTypes.func,
setSwapsCustomizationModalLimit: PropTypes.func,
gasEstimateLoadingHasFailed: PropTypes.bool,
}
state = {
gasSpeedType: '',
}
setGasSpeedType(gasEstimateType) {
if (gasEstimateType === GAS_ESTIMATE_TYPES.AVERAGE) {
this.setState({ gasSpeedType: 'average' })
} else {
this.setState({ gasSpeedType: 'fast' })
}
}
renderBasicTabContent(gasPriceButtonGroupProps) {
return (
<BasicTabContent
gasPriceButtonGroupProps={{
...gasPriceButtonGroupProps,
handleGasPriceSelection: (gasPriceInHexWei, gasEstimateType) => {
this.setGasSpeedType(gasEstimateType)
this.props.setSwapsCustomizationModalPrice(gasPriceInHexWei)
},
}}
/>
)
}
renderAdvancedTabContent() {
const {
insufficientBalance,
showCustomPriceTooLowWarning,
infoRowProps: { transactionFee },
customGasLimitMessage,
setSwapsCustomizationModalPrice,
setSwapsCustomizationModalLimit,
customGasPrice,
customGasLimit,
} = this.props
return (
<div className="advanced-tab">
<div className="advanced-tab__transaction-data-summary">
<div className="advanced-tab__transaction-data-summary__titles">
<span>{this.context.t('newTransactionFee')}</span>
</div>
<div className="advanced-tab__transaction-data-summary__container">
<div className="advanced-tab__transaction-data-summary__fee">
{transactionFee}
</div>
</div>
</div>
<div className="advanced-tab__fee-chart">
<div className="advanced-tab__gas-inputs">
<AdvancedGasInputs
updateCustomGasPrice={(updatedPrice) => {
this.setState({ gasSpeedType: 'custom' })
setSwapsCustomizationModalPrice(updatedPrice)
}}
updateCustomGasLimit={(updatedLimit) => {
this.setState({ gasSpeedType: 'custom' })
setSwapsCustomizationModalLimit(updatedLimit)
}}
customGasPrice={customGasPrice}
customGasLimit={customGasLimit}
insufficientBalance={insufficientBalance}
customPriceIsSafe={!showCustomPriceTooLowWarning}
customGasLimitMessage={customGasLimitMessage}
/>
</div>
</div>
</div>
)
}
renderInfoRows(
newTotalFiat,
newTotalEth,
sendAmount,
transactionFee,
extraInfoRow,
) {
return (
<div className="gas-modal-content__info-row-wrapper">
<div className="gas-modal-content__info-row">
<div className="gas-modal-content__info-row__send-info">
<span className="gas-modal-content__info-row__send-info__label">
{this.context.t('sendAmount')}
</span>
<span className="gas-modal-content__info-row__send-info__value">
{sendAmount}
</span>
</div>
<div className="gas-modal-content__info-row__transaction-info">
<span className="gas-modal-content__info-row__transaction-info__label">
{this.context.t('transactionFee')}
</span>
<span className="gas-modal-content__info-row__transaction-info__value">
{transactionFee}
</span>
</div>
{extraInfoRow && (
<div className="gas-modal-content__info-row__transaction-info">
<span className="gas-modal-content__info-row__transaction-info__label">
{extraInfoRow.label}
</span>
<span className="gas-modal-content__info-row__transaction-info__value">
{extraInfoRow.value}
</span>
</div>
)}
<div className="gas-modal-content__info-row__total-info">
<span className="gas-modal-content__info-row__total-info__label">
{this.context.t('newTotal')}
</span>
<span className="gas-modal-content__info-row__total-info__value">
{newTotalEth}
</span>
</div>
<div className="gas-modal-content__info-row__fiat-total-info">
<span className="gas-modal-content__info-row__fiat-total-info__value">
{newTotalFiat}
</span>
</div>
</div>
</div>
)
}
renderTabs() {
const {
gasPriceButtonGroupProps,
infoRowProps: {
newTotalFiat,
newTotalEth,
sendAmount,
transactionFee,
extraInfoRow,
},
gasEstimateLoadingHasFailed,
} = this.props
const basicTabInfo = {
name: this.context.t('basic'),
content: this.renderBasicTabContent({
...gasPriceButtonGroupProps,
handleGasPriceSelection: this.props.setSwapsCustomizationModalPrice,
}),
}
const advancedTabInfo = {
name: this.context.t('advanced'),
content: this.renderAdvancedTabContent(),
}
const tabsToRender = gasEstimateLoadingHasFailed
? [advancedTabInfo]
: [basicTabInfo, advancedTabInfo]
return (
<Tabs>
{tabsToRender.map(({ name, content }, i) => (
<Tab name={name} key={`gas-modal-tab-${i}`}>
<div className="gas-modal-content">
{content}
{this.renderInfoRows(
newTotalFiat,
newTotalEth,
sendAmount,
transactionFee,
extraInfoRow,
)}
</div>
</Tab>
))}
</Tabs>
)
}
render() {
const {
cancelAndClose,
onSubmit,
disableSave,
customGasPrice,
customGasLimit,
} = this.props
return (
<div className="gas-modal-page-container">
<PageContainer
title={this.context.t('customGas')}
subtitle={this.context.t('customGasSubTitle')}
tabsComponent={this.renderTabs()}
disabled={disableSave}
onCancel={() => cancelAndClose()}
onClose={() => cancelAndClose()}
onSubmit={() => {
const newSwapGasTotal = calcGasTotal(customGasLimit, customGasPrice)
this.context.trackEvent({
event: 'Gas Fees Changed',
category: 'swaps',
properties: {
speed_set: this.state.gasSpeedType,
gas_fees: sumHexWEIsToRenderableFiat(
[
this.props.value,
newSwapGasTotal,
this.props.customTotalSupplement,
],
'usd',
this.props.conversionRate,
)?.slice(1),
},
})
onSubmit(customGasLimit, customGasPrice)
}}
submitText={this.context.t('save')}
headerCloseText={this.context.t('close')}
hideCancel
/>
</div>
)
}
}

View File

@ -0,0 +1,159 @@
import { connect } from 'react-redux'
import { hideModal, customSwapsGasParamsUpdated } from '../../../store/actions'
import {
conversionRateSelector as getConversionRate,
getCurrentCurrency,
getCurrentEthBalance,
getDefaultActiveButtonIndex,
getRenderableGasButtonData,
} from '../../../selectors'
import {
getSwapsCustomizationModalPrice,
getSwapsCustomizationModalLimit,
swapGasEstimateLoadingHasFailed,
swapGasPriceEstimateIsLoading,
getSwapGasPriceEstimateData,
swapCustomGasModalPriceEdited,
swapCustomGasModalLimitEdited,
shouldShowCustomPriceTooLowWarning,
} from '../../../ducks/swaps/swaps'
import {
addHexes,
getValueFromWeiHex,
sumHexWEIsToRenderableFiat,
} from '../../../helpers/utils/conversions.util'
import { formatETHFee } from '../../../helpers/utils/formatters'
import { calcGasTotal, isBalanceSufficient } from '../../send/send.utils'
import SwapsGasCustomizationModalComponent from './swaps-gas-customization-modal.component'
const mapStateToProps = (state) => {
const currentCurrency = getCurrentCurrency(state)
const conversionRate = getConversionRate(state)
const { modalState: { props: modalProps } = {} } = state.appState.modal || {}
const {
value,
customGasLimitMessage = '',
customTotalSupplement = '',
extraInfoRow = null,
initialGasPrice,
initialGasLimit,
} = modalProps || {}
const buttonDataLoading = swapGasPriceEstimateIsLoading(state)
const swapsCustomizationModalPrice = getSwapsCustomizationModalPrice(state)
const swapsCustomizationModalLimit = getSwapsCustomizationModalLimit(state)
const customGasPrice = swapsCustomizationModalPrice || initialGasPrice
const customGasLimit = swapsCustomizationModalLimit || initialGasLimit
const customGasTotal = calcGasTotal(customGasLimit, customGasPrice)
const swapsGasPriceEstimates = getSwapGasPriceEstimateData(state)
const { averageEstimateData, fastEstimateData } = getRenderableGasButtonData(
swapsGasPriceEstimates,
customGasLimit,
true,
conversionRate,
currentCurrency,
)
const gasButtonInfo = [averageEstimateData, fastEstimateData]
const newTotalFiat = sumHexWEIsToRenderableFiat(
[value, customGasTotal, customTotalSupplement],
currentCurrency,
conversionRate,
)
const balance = getCurrentEthBalance(state)
const newTotalEth = sumHexWEIsToRenderableEth([
value,
customGasTotal,
customTotalSupplement,
])
const sendAmount = sumHexWEIsToRenderableEth([value, '0x0'])
const insufficientBalance = !isBalanceSufficient({
amount: value,
gasTotal: customGasTotal,
balance,
conversionRate,
})
return {
customGasPrice,
customGasLimit,
showCustomPriceTooLowWarning: shouldShowCustomPriceTooLowWarning(state),
gasPriceButtonGroupProps: {
buttonDataLoading,
defaultActiveButtonIndex: getDefaultActiveButtonIndex(
gasButtonInfo,
customGasPrice,
),
gasButtonInfo,
},
infoRowProps: {
originalTotalFiat: sumHexWEIsToRenderableFiat(
[value, customGasTotal, customTotalSupplement],
currentCurrency,
conversionRate,
),
originalTotalEth: sumHexWEIsToRenderableEth([
value,
customGasTotal,
customTotalSupplement,
]),
newTotalFiat,
newTotalEth,
transactionFee: sumHexWEIsToRenderableEth(['0x0', customGasTotal]),
sendAmount,
extraInfoRow,
},
gasEstimateLoadingHasFailed: swapGasEstimateLoadingHasFailed(state),
insufficientBalance,
customGasLimitMessage,
customTotalSupplement,
conversionRate,
value,
disableSave: insufficientBalance,
}
}
const mapDispatchToProps = (dispatch) => {
return {
cancelAndClose: () => {
dispatch(hideModal())
},
onSubmit: (gasLimit, gasPrice) => {
dispatch(customSwapsGasParamsUpdated(gasLimit, gasPrice))
dispatch(hideModal())
},
setSwapsCustomizationModalPrice: (newPrice) => {
dispatch(swapCustomGasModalPriceEdited(newPrice))
},
setSwapsCustomizationModalLimit: (newLimit) => {
dispatch(swapCustomGasModalLimitEdited(newLimit))
},
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(SwapsGasCustomizationModalComponent)
function sumHexWEIsToRenderableEth(hexWEIs) {
const hexWEIsSum = hexWEIs.filter((n) => n).reduce(addHexes)
return formatETHFee(
getValueFromWeiHex({
value: hexWEIsSum,
toCurrency: 'ETH',
numberOfDecimals: 6,
}),
)
}

View File

@ -36,6 +36,8 @@ const getBaseApi = function (type) {
return `https://api.metaswap.codefi.network/featureFlag` return `https://api.metaswap.codefi.network/featureFlag`
case 'aggregatorMetadata': case 'aggregatorMetadata':
return `https://api.metaswap.codefi.network/aggregatorMetadata` return `https://api.metaswap.codefi.network/aggregatorMetadata`
case 'gasPrices':
return `https://api.metaswap.codefi.network/gasPrices`
default: default:
throw new Error('getBaseApi requires an api call type') throw new Error('getBaseApi requires an api call type')
} }
@ -150,6 +152,27 @@ const AGGREGATOR_METADATA_VALIDATORS = [
}, },
] ]
const isValidDecimalNumber = (string) =>
!isNaN(string) && string.match(/^[.0-9]+$/u) && !isNaN(parseFloat(string))
const SWAP_GAS_PRICE_VALIDATOR = [
{
property: 'SafeGasPrice',
type: 'string',
validator: isValidDecimalNumber,
},
{
property: 'ProposeGasPrice',
type: 'string',
validator: isValidDecimalNumber,
},
{
property: 'FastGasPrice',
type: 'string',
validator: isValidDecimalNumber,
},
]
function validateData(validators, object, urlUsed) { function validateData(validators, object, urlUsed) {
return validators.every(({ property, type, validator }) => { return validators.every(({ property, type, validator }) => {
const types = type.split('|') const types = type.split('|')
@ -320,6 +343,36 @@ export async function fetchTokenBalance(address, userAddress) {
return usersToken return usersToken
} }
export async function fetchSwapsGasPrices() {
const gasPricesUrl = getBaseApi('gasPrices')
const response = await fetchWithCache(
gasPricesUrl,
{ method: 'GET' },
{ cacheRefreshTime: 15000 },
)
const responseIsValid = validateData(
SWAP_GAS_PRICE_VALIDATOR,
response,
gasPricesUrl,
)
if (!responseIsValid) {
throw new Error(`${gasPricesUrl} response is invalid`)
}
const {
SafeGasPrice: safeLow,
ProposeGasPrice: average,
FastGasPrice: fast,
} = response
return {
safeLow,
average,
fast,
}
}
export function getRenderableNetworkFeesForQuote( export function getRenderableNetworkFeesForQuote(
tradeGas, tradeGas,
approveGas, approveGas,

View File

@ -22,7 +22,7 @@ import {
getBalanceError, getBalanceError,
getCustomSwapsGas, getCustomSwapsGas,
getDestinationTokenInfo, getDestinationTokenInfo,
getSwapsTradeTxParams, getUsedSwapsGasPrice,
getTopQuote, getTopQuote,
navigateBackToBuildQuote, navigateBackToBuildQuote,
signAndSendTransactions, signAndSendTransactions,
@ -101,8 +101,7 @@ export default function ViewQuote() {
const quotesLastFetched = useSelector(getQuotesLastFetched) const quotesLastFetched = useSelector(getQuotesLastFetched)
// Select necessary data // Select necessary data
const tradeTxParams = useSelector(getSwapsTradeTxParams) const gasPrice = useSelector(getUsedSwapsGasPrice)
const { gasPrice, value: tradeValue } = tradeTxParams || {}
const customMaxGas = useSelector(getCustomSwapsGas) const customMaxGas = useSelector(getCustomSwapsGas)
const tokenConversionRates = useSelector(getTokenExchangeRates) const tokenConversionRates = useSelector(getTokenExchangeRates)
const memoizedTokenConversionRates = useEqualityCheck(tokenConversionRates) const memoizedTokenConversionRates = useEqualityCheck(tokenConversionRates)
@ -116,6 +115,7 @@ export default function ViewQuote() {
const selectedQuote = useSelector(getSelectedQuote) const selectedQuote = useSelector(getSelectedQuote)
const topQuote = useSelector(getTopQuote) const topQuote = useSelector(getTopQuote)
const usedQuote = selectedQuote || topQuote const usedQuote = selectedQuote || topQuote
const { value: tradeValue = '0x0' } = usedQuote?.trade?.value || {}
const fetchParamsSourceToken = fetchParams?.sourceToken const fetchParamsSourceToken = fetchParams?.sourceToken
@ -454,9 +454,8 @@ export default function ViewQuote() {
const onFeeCardMaxRowClick = () => const onFeeCardMaxRowClick = () =>
dispatch( dispatch(
showModal({ showModal({
name: 'CUSTOMIZE_GAS', name: 'CUSTOMIZE_METASWAP_GAS',
txData: { txParams: { ...tradeTxParams, gas: maxGasLimit } }, value: usedQuote.value,
isSwap: true,
customGasLimitMessage: approveGas customGasLimitMessage: approveGas
? t('extraApprovalGas', [hexToDecimal(approveGas)]) ? t('extraApprovalGas', [hexToDecimal(approveGas)])
: '', : '',
@ -467,8 +466,8 @@ export default function ViewQuote() {
value: t('amountInEth', [extraNetworkFeeTotalInEth]), value: t('amountInEth', [extraNetworkFeeTotalInEth]),
} }
: null, : null,
useFastestButtons: true, initialGasPrice: gasPrice,
minimumGasLimit: Number(hexToDecimal(nonCustomMaxGasLimit)), initialGasLimit: maxGasLimit,
}), }),
) )
@ -605,7 +604,7 @@ export default function ViewQuote() {
}} }}
submitText={t('swap')} submitText={t('swap')}
onCancel={async () => await dispatch(navigateBackToBuildQuote(history))} onCancel={async () => await dispatch(navigateBackToBuildQuote(history))}
disabled={balanceError} disabled={balanceError || gasPrice === null || gasPrice === undefined}
showTermsOfService showTermsOfService
showTopBorder showTopBorder
/> />

View File

@ -80,20 +80,8 @@ export function getSafeLowEstimate(state) {
return safeLow return safeLow
} }
export function getAverageEstimate(state) { export function isCustomPriceSafe(state) {
const {
gas: {
basicEstimates: { average },
},
} = state
return average
}
export function isCustomPriceSafe(state, averageIsSafe) {
const safeLow = getSafeLowEstimate(state) const safeLow = getSafeLowEstimate(state)
const average = getAverageEstimate(state)
const safeMinimumPrice = averageIsSafe ? average : safeLow
const customGasPrice = getCustomGasPrice(state) const customGasPrice = getCustomGasPrice(state)
@ -101,7 +89,7 @@ export function isCustomPriceSafe(state, averageIsSafe) {
return true return true
} }
if (safeMinimumPrice === null) { if (safeLow === null) {
return false return false
} }
@ -112,7 +100,7 @@ export function isCustomPriceSafe(state, averageIsSafe) {
fromDenomination: 'WEI', fromDenomination: 'WEI',
toDenomination: 'GWEI', toDenomination: 'GWEI',
}, },
{ value: safeMinimumPrice, fromNumericBase: 'dec' }, { value: safeLow, fromNumericBase: 'dec' },
) )
return customPriceSafe return customPriceSafe
@ -219,34 +207,23 @@ export function getGasPriceInHexWei(price) {
return addHexPrefix(priceEstimateToWei(value)) return addHexPrefix(priceEstimateToWei(value))
} }
export function getRenderableBasicEstimateData( export function getRenderableGasButtonData(
state, estimates,
gasLimit, gasLimit,
useFastestButtons, showFiat,
conversionRate,
currentCurrency,
) { ) {
if (getBasicGasEstimateLoadingStatus(state)) {
return []
}
const { showFiatInTestnets } = getPreferences(state)
const isMainnet = getIsMainnet(state)
const showFiat = isMainnet || Boolean(showFiatInTestnets)
const { conversionRate } = state.metamask
const currentCurrency = getCurrentCurrency(state)
const { const {
gas: { safeLow,
basicEstimates: { average,
safeLow, fast,
average, safeLowWait,
fast, avgWait,
safeLowWait, fastWait,
avgWait, fastest,
fastWait, fastestWait,
fastest, } = estimates
fastestWait,
},
},
} = state
const slowEstimateData = { const slowEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.SLOW, gasEstimateType: GAS_ESTIMATE_TYPES.SLOW,
@ -305,9 +282,38 @@ export function getRenderableBasicEstimateData(
priceInHexWei: getGasPriceInHexWei(fastest), priceInHexWei: getGasPriceInHexWei(fastest),
} }
return useFastestButtons return {
? [fastEstimateData, fastestEstimateData] slowEstimateData,
: [slowEstimateData, averageEstimateData, fastEstimateData] averageEstimateData,
fastEstimateData,
fastestEstimateData,
}
}
export function getRenderableBasicEstimateData(state, gasLimit) {
if (getBasicGasEstimateLoadingStatus(state)) {
return []
}
const { showFiatInTestnets } = getPreferences(state)
const isMainnet = getIsMainnet(state)
const showFiat = isMainnet || Boolean(showFiatInTestnets)
const { conversionRate } = state.metamask
const currentCurrency = getCurrentCurrency(state)
const {
slowEstimateData,
averageEstimateData,
fastEstimateData,
} = getRenderableGasButtonData(
state.gas.basicEstimates,
gasLimit,
showFiat,
conversionRate,
currentCurrency,
)
return [slowEstimateData, averageEstimateData, fastEstimateData]
} }
export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) { export function getRenderableEstimateDataForSmallButtonsFromGWEI(state) {

View File

@ -348,53 +348,6 @@ describe('custom-gas selectors', function () {
}, },
}, },
}, },
{
expectedResult: [
{
gasEstimateType: 'FAST',
feeInSecondaryCurrency: '$0.54',
feeInPrimaryCurrency: '0.00021 ETH',
timeEstimate: '~6 min 36 sec',
priceInHexWei: '0x2540be400',
},
{
feeInPrimaryCurrency: '0.00042 ETH',
feeInSecondaryCurrency: '$1.07',
gasEstimateType: 'FASTEST',
priceInHexWei: '0x4a817c800',
timeEstimate: '~1 min',
},
],
mockState: {
metamask: {
conversionRate: 2557.1,
currentCurrency: 'usd',
send: {
gasLimit: '0x5208',
},
preferences: {
showFiatInTestnets: true,
},
provider: {
type: 'mainnet',
},
},
gas: {
basicEstimates: {
blockTime: 14.16326530612245,
safeLow: 5,
safeLowWait: 13.2,
average: 7,
avgWait: 10.1,
fast: 10,
fastWait: 6.6,
fastest: 20,
fastestWait: 1.0,
},
},
},
useFastestButtons: true,
},
] ]
it('should return renderable data about basic estimates', function () { it('should return renderable data about basic estimates', function () {
tests.forEach((test) => { tests.forEach((test) => {

View File

@ -2288,7 +2288,7 @@ export function setSwapsTxGasLimit(gasLimit) {
} }
} }
export function setSwapsTxGasParams(gasLimit, gasPrice) { export function customSwapsGasParamsUpdated(gasLimit, gasPrice) {
return async (dispatch) => { return async (dispatch) => {
await promisifiedBackground.setSwapsTxGasPrice(gasPrice) await promisifiedBackground.setSwapsTxGasPrice(gasPrice)
await promisifiedBackground.setSwapsTxGasLimit(gasLimit, true) await promisifiedBackground.setSwapsTxGasLimit(gasLimit, true)