{this.context.t('setGasPrice')}
diff --git a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
index 7cbe8d0df..9ff01493a 100644
--- a/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
+++ b/ui/app/components/send/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js
@@ -2,7 +2,7 @@ import React from 'react'
import assert from 'assert'
import {shallow} from 'enzyme'
import GasFeeDisplay from '../gas-fee-display.component'
-import CurrencyDisplay from '../../../../../send/currency-display'
+import UserPreferencedCurrencyDisplay from '../../../../../user-preferenced-currency-display'
import sinon from 'sinon'
@@ -29,17 +29,15 @@ describe('SendGasRow Component', function () {
describe('render', () => {
it('should render a CurrencyDisplay component', () => {
- assert.equal(wrapper.find(CurrencyDisplay).length, 1)
+ assert.equal(wrapper.find(UserPreferencedCurrencyDisplay).length, 2)
})
it('should render the CurrencyDisplay with the correct props', () => {
const {
- conversionRate,
- convertedCurrency,
+ type,
value,
- } = wrapper.find(CurrencyDisplay).props()
- assert.equal(conversionRate, 20)
- assert.equal(convertedCurrency, 'mockConvertedCurrency')
+ } = wrapper.find(UserPreferencedCurrencyDisplay).at(0).props()
+ assert.equal(type, 'PRIMARY')
assert.equal(value, 'mockGasTotal')
})
diff --git a/ui/app/components/token-input/index.js b/ui/app/components/token-input/index.js
new file mode 100644
index 000000000..22c06111e
--- /dev/null
+++ b/ui/app/components/token-input/index.js
@@ -0,0 +1 @@
+export { default } from './token-input.container'
diff --git a/ui/app/components/token-input/tests/token-input.component.test.js b/ui/app/components/token-input/tests/token-input.component.test.js
new file mode 100644
index 000000000..2131e7705
--- /dev/null
+++ b/ui/app/components/token-input/tests/token-input.component.test.js
@@ -0,0 +1,308 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import assert from 'assert'
+import { shallow, mount } from 'enzyme'
+import sinon from 'sinon'
+import { Provider } from 'react-redux'
+import configureMockStore from 'redux-mock-store'
+import TokenInput from '../token-input.component'
+import UnitInput from '../../unit-input'
+import CurrencyDisplay from '../../currency-display'
+
+describe('TokenInput Component', () => {
+ const t = key => `translate ${key}`
+
+ describe('rendering', () => {
+ it('should render properly without a token', () => {
+ const wrapper = shallow(
+
,
+ { context: { t } }
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find(UnitInput).length, 1)
+ })
+
+ it('should render properly with a token', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+
+
+ ,
+ { context: { t },
+ childContextTypes: {
+ t: PropTypes.func,
+ },
+ },
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
+ assert.equal(wrapper.find('.currency-input__conversion-component').length, 1)
+ assert.equal(wrapper.find('.currency-input__conversion-component').text(), 'translate noConversionRateAvailable')
+ })
+
+ it('should render properly with a token and selectedTokenExchangeRate', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+
+
+ ,
+ { context: { t },
+ childContextTypes: {
+ t: PropTypes.func,
+ },
+ },
+ )
+
+ assert.ok(wrapper)
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
+ assert.equal(wrapper.find(CurrencyDisplay).length, 1)
+ })
+
+ it('should render properly with a token value for ETH', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+
+
+
+ )
+
+ assert.ok(wrapper)
+ const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
+ assert.equal(tokenInputInstance.state.decimalValue, 1)
+ assert.equal(tokenInputInstance.state.hexValue, '2710')
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
+ assert.equal(wrapper.find('.unit-input__input').props().value, '1')
+ assert.equal(wrapper.find('.currency-display-component').text(), '2 ETH')
+ })
+
+ it('should render properly with a token value for fiat', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+
+ const wrapper = mount(
+
+
+
+ )
+
+ assert.ok(wrapper)
+ const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
+ assert.equal(tokenInputInstance.state.decimalValue, 1)
+ assert.equal(tokenInputInstance.state.hexValue, '2710')
+ assert.equal(wrapper.find('.unit-input__suffix').length, 1)
+ assert.equal(wrapper.find('.unit-input__suffix').text(), 'ABC')
+ assert.equal(wrapper.find('.unit-input__input').props().value, '1')
+ assert.equal(wrapper.find('.currency-display-component').text(), '$462.12 USD')
+ })
+ })
+
+ describe('handling actions', () => {
+ const handleChangeSpy = sinon.spy()
+ const handleBlurSpy = sinon.spy()
+
+ afterEach(() => {
+ handleChangeSpy.resetHistory()
+ handleBlurSpy.resetHistory()
+ })
+
+ it('should call onChange and onBlur on input changes with the hex value for ETH', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+ const wrapper = mount(
+
+
+
+ )
+
+ assert.ok(wrapper)
+ assert.equal(handleChangeSpy.callCount, 0)
+ assert.equal(handleBlurSpy.callCount, 0)
+
+ const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
+ assert.equal(tokenInputInstance.state.decimalValue, 0)
+ assert.equal(tokenInputInstance.state.hexValue, undefined)
+ assert.equal(wrapper.find('.currency-display-component').text(), '0 ETH')
+ const input = wrapper.find('input')
+ assert.equal(input.props().value, 0)
+
+ input.simulate('change', { target: { value: 1 } })
+ assert.equal(handleChangeSpy.callCount, 1)
+ assert.ok(handleChangeSpy.calledWith('2710'))
+ assert.equal(wrapper.find('.currency-display-component').text(), '2 ETH')
+ assert.equal(tokenInputInstance.state.decimalValue, 1)
+ assert.equal(tokenInputInstance.state.hexValue, '2710')
+
+ assert.equal(handleBlurSpy.callCount, 0)
+ input.simulate('blur')
+ assert.equal(handleBlurSpy.callCount, 1)
+ assert.ok(handleBlurSpy.calledWith('2710'))
+ })
+
+ it('should call onChange and onBlur on input changes with the hex value for fiat', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+ const wrapper = mount(
+
+
+
+ )
+
+ assert.ok(wrapper)
+ assert.equal(handleChangeSpy.callCount, 0)
+ assert.equal(handleBlurSpy.callCount, 0)
+
+ const tokenInputInstance = wrapper.find(TokenInput).at(0).instance()
+ assert.equal(tokenInputInstance.state.decimalValue, 0)
+ assert.equal(tokenInputInstance.state.hexValue, undefined)
+ assert.equal(wrapper.find('.currency-display-component').text(), '$0.00 USD')
+ const input = wrapper.find('input')
+ assert.equal(input.props().value, 0)
+
+ input.simulate('change', { target: { value: 1 } })
+ assert.equal(handleChangeSpy.callCount, 1)
+ assert.ok(handleChangeSpy.calledWith('2710'))
+ assert.equal(wrapper.find('.currency-display-component').text(), '$462.12 USD')
+ assert.equal(tokenInputInstance.state.decimalValue, 1)
+ assert.equal(tokenInputInstance.state.hexValue, '2710')
+
+ assert.equal(handleBlurSpy.callCount, 0)
+ input.simulate('blur')
+ assert.equal(handleBlurSpy.callCount, 1)
+ assert.ok(handleBlurSpy.calledWith('2710'))
+ })
+
+ it('should change the state and pass in a new decimalValue when props.value changes', () => {
+ const mockStore = {
+ metamask: {
+ currentCurrency: 'usd',
+ conversionRate: 231.06,
+ },
+ }
+ const store = configureMockStore()(mockStore)
+ const wrapper = shallow(
+
+
+
+ )
+
+ assert.ok(wrapper)
+ const tokenInputInstance = wrapper.find(TokenInput).dive()
+ assert.equal(tokenInputInstance.state('decimalValue'), 0)
+ assert.equal(tokenInputInstance.state('hexValue'), undefined)
+ assert.equal(tokenInputInstance.find(UnitInput).props().value, 0)
+
+ tokenInputInstance.setProps({ value: '2710' })
+ tokenInputInstance.update()
+ assert.equal(tokenInputInstance.state('decimalValue'), 1)
+ assert.equal(tokenInputInstance.state('hexValue'), '2710')
+ assert.equal(tokenInputInstance.find(UnitInput).props().value, 1)
+ })
+ })
+})
diff --git a/ui/app/components/token-input/tests/token-input.container.test.js b/ui/app/components/token-input/tests/token-input.container.test.js
new file mode 100644
index 000000000..d73bc9a94
--- /dev/null
+++ b/ui/app/components/token-input/tests/token-input.container.test.js
@@ -0,0 +1,129 @@
+import assert from 'assert'
+import proxyquire from 'proxyquire'
+
+let mapStateToProps, mergeProps
+
+proxyquire('../token-input.container.js', {
+ 'react-redux': {
+ connect: (ms, md, mp) => {
+ mapStateToProps = ms
+ mergeProps = mp
+ return () => ({})
+ },
+ },
+})
+
+describe('TokenInput container', () => {
+ describe('mapStateToProps()', () => {
+ it('should return the correct props when send is empty', () => {
+ const mockState = {
+ metamask: {
+ currentCurrency: 'usd',
+ tokens: [
+ {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ ],
+ selectedTokenAddress: '0x1',
+ contractExchangeRates: {},
+ send: {},
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ selectedTokenExchangeRate: 0,
+ })
+ })
+
+ it('should return the correct props when selectedTokenAddress is not found and send is populated', () => {
+ const mockState = {
+ metamask: {
+ currentCurrency: 'usd',
+ tokens: [
+ {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ ],
+ selectedTokenAddress: '0x2',
+ contractExchangeRates: {},
+ send: {
+ token: { address: 'test' },
+ },
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: 'test',
+ },
+ selectedTokenExchangeRate: 0,
+ })
+ })
+
+ it('should return the correct props when contractExchangeRates is populated', () => {
+ const mockState = {
+ metamask: {
+ currentCurrency: 'usd',
+ tokens: [
+ {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ ],
+ selectedTokenAddress: '0x1',
+ contractExchangeRates: {
+ '0x1': 5,
+ },
+ send: {},
+ },
+ }
+
+ assert.deepEqual(mapStateToProps(mockState), {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ selectedTokenExchangeRate: 5,
+ })
+ })
+ })
+
+ describe('mergeProps()', () => {
+ it('should return the correct props', () => {
+ const mockStateProps = {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ selectedTokenExchangeRate: 5,
+ }
+
+ assert.deepEqual(mergeProps(mockStateProps, {}, {}), {
+ currentCurrency: 'usd',
+ selectedToken: {
+ address: '0x1',
+ decimals: '4',
+ symbol: 'ABC',
+ },
+ selectedTokenExchangeRate: 5,
+ suffix: 'ABC',
+ })
+ })
+ })
+})
diff --git a/ui/app/components/token-input/token-input.component.js b/ui/app/components/token-input/token-input.component.js
new file mode 100644
index 000000000..d1388945b
--- /dev/null
+++ b/ui/app/components/token-input/token-input.component.js
@@ -0,0 +1,136 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import UnitInput from '../unit-input'
+import CurrencyDisplay from '../currency-display'
+import { getWeiHexFromDecimalValue } from '../../helpers/conversions.util'
+import ethUtil from 'ethereumjs-util'
+import { conversionUtil, multiplyCurrencies } from '../../conversion-util'
+import { ETH } from '../../constants/common'
+
+/**
+ * Component that allows user to enter token values as a number, and props receive a converted
+ * hex value. props.value, used as a default or forced value, should be a hex value, which
+ * gets converted into a decimal value.
+ */
+export default class TokenInput extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ currentCurrency: PropTypes.string,
+ onChange: PropTypes.func,
+ onBlur: PropTypes.func,
+ value: PropTypes.string,
+ suffix: PropTypes.string,
+ showFiat: PropTypes.bool,
+ selectedToken: PropTypes.object,
+ selectedTokenExchangeRate: PropTypes.number,
+ }
+
+ constructor (props) {
+ super(props)
+
+ const { value: hexValue } = props
+ const decimalValue = hexValue ? this.getDecimalValue(props) : 0
+
+ this.state = {
+ decimalValue,
+ hexValue,
+ }
+ }
+
+ componentDidUpdate (prevProps) {
+ const { value: prevPropsHexValue } = prevProps
+ const { value: propsHexValue } = this.props
+ const { hexValue: stateHexValue } = this.state
+
+ if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) {
+ const decimalValue = this.getDecimalValue(this.props)
+ this.setState({ hexValue: propsHexValue, decimalValue })
+ }
+ }
+
+ getDecimalValue (props) {
+ const { value: hexValue, selectedToken: { decimals, symbol } = {} } = props
+
+ const multiplier = Math.pow(10, Number(decimals || 0))
+ const decimalValueString = conversionUtil(ethUtil.addHexPrefix(hexValue), {
+ fromNumericBase: 'hex',
+ toNumericBase: 'dec',
+ toCurrency: symbol,
+ conversionRate: multiplier,
+ invertConversionRate: true,
+ })
+
+ return Number(decimalValueString) || 0
+ }
+
+ handleChange = decimalValue => {
+ const { selectedToken: { decimals } = {}, onChange } = this.props
+
+ const multiplier = Math.pow(10, Number(decimals || 0))
+ const hexValue = multiplyCurrencies(decimalValue || 0, multiplier, { toNumericBase: 'hex' })
+
+ this.setState({ hexValue, decimalValue })
+ onChange(hexValue)
+ }
+
+ handleBlur = () => {
+ this.props.onBlur(this.state.hexValue)
+ }
+
+ renderConversionComponent () {
+ const { selectedTokenExchangeRate, showFiat, currentCurrency } = this.props
+ const { decimalValue } = this.state
+ let currency, numberOfDecimals
+
+ if (showFiat) {
+ // Display Fiat
+ currency = currentCurrency
+ numberOfDecimals = 2
+ } else {
+ // Display ETH
+ currency = ETH
+ numberOfDecimals = 6
+ }
+
+ const decimalEthValue = (decimalValue * selectedTokenExchangeRate) || 0
+ const hexWeiValue = getWeiHexFromDecimalValue({
+ value: decimalEthValue,
+ fromCurrency: ETH,
+ fromDenomination: ETH,
+ })
+
+ return selectedTokenExchangeRate
+ ? (
+
+ ) : (
+
+ { this.context.t('noConversionRateAvailable') }
+
+ )
+ }
+
+ render () {
+ const { suffix, ...restProps } = this.props
+ const { decimalValue } = this.state
+
+ return (
+
+ { this.renderConversionComponent() }
+
+ )
+ }
+}
diff --git a/ui/app/components/token-input/token-input.container.js b/ui/app/components/token-input/token-input.container.js
new file mode 100644
index 000000000..ec233b1b8
--- /dev/null
+++ b/ui/app/components/token-input/token-input.container.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux'
+import TokenInput from './token-input.component'
+import { getSelectedToken, getSelectedTokenExchangeRate } from '../../selectors'
+
+const mapStateToProps = state => {
+ const { metamask: { currentCurrency } } = state
+
+ return {
+ currentCurrency,
+ selectedToken: getSelectedToken(state),
+ selectedTokenExchangeRate: getSelectedTokenExchangeRate(state),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { selectedToken } = stateProps
+ const suffix = selectedToken && selectedToken.symbol
+
+ return {
+ ...stateProps,
+ ...dispatchProps,
+ ...ownProps,
+ suffix,
+ }
+}
+
+export default connect(mapStateToProps, null, mergeProps)(TokenInput)
diff --git a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js
index 32834ff47..97aa9a8f1 100644
--- a/ui/app/components/transaction-activity-log/transaction-activity-log.util.js
+++ b/ui/app/components/transaction-activity-log/transaction-activity-log.util.js
@@ -46,11 +46,15 @@ export function getActivities (transaction) {
if (!Array.isArray(base) && base.status === UNAPPROVED_STATUS && base.txParams) {
const { time, txParams: { value } = {} } = base
return acc.concat(eventCreator(TRANSACTION_CREATED_EVENT, time, value))
+ // An entry in the history may be an array of more sub-entries.
} else if (Array.isArray(base)) {
const events = []
base.forEach(entry => {
- const { op, path, value, timestamp } = entry
+ const { op, path, value, timestamp: entryTimestamp } = entry
+ // Not all sub-entries in a history entry have a timestamp. If the sub-entry does not have a
+ // timestamp, the first sub-entry in a history entry should.
+ const timestamp = entryTimestamp || base[0] && base[0].timestamp
if (path in eventPathsHash && op === REPLACE_OP) {
switch (path) {
diff --git a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js
index bb6075e9f..77bedcad7 100644
--- a/ui/app/components/transaction-breakdown/transaction-breakdown.component.js
+++ b/ui/app/components/transaction-breakdown/transaction-breakdown.component.js
@@ -4,8 +4,9 @@ import classnames from 'classnames'
import TransactionBreakdownRow from './transaction-breakdown-row'
import Card from '../card'
import CurrencyDisplay from '../currency-display'
+import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
import HexToDecimal from '../hex-to-decimal'
-import { ETH, GWEI } from '../../constants/common'
+import { ETH, GWEI, PRIMARY, SECONDARY } from '../../constants/common'
import { getHexGasTotal } from '../../helpers/confirm-transaction/util'
import { sumHexes } from '../../helpers/transactions.util'
@@ -26,8 +27,11 @@ export default class TransactionBreakdown extends PureComponent {
render () {
const { t } = this.context
const { transaction, className } = this.props
- const { txParams: { gas, gasPrice, value } = {} } = transaction
- const hexGasTotal = getHexGasTotal({ gasLimit: gas, gasPrice })
+ const { txParams: { gas, gasPrice, value } = {}, txReceipt: { gasUsed } = {} } = transaction
+
+ const gasLimit = typeof gasUsed === 'string' ? gasUsed : gas
+
+ const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice })
const totalInHex = sumHexes(hexGasTotal, value)
return (
@@ -37,9 +41,9 @@ export default class TransactionBreakdown extends PureComponent {
className="transaction-breakdown__card"
>
-
@@ -52,6 +56,19 @@ export default class TransactionBreakdown extends PureComponent {
value={gas}
/>
+ {
+ typeof gasUsed === 'string' && (
+
+
+
+ )
+ }
-
-
diff --git a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
index 13cb51349..a4f28fd63 100644
--- a/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
+++ b/ui/app/components/transaction-list-item-details/transaction-list-item-details.component.js
@@ -5,6 +5,7 @@ import { CARDS_VARIANT } from '../sender-to-recipient/sender-to-recipient.consta
import TransactionActivityLog from '../transaction-activity-log'
import TransactionBreakdown from '../transaction-breakdown'
import Button from '../button'
+import Tooltip from '../tooltip'
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
export default class TransactionListItemDetails extends PureComponent {
@@ -75,13 +76,15 @@ export default class TransactionListItemDetails extends PureComponent {
)
}
-
+
+
+