Merge pull request #9644 from MetaMask/Version-v8.1.2

Version v8.1.2 RC
feature/default_network_editable
Mark Stacey 4 years ago committed by GitHub
commit 074ed8d303
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      CHANGELOG.md
  2. 20
      app/_locales/en/messages.json
  3. 2
      app/manifest/_base.json
  4. 29
      app/scripts/lib/account-tracker.js
  5. 2
      app/scripts/metamask-controller.js
  6. 15
      ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js
  7. 3
      ui/app/components/app/gas-customization/advanced-gas-inputs/tests/advanced-gas-input-component.test.js
  8. 3
      ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
  9. 5
      ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.component.js
  10. 8
      ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
  11. 2
      ui/app/components/app/gas-customization/gas-modal-page-container/tests/gas-modal-page-container-container.test.js
  12. 18
      ui/app/components/app/modals/qr-scanner/qr-scanner.component.js
  13. 3
      ui/app/components/app/transaction-list-item/transaction-list-item.component.js
  14. 19
      ui/app/components/ui/info-tooltip/index.scss
  15. 12
      ui/app/components/ui/info-tooltip/info-tooltip.js
  16. 1
      ui/app/hooks/tests/useTransactionDisplayData.test.js
  17. 3
      ui/app/hooks/useTransactionDisplayData.js
  18. 18
      ui/app/hooks/useTransactionTimeRemaining.js
  19. 13
      ui/app/pages/swaps/awaiting-swap/awaiting-swap.js
  20. 30
      ui/app/pages/swaps/fee-card/fee-card.js
  21. 51
      ui/app/pages/swaps/fee-card/fee-card.stories.js
  22. 26
      ui/app/pages/swaps/fee-card/index.scss
  23. 5
      ui/app/pages/swaps/main-quote-summary/index.scss
  24. 12
      ui/app/pages/swaps/main-quote-summary/quote-backdrop.js
  25. 4
      ui/app/pages/swaps/swaps.util.js
  26. 27
      ui/app/pages/swaps/view-quote/index.scss
  27. 18
      ui/app/pages/swaps/view-quote/view-quote.js
  28. 2
      ui/app/pages/unlock-page/unlock-page.component.js
  29. 31
      ui/app/selectors/custom-gas.js
  30. 7
      ui/app/selectors/tests/custom-gas.test.js

@ -2,6 +2,14 @@
## Current Develop Branch
## 8.1.2 Mon Oct 19 2020
- [#9608](https://github.com/MetaMask/metamask-extension/pull/9608): Ensure QR code scanner works
- [#9624](https://github.com/MetaMask/metamask-extension/pull/9624): Help users avoid insufficient gas prices in swaps
- [#9614](https://github.com/MetaMask/metamask-extension/pull/9614): Update swaps network fee tooltip
- [#9623](https://github.com/MetaMask/metamask-extension/pull/9623): Prevent reducing the gas limit for swaps
- [#9630](https://github.com/MetaMask/metamask-extension/pull/9630): Fix UI crash when trying to render estimated time remaining of non-submitted transaction
- [#9633](https://github.com/MetaMask/metamask-extension/pull/9633): Update View Quote page to better represent the MetaMask fee
## 8.1.1 Tue Oct 13 2020
- [#9586](https://github.com/MetaMask/metamask-extension/pull/9586): Prevent build quote crash when swapping from non-tracked token with balance (#9586)
- [#9592](https://github.com/MetaMask/metamask-extension/pull/9592): Remove commitment to maintain a public metrics dashboard (#9592)

@ -708,6 +708,10 @@
"gasLimitTooLow": {
"message": "Gas limit must be at least 21000"
},
"gasLimitTooLowWithDynamicFee": {
"message": "Gas limit must be at least $1",
"description": "$1 is the custom gas limit, in decimal."
},
"gasPrice": {
"message": "Gas Price (GWEI)"
},
@ -1643,6 +1647,10 @@
"swapEstimatedNetworkFee": {
"message": "Estimated network fee"
},
"swapEstimatedNetworkFeeSummary": {
"message": "The “$1” is what we expect the actual fee to be. The exact amount depends on network conditions.",
"description": "$1 will be the translation of swapEstimatedNetworkFee, with the font bolded"
},
"swapEstimatedNetworkFees": {
"message": "Estimated network fees"
},
@ -1677,6 +1685,9 @@
"swapFinalizing": {
"message": "Finalizing..."
},
"swapGasFeeSummary": {
"message": "The gas fee covers the cost of processing your swap and storing it on the Ethereum network. MetaMask does not profit from this fee."
},
"swapGetQuotes": {
"message": "Get quotes"
},
@ -1705,7 +1716,8 @@
"message": "Transaction may fail, max slippage too low."
},
"swapMaxNetworkFeeInfo": {
"message": "The Max network fee is the most you’ll pay to complete your transaction. The max fee helps ensure your Swap has the best chance of succeeding. MetaMask does not profit from network fees."
"message": "“$1” is the most you’ll spend. When the network is volatile this can be a large amount.",
"description": "$1 will be the translation of swapMaxNetworkFees, with the font bolded"
},
"swapMaxNetworkFees": {
"message": "Max network fee"
@ -1717,7 +1729,7 @@
"message": "MetaMask fee"
},
"swapMetaMaskFeeDescription": {
"message": "A service fee of $1% is automatically factored into each quote, which supports ongoing development to make MetaMask even better.",
"message": "We find the best price from the top liquidity sources, every time. A fee of $1% is automatically factored into each quote, which supports ongoing development to make MetaMask even better.",
"description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number."
},
"swapNQuotesAvailable": {
@ -1741,6 +1753,10 @@
"swapQuoteDetailsSlippageInfo": {
"message": "If the price changes between the time your order is placed and confirmed it’s called \"slippage\". Your Swap will automatically cancel if slippage exceeds your \"max slippage\" setting."
},
"swapQuoteIncludesRate": {
"message": "Quote includes a $1% MetaMask fee",
"description": "Provides information about the fee that metamask takes for swaps. $1 is a decimal number."
},
"swapQuoteNofN": {
"message": "Quote $1 of $2",
"description": "A count of loaded quotes shown to the user while they are waiting for quotes to be fetched. $1 is the number of quotes already loaded, and $2 is the total number of quotes to load."

@ -68,6 +68,6 @@
"notifications"
],
"short_name": "__MSG_appName__",
"version": "8.1.1",
"version": "8.1.2",
"web_accessible_resources": ["inpage.js", "phishing.html"]
}

@ -14,7 +14,7 @@ import log from 'loglevel'
import pify from 'pify'
import Web3 from 'web3'
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'
import { MAINNET_NETWORK_ID, RINKEBY_NETWORK_ID, ROPSTEN_NETWORK_ID, KOVAN_NETWORK_ID } from '../controllers/network/enums'
import { MAINNET_CHAIN_ID, RINKEBY_CHAIN_ID, ROPSTEN_CHAIN_ID, KOVAN_CHAIN_ID } from '../controllers/network/enums'
import {
SINGLE_CALL_BALANCES_ADDRESS,
@ -24,16 +24,13 @@ import {
} from '../controllers/network/contract-addresses'
import { bnToHex } from './util'
export default class AccountTracker {
/**
/**
* This module is responsible for tracking any number of accounts and caching their current balances & transaction
* counts.
*
* It also tracks transaction hashes, and checks their inclusion status on each new block.
*
* @typedef {Object} AccountTracker
* @param {Object} opts - Initialize various properties of the class.
* @property {Object} store The stored object containing all accounts to track, as well as the current block's gas limit.
* @property {Object} store.accounts The accounts currently stored in this AccountTracker
* @property {string} store.currentBlockGasLimit A hex string indicating the gas limit of the current block
@ -44,6 +41,14 @@ export default class AccountTracker {
* @property {Object} _currentBlockNumber Reference to a property on the _blockTracker: the number (i.e. an id) of the the current block
*
*/
export default class AccountTracker {
/**
* @param {Object} opts - Options for initializing the controller
* @param {Object} opts.provider - An EIP-1193 provider instance that uses the current global network
* @param {Object} opts.blockTracker - A block tracker, which emits events for each new block
* @param {Function} opts.getCurrentChainId - A function that returns the `chainId` for the current global network
*/
constructor (opts = {}) {
const initState = {
accounts: {},
@ -61,7 +66,7 @@ export default class AccountTracker {
})
// bind function for easier listener syntax
this._updateForBlock = this._updateForBlock.bind(this)
this.network = opts.network
this.getCurrentChainId = opts.getCurrentChainId
this.web3 = new Web3(this._provider)
}
@ -196,22 +201,22 @@ export default class AccountTracker {
async _updateAccounts () {
const { accounts } = this.store.getState()
const addresses = Object.keys(accounts)
const currentNetwork = this.network.getNetworkState()
const chainId = this.getCurrentChainId()
switch (currentNetwork) {
case MAINNET_NETWORK_ID.toString():
switch (chainId) {
case MAINNET_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS)
break
case RINKEBY_NETWORK_ID.toString():
case RINKEBY_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_RINKEBY)
break
case ROPSTEN_NETWORK_ID.toString():
case ROPSTEN_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN)
break
case KOVAN_NETWORK_ID.toString():
case KOVAN_CHAIN_ID:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_KOVAN)
break

@ -168,7 +168,7 @@ export default class MetamaskController extends EventEmitter {
this.accountTracker = new AccountTracker({
provider: this.provider,
blockTracker: this.blockTracker,
network: this.networkController,
getCurrentChainId: this.networkController.getCurrentChainId.bind(this.networkController),
})
// start and stop polling for balances based on activeControllerConnections

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import classnames from 'classnames'
import { debounce } from 'lodash'
import Tooltip from '../../../ui/tooltip'
import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants'
export default class AdvancedGasInputs extends Component {
static contextTypes = {
@ -18,6 +19,11 @@ export default class AdvancedGasInputs extends Component {
customPriceIsSafe: PropTypes.bool,
isSpeedUp: PropTypes.bool,
customGasLimitMessage: PropTypes.string,
minimumGasLimit: PropTypes.number,
}
static defaultProps = {
minimumGasLimit: Number(MIN_GAS_LIMIT_DEC),
}
constructor (props) {
@ -84,7 +90,7 @@ export default class AdvancedGasInputs extends Component {
return {}
}
gasLimitError ({ insufficientBalance, gasLimit }) {
gasLimitError ({ insufficientBalance, gasLimit, minimumGasLimit }) {
const { t } = this.context
if (insufficientBalance) {
@ -92,9 +98,9 @@ export default class AdvancedGasInputs extends Component {
errorText: t('insufficientBalance'),
errorType: 'error',
}
} else if (gasLimit < 21000) {
} else if (gasLimit < minimumGasLimit) {
return {
errorText: t('gasLimitTooLow'),
errorText: t('gasLimitTooLowWithDynamicFee', [minimumGasLimit]),
errorType: 'error',
}
}
@ -153,6 +159,7 @@ export default class AdvancedGasInputs extends Component {
customPriceIsSafe,
isSpeedUp,
customGasLimitMessage,
minimumGasLimit,
} = this.props
const {
gasPrice,
@ -172,7 +179,7 @@ export default class AdvancedGasInputs extends Component {
const {
errorText: gasLimitErrorText,
errorType: gasLimitErrorType,
} = this.gasLimitError({ insufficientBalance, gasLimit })
} = this.gasLimitError({ insufficientBalance, gasLimit, minimumGasLimit })
const gasLimitErrorComponent = gasLimitErrorType ? (
<div className={`advanced-gas-inputs__gas-edit-row__${gasLimitErrorType}-text`}>
{ gasLimitErrorText }

@ -17,6 +17,7 @@ describe('Advanced Gas Inputs', function () {
insufficientBalance: false,
customPriceIsSafe: true,
isSpeedUp: false,
minimumGasLimit: 21000,
}
beforeEach(function () {
@ -91,7 +92,7 @@ describe('Advanced Gas Inputs', function () {
assert.equal(renderError.length, 2)
assert.equal(renderError.at(0).text(), 'zeroGasPriceOnSpeedUpError')
assert.equal(renderError.at(1).text(), 'gasLimitTooLow')
assert.equal(renderError.at(1).text(), 'gasLimitTooLowWithDynamicFee')
})
it('warns when custom gas price is too low', function () {

@ -27,6 +27,7 @@ export default class AdvancedTabContent extends Component {
isSpeedUp: PropTypes.bool,
isEthereumNetwork: PropTypes.bool,
customGasLimitMessage: PropTypes.string,
minimumGasLimit: PropTypes.number.isRequired,
}
renderDataSummary (transactionFee, timeRemaining) {
@ -67,6 +68,7 @@ export default class AdvancedTabContent extends Component {
transactionFee,
isEthereumNetwork,
customGasLimitMessage,
minimumGasLimit,
} = this.props
return (
@ -83,6 +85,7 @@ export default class AdvancedTabContent extends Component {
customPriceIsSafe={customPriceIsSafe}
isSpeedUp={isSpeedUp}
customGasLimitMessage={customGasLimitMessage}
minimumGasLimit={minimumGasLimit}
/>
</div>
{ isEthereumNetwork

@ -53,7 +53,8 @@ export default class GasModalPageContainer extends Component {
customTotalSupplement: PropTypes.string,
isSwap: PropTypes.bool,
value: PropTypes.string,
conversionRate: PropTypes.number,
conversionRate: PropTypes.string,
minimumGasLimit: PropTypes.number.isRequired,
}
state = {
@ -98,6 +99,7 @@ export default class GasModalPageContainer extends Component {
},
isEthereumNetwork,
customGasLimitMessage,
minimumGasLimit,
} = this.props
return (
@ -116,6 +118,7 @@ export default class GasModalPageContainer extends Component {
isSpeedUp={isSpeedUp}
isRetry={isRetry}
isEthereumNetwork={isEthereumNetwork}
minimumGasLimit={minimumGasLimit}
/>
)
}

@ -62,6 +62,7 @@ import {
calcGasTotal,
isBalanceSufficient,
} from '../../../../pages/send/send.utils'
import { MIN_GAS_LIMIT_DEC } from '../../../../pages/send/send.constants'
import { calcMaxAmount } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils'
import GasModalPageContainer from './gas-modal-page-container.component'
@ -75,6 +76,7 @@ const mapStateToProps = (state, ownProps) => {
customTotalSupplement = '',
extraInfoRow = null,
useFastestButtons = false,
minimumGasLimit = Number(MIN_GAS_LIMIT_DEC),
} = modalProps || {}
const { transaction = {} } = ownProps
const selectedTransaction = isSwap
@ -158,7 +160,7 @@ const mapStateToProps = (state, ownProps) => {
newTotalFiat,
currentTimeEstimate,
blockTime: getBasicGasEstimateBlockTime(state),
customPriceIsSafe: isCustomPriceSafe(state),
customPriceIsSafe: isCustomPriceSafe(state, isSwap),
maxModeOn,
gasPriceButtonGroupProps: {
buttonDataLoading,
@ -202,6 +204,7 @@ const mapStateToProps = (state, ownProps) => {
conversionRate,
value,
customTotalSupplement,
minimumGasLimit,
}
}
@ -264,6 +267,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
tokenBalance,
customGasLimit,
transaction,
minimumGasLimit,
} = stateProps
const {
hideGasButtonGroup: dispatchHideGasButtonGroup,
@ -333,7 +337,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
disableSave: (
insufficientBalance ||
(isSpeedUp && customGasPrice === 0) ||
customGasLimit < 21000
customGasLimit < minimumGasLimit
),
}
}

@ -63,6 +63,7 @@ describe('gas-modal-page-container container', function () {
id: 34,
},
extraInfoRow: { label: 'mockLabel', value: 'mockValue' },
minimumGasLimit: 21000,
},
},
},
@ -170,6 +171,7 @@ describe('gas-modal-page-container container', function () {
id: 34,
},
value: '0x640000000000000',
minimumGasLimit: 21000,
}
const baseMockOwnProps = { transaction: { id: 34 } }
const tests = [

@ -104,15 +104,29 @@ export default class QrScanner extends Component {
componentWillUnmount () {
this.mounted = false
clearTimeout(this.permissionChecker)
this.teardownCodeReader()
}
teardownCodeReader () {
if (this.codeReader) {
this.codeReader.reset()
this.codeReader.stop()
this.codeReader = null
}
}
initCamera = async () => {
// The `decodeFromInputVideoDevice` call prompts the browser to show
// the user the camera permission request. We must then call it again
// once we receive permission so that the video displays.
// It's important to prevent this codeReader from being created twice;
// Firefox otherwise starts 2 video streams, one of which cannot be stopped
if (!this.codeReader) {
this.codeReader = new BrowserQRCodeReader()
}
try {
await this.codeReader.getVideoInputDevices()
this.checkPermissions()
const content = await this.codeReader.decodeFromInputVideoDevice(undefined, 'video')
const result = this.parseContent(content.text)
if (!this.mounted) {
@ -162,7 +176,7 @@ export default class QrScanner extends Component {
stopAndClose = () => {
if (this.codeReader) {
this.codeReader.reset()
this.teardownCodeReader()
}
this.props.hideModal()
}
@ -170,7 +184,7 @@ export default class QrScanner extends Component {
tryAgain = () => {
clearTimeout(this.permissionChecker)
if (this.codeReader) {
this.codeReader.reset()
this.teardownCodeReader()
}
this.setState(this.getInitialState(), () => {
this.checkEnvironment()

@ -50,9 +50,10 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
displayedStatusKey,
isPending,
senderAddress,
isSubmitted,
} = useTransactionDisplayData(transactionGroup)
const timeRemaining = useTransactionTimeRemaining(isPending, isEarliestNonce, submittedTime, gasPrice)
const timeRemaining = useTransactionTimeRemaining(isSubmitted, isEarliestNonce, submittedTime, gasPrice)
const isSignatureReq = category === TRANSACTION_CATEGORY_SIGNATURE_REQUEST
const isApproval = category === TRANSACTION_CATEGORY_APPROVAL

@ -5,24 +5,29 @@
}
}
.tippy-popper[x-placement^=top] .tippy-tooltip-info-theme [x-arrow] {
.tippy-popper[x-placement^=top] .tippy-tooltip-info-theme [x-arrow],
.tippy-popper[x-placement^=top] .tippy-tooltip-wideInfo-theme [x-arrow] {
border-top-color: $white;
}
.tippy-popper[x-placement^=right] .tippy-tooltip-info-theme [x-arrow] {
.tippy-popper[x-placement^=right] .tippy-tooltip-info-theme [x-arrow],
.tippy-popper[x-placement^=right] .tippy-tooltip-wideInfo-theme [x-arrow] {
border-right-color: $white;
}
.tippy-popper[x-placement^=left] .tippy-tooltip-info-theme [x-arrow] {
.tippy-popper[x-placement^=left] .tippy-tooltip-info-theme [x-arrow],
.tippy-popper[x-placement^=left] .tippy-tooltip-wideInfo-theme [x-arrow] {
border-left-color: $white;
}
.tippy-popper[x-placement^=bottom] .tippy-tooltip-info-theme [x-arrow] {
.tippy-popper[x-placement^=bottom] .tippy-tooltip-info-theme [x-arrow],
.tippy-popper[x-placement^=bottom] .tippy-tooltip-wideInfo-theme [x-arrow] {
border-bottom-color: $white;
}
.tippy-tooltip {
&#{&}-info-theme {
&#{&}-info-theme,
&#{&}-wideInfo-theme {
background: white;
color: black;
box-shadow: 0 0 14px rgba(0, 0, 0, 0.18);
@ -38,4 +43,8 @@
color: $Grey-500;
}
}
&#{&}-wideInfo-theme {
max-width: 260px;
}
}

@ -1,5 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Tooltip from '../tooltip'
const positionArrowClassMap = {
@ -12,17 +13,21 @@ const positionArrowClassMap = {
export default function InfoTooltip ({
contentText = '',
position = '',
containerClassName,
wrapperClassName,
wide,
}) {
return (
<div className="info-tooltip">
<Tooltip
interactive
position={position}
containerClassName="info-tooltip__tooltip-container"
containerClassName={classnames('info-tooltip__tooltip-container', containerClassName)}
wrapperClassName={wrapperClassName}
tooltipInnerClassName="info-tooltip__tooltip-content"
tooltipArrowClassName={positionArrowClassMap[position]}
html={contentText}
theme="tippy-tooltip-info"
theme={wide ? 'tippy-tooltip-wideInfo' : 'tippy-tooltip-info'}
>
<img src="images/mm-info-icon.svg" />
</Tooltip>
@ -33,4 +38,7 @@ export default function InfoTooltip ({
InfoTooltip.propTypes = {
contentText: PropTypes.string,
position: PropTypes.oneOf(['top', 'left', 'bottom', 'right']),
wide: PropTypes.bool,
containerClassName: PropTypes.string,
wrapperClassName: PropTypes.string,
}

@ -27,6 +27,7 @@ const expectedResults = [
secondaryCurrency: '-1 ETH',
isPending: false,
displayedStatusKey: 'confirmed',
isSubmitted: false,
},
{
title: 'Send ETH',

@ -23,6 +23,7 @@ import {
TOKEN_CATEGORY_HASH,
SWAP,
SWAP_APPROVAL,
SUBMITTED_STATUS,
} from '../helpers/constants/transactions'
import { getTokens } from '../ducks/metamask/metamask'
import { useI18nContext } from './useI18nContext'
@ -74,6 +75,7 @@ export function useTransactionDisplayData (transactionGroup) {
const displayedStatusKey = getStatusKey(primaryTransaction)
const isPending = displayedStatusKey in PENDING_STATUS_HASH
const isSubmitted = displayedStatusKey === SUBMITTED_STATUS
const primaryValue = primaryTransaction.txParams?.value
let prefix = '-'
@ -213,5 +215,6 @@ export function useTransactionDisplayData (transactionGroup) {
) ? undefined : secondaryCurrency,
displayedStatusKey,
isPending,
isSubmitted,
}
}

@ -26,7 +26,7 @@ function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) {
* returns a string representing the number of minutes predicted for the transaction to be
* completed. Only returns this prediction if the transaction is the earliest pending
* transaction, and the feature flag for showing timing is enabled.
* @param {bool} isPending - is the transaction currently pending
* @param {bool} isSubmitted - is the transaction currently in the 'submitted' state
* @param {bool} isEarliestNonce - is this transaction the earliest nonce in list
* @param {number} submittedTime - the timestamp for when the transaction was submitted
* @param {number} currentGasPrice - gas price to use for calculation of time
@ -34,7 +34,7 @@ function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) {
* @returns {string | undefined} i18n formatted string if applicable
*/
export function useTransactionTimeRemaining (
isPending,
isSubmitted,
isEarliestNonce,
submittedTime,
currentGasPrice,
@ -73,7 +73,7 @@ export function useTransactionTimeRemaining (
if (
(isMainNet &&
(transactionTimeFeatureActive || forceAllow)) &&
isPending &&
isSubmitted &&
isEarliestNonce &&
!isNaN(initialTimeEstimate)
) {
@ -93,10 +93,10 @@ export function useTransactionTimeRemaining (
isMainNet,
transactionTimeFeatureActive,
isEarliestNonce,
isPending,
submittedTime,
initialTimeEstimate,
forceAllow,
isSubmitted,
])
// there are numerous checks to determine if time should be displayed.
@ -104,8 +104,10 @@ export function useTransactionTimeRemaining (
// User is currently not on the mainnet
// User does not have the transactionTime feature flag enabled
// The transaction is not pending, or isn't the earliest nonce
const usedFormat = dontFormat
? timeRemaining
: rtf.format(timeRemaining, 'minute')
return timeRemaining ? usedFormat : undefined
if (timeRemaining && dontFormat) {
return timeRemaining
} else if (timeRemaining) {
return rtf.format(timeRemaining, 'minute')
}
return undefined
}

@ -19,7 +19,7 @@ import { useTransactionTimeRemaining } from '../../../hooks/useTransactionTimeRe
import { usePrevious } from '../../../hooks/usePrevious'
import Mascot from '../../../components/ui/mascot'
import PulseLoader from '../../../components/ui/pulse-loader'
import { getBlockExplorerUrlForTx } from '../../../helpers/utils/transactions.util'
import { getBlockExplorerUrlForTx, getStatusKey } from '../../../helpers/utils/transactions.util'
import CountdownTimer from '../countdown-timer'
import {
QUOTES_EXPIRED_ERROR,
@ -28,6 +28,7 @@ import {
QUOTES_NOT_AVAILABLE_ERROR,
OFFLINE_FOR_MAINTENANCE,
} from '../../../helpers/constants/swaps'
import { SUBMITTED_STATUS } from '../../../helpers/constants/transactions'
import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes'
import { getRenderableGasFeesForQuote } from '../swaps.util'
@ -96,7 +97,15 @@ export default function AwaitingSwap ({
rpcPrefs,
)
const timeRemaining = useTransactionTimeRemaining(true, true, tradeTxData?.submittedTime, usedGasPrice, true, true)
const statusKey = tradeTxData && getStatusKey(tradeTxData)
const timeRemaining = useTransactionTimeRemaining(
statusKey === SUBMITTED_STATUS,
true,
tradeTxData?.submittedTime,
usedGasPrice,
true,
true,
)
const previousTimeRemaining = usePrevious(timeRemaining)
const timeRemainingIsNumber = typeof timeRemaining === 'number' && !isNaN(timeRemaining)
const previousTimeRemainingIsNumber = typeof previousTimeRemaining === 'number' && !isNaN(previousTimeRemaining)

@ -22,6 +22,30 @@ export default function FeeCard ({
<div className="fee-card__row-header-text--bold">
{t('swapEstimatedNetworkFee')}
</div>
<InfoTooltip
position="top"
contentText={(
<>
<p className="fee-card__info-tooltip-paragraph">{ t('swapGasFeeSummary') }</p>
<p className="fee-card__info-tooltip-paragraph">{ t('swapEstimatedNetworkFeeSummary', [
<span className="fee-card__bold" key="fee-card-bold-1">
{ t('swapEstimatedNetworkFee') }
</span>,
]) }
</p>
<p className="fee-card__info-tooltip-paragraph">{ t('swapMaxNetworkFeeInfo', [
<span className="fee-card__bold" key="fee-card-bold-2">
{ t('swapMaxNetworkFees') }
</span>,
]) }
</p>
</>
)
}
containerClassName="fee-card__info-tooltip-content-container"
wrapperClassName="fee-card__row-label fee-card__info-tooltip-container"
wide
/>
</div>
<div>
<div className="fee-card__row-header-secondary--bold">
@ -42,12 +66,6 @@ export default function FeeCard ({
<div className="fee-card__link">
{t('edit')}
</div>
<div className="fee-card__row-label">
<InfoTooltip
position="top"
contentText={t('swapMaxNetworkFeeInfo')}
/>
</div>
</div>
<div>
<div className="fee-card__row-header-secondary">

@ -3,6 +3,15 @@ import { action } from '@storybook/addon-actions'
import { text } from '@storybook/addon-knobs/react'
import FeeCard from './fee-card'
const tokenApprovalTextComponent = (
<span
key="swaps-view-quote-approve-symbol-1"
className="view-quote__bold"
>
ABC
</span>
)
const containerStyle = {
width: '300px',
}
@ -24,19 +33,11 @@ export const WithAllProps = () => {
fee: text('secondaryFee', '100 USD'),
maxFee: text('secondaryMaxFee', '200 USD'),
})}
maxFeeRow={({
text: text('maxFeeText', 'Max Fee'),
linkText: text('maxFeeLinkText', 'Edit'),
tooltipText: text('maxFeeTooltipText', 'Click here to edit.'),
onClick: action('Clicked max fee row link'),
})}
thirdRow={({
text: text('thirdRowText', 'Extra Option'),
linkText: text('thirdRowLinkText', 'Click Me'),
tooltipText: text('thirdRowTooltipText', 'Something happens if you click this'),
onClick: action('Clicked third row link'),
hide: false,
})}
onFeeCardMaxRowClick={action('Clicked max fee row link')}
tokenApprovalTextComponent={tokenApprovalTextComponent}
tokenApprovalSourceTokenSymbol="ABC"
onTokenApprovalClick={action('Clicked third row link')}
hideTokenApprovalRow={false}
/>
</div>
)
@ -55,19 +56,8 @@ export const WithoutThirdRow = () => {
fee: text('secondaryFee', '100 USD'),
maxFee: text('secondaryMaxFee', '200 USD'),
})}
maxFeeRow={({
text: text('maxFeeText', 'Max Fee'),
linkText: text('maxFeeLinkText', 'Edit'),
tooltipText: text('maxFeeTooltipText', 'Click here to edit.'),
onClick: action('Clicked max fee row link'),
})}
thirdRow={({
text: text('thirdRowText', 'Extra Option'),
linkText: text('thirdRowLinkText', 'Click Me'),
tooltipText: text('thirdRowTooltipText', 'Something happens if you click this'),
onClick: action('Clicked third row link'),
hide: true,
})}
onFeeCardMaxRowClick={action('Clicked max fee row link')}
hideTokenApprovalRow
/>
</div>
)
@ -77,17 +67,12 @@ export const WithOnlyRequiredProps = () => {
return (
<div style={containerStyle}>
<FeeCard
feeRowText={text('feeRowText', 'Network fees')}
primaryFee={({
fee: text('primaryFee', '1 ETH'),
maxFee: text('primaryMaxFee', '2 ETH'),
})}
maxFeeRow={({
text: text('maxFeeText', 'Max Fee'),
linkText: text('maxFeeLinkText', 'Edit'),
tooltipText: text('maxFeeTooltipText', 'Click here to edit.'),
onClick: action('Clicked max fee row link'),
})}
onFeeCardMaxRowClick={action('Clicked max fee row link')}
hideTokenApprovalRow
/>
</div>
)

@ -27,7 +27,7 @@
&__row-header-text,
&__row-header-text--bold {
margin-right: 6px;
margin-right: 4px;
cursor: pointer;
}
@ -56,6 +56,21 @@
}
}
&__info-tooltip-container {
height: 10px;
width: 10px;
justify-content: center;
margin-top: 2px;
}
&__info-tooltip-paragraph {
margin-bottom: 8px;
}
&__info-tooltip-paragraph:last-of-type {
margin-bottom: 0;
}
&__row-fee {
margin-right: 4px;
}
@ -91,11 +106,8 @@
color: $Grey-500;
}
&__row-header-secondary,
&__row-header-secondary--bold {
margin-right: 16px;
}
&__row-header-secondary {
margin-right: 12px;
}
@ -109,6 +121,10 @@
&__row-header-primary--bold {
font-weight: bold;
}
&__bold {
font-weight: bold;
}
}
.info-tooltip {

@ -22,7 +22,7 @@
&__quote-backdrop {
width: 310px;
height: 179.15px;
height: 164px;
}
&__details {
@ -49,13 +49,14 @@
}
&__quote-details-top {
height: 137px;
height: 94px;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
width: 100%;
padding: 12px;
padding-top: 2px;
margin-top: 4px;
}

@ -5,13 +5,13 @@ export default function QuotesBackdrop ({
withTopTab,
}) {
return (
<svg width="311" height="199" viewBox="25.5 29.335899353027344 311 199"fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="311" height="164" viewBox="25.5 29.335899353027344 311 164" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<path d="M25.5 57.5046C25.5 53.0864 29.0817 49.5046 33.5 49.5046H328.5C332.918 49.5046 336.5 53.0864 336.5 57.5046V221.005C336.5 225.423 332.918 229.005 328.5 229.005H33.5C29.0817 229.005 25.5 225.423 25.5 221.005V57.5046Z" fill="url(#paint0_linear)" />
{withTopTab && <path d="M121.705 34.8352C122.929 31.816 125.861 29.8406 129.119 29.8406H230.883C234.141 29.8406 237.073 31.816 238.297 34.8352L251.468 62.9263C253.601 68.1861 249.73 73.9317 244.054 73.9317H115.948C110.272 73.9317 106.401 68.1861 108.534 62.9263L121.705 34.8352Z" fill="url(#paint1_linear)" />}
<path d="M25.4749 54C25.4749 49.5817 29.0566 46 33.4749 46H328.475C332.893 46 336.475 49.5817 336.475 54V185.5C336.475 189.918 332.893 193.5 328.475 193.5H33.4749C29.0566 193.5 25.4749 189.918 25.4749 185.5V54Z" fill="url(#paint0_linear)" />
{withTopTab && <path d="M132.68 34.3305C133.903 31.3114 136.836 29.3359 140.094 29.3359H219.858C223.116 29.3359 226.048 31.3114 227.272 34.3305L237.443 59.4217C239.575 64.6815 235.705 70.4271 230.029 70.4271H129.922C124.247 70.4271 120.376 64.6814 122.508 59.4217L132.68 34.3305Z" fill="url(#paint1_linear)" />}
</g>
<defs>
<filter id="filter0_d" x="-13.5" y="0.840576" width="389" height="277.164" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
<filter id="filter0_d" x="-13.5251" y="0.335938" width="389" height="242.164" filterUnits="userSpaceOnUse" colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dy="10" />
@ -20,11 +20,11 @@ export default function QuotesBackdrop ({
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
</filter>
<linearGradient id="paint0_linear" x1="25.5" y1="94.1976" x2="342.259" y2="94.1976" gradientUnits="userSpaceOnUse">
<linearGradient id="paint0_linear" x1="25.4749" y1="90.693" x2="342.234" y2="90.693" gradientUnits="userSpaceOnUse">
<stop stopColor="#037DD6" />
<stop offset="0.994792" stopColor="#1098FC" />
</linearGradient>
<linearGradient id="paint1_linear" x1="25.5" y1="94.1976" x2="342.259" y2="94.1976" gradientUnits="userSpaceOnUse">
<linearGradient id="paint1_linear" x1="25.4749" y1="90.693" x2="342.234" y2="90.693" gradientUnits="userSpaceOnUse">
<stop stopColor="#037DD6" />
<stop offset="0.994792" stopColor="#1098FC" />
</linearGradient>

@ -275,7 +275,7 @@ export function getRenderableGasFeesForQuote (tradeGas, approveGas, gasPrice, cu
const ethFee = getValueFromWeiHex({
value: gasTotalInWeiHex,
toDenomination: 'ETH',
numberOfDecimals: 6,
numberOfDecimals: 5,
})
const rawNetworkFees = getValueFromWeiHex({
value: gasTotalInWeiHex,
@ -315,7 +315,6 @@ export function quotesToRenderableData (quotes, gasPrice, conversionRate, curren
conversionRate,
)
const metaMaskFee = `0.875%`
const slippageMultiplier = (new BigNumber(100 - slippage)).div(100)
const minimumAmountReceived = (new BigNumber(destinationValue)).times(slippageMultiplier).toFixed(6)
@ -348,7 +347,6 @@ export function quotesToRenderableData (quotes, gasPrice, conversionRate, curren
destinationTokenValue: formatSwapsValueForDisplay(destinationValue),
isBestQuote: quote.isBestQuote,
liquiditySourceKey,
metaMaskFee,
feeInEth,
detailedNetworkFees: `${feeInEth} (${feeInFiat})`,
networkFees: feeInFiat,

@ -13,6 +13,7 @@
height: 100%;
padding-left: 20px;
padding-right: 20px;
justify-content: space-between;
@media screen and (max-width: 576px) {
overflow-y: auto;
@ -38,16 +39,11 @@
&__view-other-button-container {
border-radius: 28px;
margin-top: 38px;
width: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
@media screen and (min-width: 576px) {
margin-top: auto;
}
}
&__view-other-button,
@ -131,13 +127,9 @@
&__fee-card-container {
width: 100%;
margin-top: auto;
margin-top: 8px;
margin-bottom: 8px;
@media screen and (max-width: 576px) {
margin-top: 16px;
}
@media screen and (min-width: 576px) {
margin-bottom: 0;
@ -158,4 +150,19 @@
margin-top: 8px;
}
}
&__metamask-rate {
display: flex;
margin-top: 8%;
}
&__metamask-rate-text {
@include H7;
color: $Grey-500;
}
&__metamask-rate-info-icon {
margin-left: 4px;
}
}

@ -22,6 +22,7 @@ import {
getBalanceError,
getCustomSwapsGas,
getDestinationTokenInfo,
getMetaMaskFeeAmount,
getSwapsTradeTxParams,
getTopQuote,
navigateBackToBuildQuote,
@ -72,6 +73,7 @@ import { useTokenTracker } from '../../../hooks/useTokenTracker'
import { QUOTES_EXPIRED_ERROR } from '../../../helpers/constants/swaps'
import CountdownTimer from '../countdown-timer'
import SwapsFooter from '../swaps-footer'
import InfoTooltip from '../../../components/ui/info-tooltip'
export default function ViewQuote () {
const history = useHistory()
@ -131,12 +133,11 @@ export default function ViewQuote () {
.round(0)
.toString(16)
const maxGasLimit = (customMaxGas ||
hexMax(
const nonCustomMaxGasLimit = hexMax(
(`0x${decimalToHex(usedQuote?.maxGas || 0)}`),
usedGasLimitWithMultiplier,
)
)
const maxGasLimit = customMaxGas || nonCustomMaxGasLimit
const gasTotalInWeiHex = calcGasTotal(maxGasLimit, gasPrice)
@ -340,6 +341,8 @@ export default function ViewQuote () {
}
}, [sourceTokenSymbol, sourceTokenValue, destinationTokenSymbol, destinationTokenValue, fetchParams, topQuote, numberOfQuotes, feeInFiat, bestQuoteReviewedEvent, anonymousBestQuoteReviewedEvent])
const metaMaskFee = useSelector(getMetaMaskFeeAmount)
const onFeeCardTokenApprovalClick = () => {
anonymousEditSpendLimitOpened()
editSpendLimitOpened()
@ -394,6 +397,7 @@ export default function ViewQuote () {
: null
),
useFastestButtons: true,
minimumGasLimit: Number(hexToDecimal(nonCustomMaxGasLimit)),
}))
const tokenApprovalTextComponent = (
@ -494,6 +498,14 @@ export default function ViewQuote () {
<i className="fa fa-arrow-right" />
</div>
</div>
<div className="view-quote__metamask-rate">
<p className="view-quote__metamask-rate-text">{ t('swapQuoteIncludesRate', [metaMaskFee]) }</p>
<InfoTooltip
position="top"
contentText={t('swapMetaMaskFeeDescription', [metaMaskFee])}
wrapperClassName="view-quote__metamask-rate-info-icon"
/>
</div>
<div
className={classnames('view-quote__fee-card-container', {
'view-quote__fee-card-container--thin': showWarning,

@ -122,7 +122,7 @@ export default class UnlockPage extends Component {
style={style}
disabled={!this.state.password}
fullWidth
variant="raised"
variant="contained"
size="large"
onClick={this.handleSubmit}
disableRipple

@ -88,16 +88,31 @@ export function getSafeLowEstimate (state) {
return safeLow
}
export function isCustomPriceSafe (state) {
export function getAverageEstimate (state) {
const {
gas: {
basicEstimates: {
average,
},
},
} = state
return average
}
export function isCustomPriceSafe (state, averageIsSafe) {
const safeLow = getSafeLowEstimate(state)
const average = getAverageEstimate(state)
const safeMinimumPrice = averageIsSafe ? average : safeLow
const customGasPrice = getCustomGasPrice(state)
if (!customGasPrice) {
return true
}
if (safeLow === null) {
return null
if (safeMinimumPrice === null) {
return false
}
const customPriceSafe = conversionGreaterThan(
@ -107,7 +122,7 @@ export function isCustomPriceSafe (state) {
fromDenomination: 'WEI',
toDenomination: 'GWEI',
},
{ value: safeLow, fromNumericBase: 'dec' },
{ value: safeMinimumPrice, fromNumericBase: 'dec' },
)
return customPriceSafe
@ -216,7 +231,7 @@ export function getRenderableBasicEstimateData (state, gasLimit, useFastestButto
},
} = state
const slowEstimatData = {
const slowEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.SLOW,
feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit),
feeInSecondaryCurrency: showFiat
@ -234,7 +249,7 @@ export function getRenderableBasicEstimateData (state, gasLimit, useFastestButto
timeEstimate: avgWait && getRenderableTimeEstimate(avgWait),
priceInHexWei: getGasPriceInHexWei(average),
}
const fastEstimatData = {
const fastEstimateData = {
gasEstimateType: GAS_ESTIMATE_TYPES.FAST,
feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit),
feeInSecondaryCurrency: showFiat
@ -254,8 +269,8 @@ export function getRenderableBasicEstimateData (state, gasLimit, useFastestButto
}
return useFastestButtons
? [averageEstimateData, fastEstimatData, fastestEstimateData]
: [slowEstimatData, averageEstimateData, fastEstimatData]
? [fastEstimateData, fastestEstimateData]
: [slowEstimateData, averageEstimateData, fastEstimateData]
}
export function getRenderableEstimateDataForSmallButtonsFromGWEI (state) {

@ -346,13 +346,6 @@ describe('custom-gas selectors', function () {
},
{
expectedResult: [
{
gasEstimateType: 'AVERAGE',
feeInPrimaryCurrency: '0.000147 ETH',
feeInSecondaryCurrency: '$0.38',
priceInHexWei: '0x1a13b8600',
timeEstimate: '~10 min 6 sec',
},
{
gasEstimateType: 'FAST',
feeInSecondaryCurrency: '$0.54',

Loading…
Cancel
Save