Merge pull request #4800 from MetaMask/i4785-confirm-screen-fixes

Add fallback when no function found, fix network colors, add fiat values for tokens with contract exchange rates
feature/default_network_editable
Alexander Tseung 6 years ago committed by GitHub
commit f6ca06f775
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      app/_locales/en/messages.json
  2. 16
      test/e2e/beta/metamask-beta-ui.spec.js
  3. 67
      test/unit/components/pending-tx-test.js
  4. 18
      ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js
  5. 6
      ui/app/components/network-display/index.scss
  6. 19
      ui/app/components/pages/confirm-approve/confirm-approve.component.js
  7. 19
      ui/app/components/pages/confirm-approve/confirm-approve.container.js
  8. 20
      ui/app/components/pages/confirm-send-token/confirm-send-token.component.js
  9. 26
      ui/app/components/pages/confirm-send-token/confirm-send-token.container.js
  10. 19
      ui/app/components/pages/confirm-send-token/index.scss
  11. 85
      ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
  12. 34
      ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
  13. 2
      ui/app/components/pages/confirm-token-transaction-base/index.js
  14. 40
      ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
  15. 20
      ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
  16. 1
      ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
  17. 7
      ui/app/components/pages/confirm-transaction/confirm-transaction.component.js
  18. 2
      ui/app/components/pages/index.scss
  19. 358
      ui/app/components/pending-tx/confirm-deploy-contract.js
  20. 692
      ui/app/components/pending-tx/confirm-send-ether.js
  21. 696
      ui/app/components/pending-tx/confirm-send-token.js
  22. 165
      ui/app/components/pending-tx/index.js
  23. 136
      ui/app/conf-tx.js
  24. 17
      ui/app/helpers/confirm-transaction/util.js
  25. 2
      ui/app/routes.js
  26. 99
      ui/app/selectors/confirm-transaction.js

@ -593,6 +593,9 @@
"noTransactions": { "noTransactions": {
"message": "No Transactions" "message": "No Transactions"
}, },
"notFound": {
"message": "Not Found"
},
"notStarted": { "notStarted": {
"message": "Not Started" "message": "Not Started"
}, },
@ -972,6 +975,9 @@
"unknown": { "unknown": {
"message": "Unknown" "message": "Unknown"
}, },
"unknownFunction": {
"message": "Unknown Function"
},
"unknownNetwork": { "unknownNetwork": {
"message": "Unknown Private Network" "message": "Unknown Private Network"
}, },

@ -513,7 +513,7 @@ describe('MetaMask', function () {
it('displays the contract creation data', async () => { it('displays the contract creation data', async () => {
const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
dataTab.click() dataTab.click()
await (regularDelayMs) await delay(regularDelayMs)
await findElement(driver, By.xpath(`//div[contains(text(), '127.0.0.1')]`)) await findElement(driver, By.xpath(`//div[contains(text(), '127.0.0.1')]`))
@ -523,7 +523,7 @@ describe('MetaMask', function () {
const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
detailsTab.click() detailsTab.click()
await (regularDelayMs) await delay(regularDelayMs)
}) })
it('confirms a deploy contract transaction', async () => { it('confirms a deploy contract transaction', async () => {
@ -548,7 +548,7 @@ describe('MetaMask', function () {
await delay(regularDelayMs) await delay(regularDelayMs)
await driver.switchTo().window(extension) await driver.switchTo().window(extension)
await delay(regularDelayMs) await delay(largeDelayMs)
await findElements(driver, By.css('.tx-list-pending-item-container')) await findElements(driver, By.css('.tx-list-pending-item-container'))
const [txListValue] = await findElements(driver, By.css('.tx-list-value')) const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
@ -741,7 +741,7 @@ describe('MetaMask', function () {
it('displays the token transfer data', async () => { it('displays the token transfer data', async () => {
const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
dataTab.click() dataTab.click()
await (regularDelayMs) await delay(regularDelayMs)
const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type')) const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type'))
const functionTypeText = await functionType.getText() const functionTypeText = await functionType.getText()
@ -753,7 +753,7 @@ describe('MetaMask', function () {
const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
detailsTab.click() detailsTab.click()
await (regularDelayMs) await delay(regularDelayMs)
}) })
it('submits the transaction', async function () { it('submits the transaction', async function () {
@ -901,7 +901,7 @@ describe('MetaMask', function () {
it('displays the token approval data', async () => { it('displays the token approval data', async () => {
const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
dataTab.click() dataTab.click()
await (regularDelayMs) await delay(regularDelayMs)
const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type')) const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type'))
const functionTypeText = await functionType.getText() const functionTypeText = await functionType.getText()
@ -913,12 +913,12 @@ describe('MetaMask', function () {
const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
detailsTab.click() detailsTab.click()
await (regularDelayMs) await delay(regularDelayMs)
const approvalWarning = await findElement(driver, By.css('.confirm-page-container-warning__warning')) const approvalWarning = await findElement(driver, By.css('.confirm-page-container-warning__warning'))
const approvalWarningText = await approvalWarning.getText() const approvalWarningText = await approvalWarning.getText()
assert(approvalWarningText.match(/By approving this/)) assert(approvalWarningText.match(/By approving this/))
await (regularDelayMs) await delay(regularDelayMs)
}) })
it('opens the gas edit modal', async () => { it('opens the gas edit modal', async () => {

@ -1,67 +0,0 @@
const assert = require('assert')
const h = require('react-hyperscript')
const PendingTx = require('../../../ui/app/components/pending-tx')
const ethUtil = require('ethereumjs-util')
const { createMockStore } = require('redux-test-utils')
const { shallowWithStore } = require('../../lib/shallow-with-store')
const identities = { abc: {}, def: {} }
const mockState = {
metamask: {
accounts: { abc: {} },
identities,
conversionRate: 10,
selectedAddress: 'abc',
},
}
describe('PendingTx', function () {
const gasPrice = '0x4A817C800' // 20 Gwei
const txData = {
'id': 5021615666270214,
'time': 1494458763011,
'status': 'unapproved',
'metamaskNetworkId': '1494442339676',
'txParams': {
'from': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b826',
'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
'value': '0xde0b6b3a7640000',
gasPrice,
'gas': '0x7b0c',
},
'gasLimitSpecified': false,
'estimatedGas': '0x5208',
}
const newGasPrice = '0x77359400'
const computedBalances = {}
computedBalances[Object.keys(identities)[0]] = {
ethBalance: '0x00000000000000056bc75e2d63100000',
}
const props = {
txData,
computedBalances,
sendTransaction: (txMeta, event) => {
// Assert changes:
const result = ethUtil.addHexPrefix(txMeta.txParams.gasPrice)
assert.notEqual(result, gasPrice, 'gas price should change')
assert.equal(result, newGasPrice, 'gas price assigned.')
},
}
let pendingTxComponent
let store
let component
beforeEach(function () {
store = createMockStore(mockState)
component = shallowWithStore(h(PendingTx, props), store)
pendingTxComponent = component
})
it('should render correctly', function (done) {
assert.equal(pendingTxComponent.props().identities, identities)
done()
})
})

@ -5,10 +5,10 @@ import classnames from 'classnames'
const ConfirmDetailRow = props => { const ConfirmDetailRow = props => {
const { const {
label, label,
fiatFee, fiatText,
ethFee, ethText,
onHeaderClick, onHeaderClick,
fiatFeeColor, fiatTextColor,
headerText, headerText,
headerTextClassName, headerTextClassName,
} = props } = props
@ -27,12 +27,12 @@ const ConfirmDetailRow = props => {
</div> </div>
<div <div
className="confirm-detail-row__fiat" className="confirm-detail-row__fiat"
style={{ color: fiatFeeColor }} style={{ color: fiatTextColor }}
> >
{ fiatFee } { fiatText }
</div> </div>
<div className="confirm-detail-row__eth"> <div className="confirm-detail-row__eth">
{ `\u2666 ${ethFee}` } { ethText }
</div> </div>
</div> </div>
</div> </div>
@ -41,9 +41,9 @@ const ConfirmDetailRow = props => {
ConfirmDetailRow.propTypes = { ConfirmDetailRow.propTypes = {
label: PropTypes.string, label: PropTypes.string,
fiatFee: PropTypes.string, fiatText: PropTypes.string,
ethFee: PropTypes.string, ethText: PropTypes.string,
fiatFeeColor: PropTypes.string, fiatTextColor: PropTypes.string,
onHeaderClick: PropTypes.func, onHeaderClick: PropTypes.func,
headerText: PropTypes.string, headerText: PropTypes.string,
headerTextClassName: PropTypes.string, headerTextClassName: PropTypes.string,

@ -9,7 +9,7 @@
height: 25px; height: 25px;
&--mainnet { &--mainnet {
background-color: lighten($blue-lagoon, 45%); background-color: lighten($blue-lagoon, 68%);
} }
&--ropsten { &--ropsten {
@ -17,11 +17,11 @@
} }
&--kovan { &--kovan {
background-color: lighten($purple, 45%); background-color: lighten($purple, 65%);
} }
&--rinkeby { &--rinkeby {
background-color: lighten($tulip-tree, 45%); background-color: lighten($tulip-tree, 35%);
} }
} }

@ -1,29 +1,20 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import ConfirmTransactionBase from '../confirm-transaction-base' import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
export default class ConfirmApprove extends Component { export default class ConfirmApprove extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = { static propTypes = {
tokenAddress: PropTypes.string, tokenAmount: PropTypes.number,
toAddress: PropTypes.string,
tokenAmount: PropTypes.string,
tokenSymbol: PropTypes.string, tokenSymbol: PropTypes.string,
} }
render () { render () {
const { toAddress, tokenAddress, tokenAmount, tokenSymbol } = this.props const { tokenAmount, tokenSymbol } = this.props
return ( return (
<ConfirmTransactionBase <ConfirmTokenTransactionBase
toAddress={toAddress} tokenAmount={tokenAmount}
identiconAddress={tokenAddress}
title={`${tokenAmount} ${tokenSymbol}`}
warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`} warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`}
hideSubtitle
/> />
) )
} }

@ -1,25 +1,12 @@
import { connect } from 'react-redux' import { connect } from 'react-redux'
import ConfirmApprove from './confirm-approve.component' import ConfirmApprove from './confirm-approve.component'
import { approveTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction'
const mapStateToProps = state => { const mapStateToProps = state => {
const { confirmTransaction } = state const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state
const { const { tokenAmount } = approveTokenAmountAndToAddressSelector(state)
tokenData = {},
txData: { txParams: { to: tokenAddress } = {} } = {},
tokenProps: { tokenSymbol } = {},
} = confirmTransaction
const { params = [] } = tokenData
let toAddress = ''
let tokenAmount = ''
if (params && params.length === 2) {
[{ value: toAddress }, { value: tokenAmount }] = params
}
return { return {
toAddress,
tokenAddress,
tokenAmount, tokenAmount,
tokenSymbol, tokenSymbol,
} }

@ -1,20 +1,13 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import ConfirmTransactionBase from '../confirm-transaction-base' import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
import { SEND_ROUTE } from '../../../routes' import { SEND_ROUTE } from '../../../routes'
export default class ConfirmSendToken extends Component { export default class ConfirmSendToken extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = { static propTypes = {
history: PropTypes.object, history: PropTypes.object,
tokenAddress: PropTypes.string,
toAddress: PropTypes.string,
numberOfTokens: PropTypes.number,
tokenSymbol: PropTypes.string,
editTransaction: PropTypes.func, editTransaction: PropTypes.func,
tokenAmount: PropTypes.number,
} }
handleEdit (confirmTransactionData) { handleEdit (confirmTransactionData) {
@ -24,15 +17,12 @@ export default class ConfirmSendToken extends Component {
} }
render () { render () {
const { toAddress, tokenAddress, tokenSymbol, numberOfTokens } = this.props const { tokenAmount } = this.props
return ( return (
<ConfirmTransactionBase <ConfirmTokenTransactionBase
toAddress={toAddress}
identiconAddress={tokenAddress}
title={`${numberOfTokens} ${tokenSymbol}`}
onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)} onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
hideSubtitle tokenAmount={tokenAmount}
/> />
) )
} }

@ -2,36 +2,16 @@ import { connect } from 'react-redux'
import { compose } from 'recompose' import { compose } from 'recompose'
import { withRouter } from 'react-router-dom' import { withRouter } from 'react-router-dom'
import ConfirmSendToken from './confirm-send-token.component' import ConfirmSendToken from './confirm-send-token.component'
import { calcTokenAmount } from '../../../token-util'
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck' import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck'
import { setSelectedToken, updateSend, showSendTokenPage } from '../../../actions' import { setSelectedToken, updateSend, showSendTokenPage } from '../../../actions'
import { conversionUtil } from '../../../conversion-util' import { conversionUtil } from '../../../conversion-util'
import { sendTokenTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction'
const mapStateToProps = state => { const mapStateToProps = state => {
const { confirmTransaction } = state const { tokenAmount } = sendTokenTokenAmountAndToAddressSelector(state)
const {
tokenData = {},
tokenProps: { tokenSymbol, tokenDecimals } = {},
txData: { txParams: { to: tokenAddress } = {} } = {},
} = confirmTransaction
const { params = [] } = tokenData
let toAddress = ''
let tokenAmount = ''
if (params && params.length === 2) {
[{ value: toAddress }, { value: tokenAmount }] = params
}
const numberOfTokens = tokenAmount && tokenDecimals
? calcTokenAmount(tokenAmount, tokenDecimals)
: 0
return { return {
toAddress, tokenAmount,
tokenAddress,
tokenSymbol,
numberOfTokens,
} }
} }

@ -1,19 +0,0 @@
.confirm-send-token {
&__title {
padding: 4px 0;
display: flex;
align-items: center;
}
&__identicon {
flex: 0 0 auto;
}
&__title-text {
font-size: 2.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 8px;
}
}

@ -0,0 +1,85 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ConfirmTransactionBase from '../confirm-transaction-base'
import {
formatCurrency,
convertTokenToFiat,
addFiat,
} from '../../../helpers/confirm-transaction/util'
export default class ConfirmTokenTransactionBase extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
tokenAddress: PropTypes.string,
toAddress: PropTypes.string,
tokenAmount: PropTypes.number,
tokenSymbol: PropTypes.string,
fiatTransactionTotal: PropTypes.string,
ethTransactionTotal: PropTypes.string,
contractExchangeRate: PropTypes.number,
conversionRate: PropTypes.number,
currentCurrency: PropTypes.string,
}
getFiatTransactionAmount () {
const { tokenAmount, currentCurrency, conversionRate, contractExchangeRate } = this.props
return convertTokenToFiat({
value: tokenAmount,
toCurrency: currentCurrency,
conversionRate,
contractExchangeRate,
})
}
getSubtitle () {
const { currentCurrency, contractExchangeRate } = this.props
if (typeof contractExchangeRate === 'undefined') {
return this.context.t('noConversionRateAvailable')
} else {
const fiatTransactionAmount = this.getFiatTransactionAmount()
return formatCurrency(fiatTransactionAmount, currentCurrency)
}
}
getFiatTotalTextOverride () {
const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props
if (typeof contractExchangeRate === 'undefined') {
return formatCurrency(fiatTransactionTotal, currentCurrency)
} else {
const fiatTransactionAmount = this.getFiatTransactionAmount()
const fiatTotal = addFiat(fiatTransactionAmount, fiatTransactionTotal)
return formatCurrency(fiatTotal, currentCurrency)
}
}
render () {
const {
toAddress,
tokenAddress,
tokenSymbol,
tokenAmount,
ethTransactionTotal,
...restProps
} = this.props
const tokensText = `${tokenAmount} ${tokenSymbol}`
return (
<ConfirmTransactionBase
toAddress={toAddress}
identiconAddress={tokenAddress}
title={tokensText}
subtitle={this.getSubtitle()}
ethTotalTextOverride={`${tokensText} + \u2666 ${ethTransactionTotal}`}
fiatTotalTextOverride={this.getFiatTotalTextOverride()}
{...restProps}
/>
)
}
}

@ -0,0 +1,34 @@
import { connect } from 'react-redux'
import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'
import {
tokenAmountAndToAddressSelector,
contractExchangeRateSelector,
} from '../../../selectors/confirm-transaction'
const mapStateToProps = (state, ownProps) => {
const { tokenAmount: ownTokenAmount } = ownProps
const { confirmTransaction, metamask: { currentCurrency, conversionRate } } = state
const {
txData: { txParams: { to: tokenAddress } = {} } = {},
tokenProps: { tokenSymbol } = {},
fiatTransactionTotal,
ethTransactionTotal,
} = confirmTransaction
const { tokenAmount, toAddress } = tokenAmountAndToAddressSelector(state)
const contractExchangeRate = contractExchangeRateSelector(state)
return {
toAddress,
tokenAddress,
tokenAmount: typeof ownTokenAmount !== 'undefined' ? ownTokenAmount : tokenAmount,
tokenSymbol,
currentCurrency,
conversionRate,
contractExchangeRate,
fiatTransactionTotal,
ethTransactionTotal,
}
}
export default connect(mapStateToProps)(ConfirmTokenTransactionBase)

@ -0,0 +1,2 @@
export { default } from './confirm-token-transaction-base.container'
export { default as ConfirmTokenTransactionBase } from './confirm-token-transaction-base.component'

@ -54,6 +54,8 @@ export default class ConfirmTransactionBase extends Component {
detailsComponent: PropTypes.node, detailsComponent: PropTypes.node,
errorKey: PropTypes.string, errorKey: PropTypes.string,
errorMessage: PropTypes.string, errorMessage: PropTypes.string,
ethTotalTextOverride: PropTypes.string,
fiatTotalTextOverride: PropTypes.string,
hideData: PropTypes.bool, hideData: PropTypes.bool,
hideDetails: PropTypes.bool, hideDetails: PropTypes.bool,
hideSubtitle: PropTypes.bool, hideSubtitle: PropTypes.bool,
@ -146,6 +148,8 @@ export default class ConfirmTransactionBase extends Component {
currentCurrency, currentCurrency,
fiatTransactionTotal, fiatTransactionTotal,
ethTransactionTotal, ethTransactionTotal,
fiatTotalTextOverride,
ethTotalTextOverride,
hideDetails, hideDetails,
} = this.props } = this.props
@ -153,14 +157,16 @@ export default class ConfirmTransactionBase extends Component {
return null return null
} }
const formattedCurrency = formatCurrency(fiatTransactionTotal, currentCurrency)
return ( return (
detailsComponent || ( detailsComponent || (
<div className="confirm-page-container-content__details"> <div className="confirm-page-container-content__details">
<div className="confirm-page-container-content__gas-fee"> <div className="confirm-page-container-content__gas-fee">
<ConfirmDetailRow <ConfirmDetailRow
label="Gas Fee" label="Gas Fee"
fiatFee={formatCurrency(fiatTransactionFee, currentCurrency)} fiatText={formatCurrency(fiatTransactionFee, currentCurrency)}
ethFee={ethTransactionFee} ethText={`\u2666 ${ethTransactionFee}`}
headerText="Edit" headerText="Edit"
headerTextClassName="confirm-detail-row__header-text--edit" headerTextClassName="confirm-detail-row__header-text--edit"
onHeaderClick={() => this.handleEditGas()} onHeaderClick={() => this.handleEditGas()}
@ -169,11 +175,11 @@ export default class ConfirmTransactionBase extends Component {
<div> <div>
<ConfirmDetailRow <ConfirmDetailRow
label="Total" label="Total"
fiatFee={formatCurrency(fiatTransactionTotal, currentCurrency)} fiatText={fiatTotalTextOverride || formattedCurrency}
ethFee={ethTransactionTotal} ethText={ethTotalTextOverride || `\u2666 ${ethTransactionTotal}`}
headerText="Amount + Gas Fee" headerText="Amount + Gas Fee"
headerTextClassName="confirm-detail-row__header-text--total" headerTextClassName="confirm-detail-row__header-text--total"
fiatFeeColor="#2f9ae0" fiatTextColor="#2f9ae0"
/> />
</div> </div>
</div> </div>
@ -206,17 +212,21 @@ export default class ConfirmTransactionBase extends Component {
<div className="confirm-page-container-content__data-box-label"> <div className="confirm-page-container-content__data-box-label">
{`${t('functionType')}:`} {`${t('functionType')}:`}
<span className="confirm-page-container-content__function-type"> <span className="confirm-page-container-content__function-type">
{ name } { name || t('notFound') }
</span> </span>
</div> </div>
<div className="confirm-page-container-content__data-box"> {
<div className="confirm-page-container-content__data-field-label"> params && (
{ `${t('parameters')}:` } <div className="confirm-page-container-content__data-box">
</div> <div className="confirm-page-container-content__data-field-label">
<div> { `${t('parameters')}:` }
<pre>{ JSON.stringify(params, null, 2) }</pre> </div>
</div> <div>
</div> <pre>{ JSON.stringify(params, null, 2) }</pre>
</div>
</div>
)
}
<div className="confirm-page-container-content__data-box-label"> <div className="confirm-page-container-content__data-box-label">
{`${t('hexData')}:`} {`${t('hexData')}:`}
</div> </div>
@ -297,7 +307,7 @@ export default class ConfirmTransactionBase extends Component {
toName={toName} toName={toName}
toAddress={toAddress} toAddress={toAddress}
showEdit={onEdit && !isTxReprice} showEdit={onEdit && !isTxReprice}
action={action || name} action={action || name || this.context.t('unknownFunction')}
title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`} title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`}
subtitle={subtitle || `\u2666 ${ethTransactionAmount}`} subtitle={subtitle || `\u2666 ${ethTransactionAmount}`}
hideSubtitle={hideSubtitle} hideSubtitle={hideSubtitle}

@ -8,11 +8,16 @@ import {
CONFIRM_SEND_ETHER_PATH, CONFIRM_SEND_ETHER_PATH,
CONFIRM_SEND_TOKEN_PATH, CONFIRM_SEND_TOKEN_PATH,
CONFIRM_APPROVE_PATH, CONFIRM_APPROVE_PATH,
CONFIRM_TRANSFER_FROM_PATH,
CONFIRM_TOKEN_METHOD_PATH, CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH, SIGNATURE_REQUEST_PATH,
} from '../../../routes' } from '../../../routes'
import { isConfirmDeployContract } from './confirm-transaction-switch.util' import { isConfirmDeployContract } from './confirm-transaction-switch.util'
import { TOKEN_METHOD_TRANSFER, TOKEN_METHOD_APPROVE } from './confirm-transaction-switch.constants' import {
TOKEN_METHOD_TRANSFER,
TOKEN_METHOD_APPROVE,
TOKEN_METHOD_TRANSFER_FROM,
} from './confirm-transaction-switch.constants'
export default class ConfirmTransactionSwitch extends Component { export default class ConfirmTransactionSwitch extends Component {
static propTypes = { static propTypes = {
@ -27,8 +32,7 @@ export default class ConfirmTransactionSwitch extends Component {
methodData: { name }, methodData: { name },
fetchingMethodData, fetchingMethodData,
} = this.props } = this.props
const { id } = txData const { id, txParams: { data } = {} } = txData
if (isConfirmDeployContract(txData)) { if (isConfirmDeployContract(txData)) {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}` const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`
@ -39,10 +43,10 @@ export default class ConfirmTransactionSwitch extends Component {
return <Loading /> return <Loading />
} }
if (name) { if (data) {
const methodName = name.toLowerCase() const methodName = name && name.toLowerCase()
switch (methodName.toLowerCase()) { switch (methodName) {
case TOKEN_METHOD_TRANSFER: { case TOKEN_METHOD_TRANSFER: {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}` const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}`
return <Redirect to={{ pathname }} /> return <Redirect to={{ pathname }} />
@ -51,6 +55,10 @@ export default class ConfirmTransactionSwitch extends Component {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}` const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}`
return <Redirect to={{ pathname }} /> return <Redirect to={{ pathname }} />
} }
case TOKEN_METHOD_TRANSFER_FROM: {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}`
return <Redirect to={{ pathname }} />
}
default: { default: {
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}` const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}`
return <Redirect to={{ pathname }} /> return <Redirect to={{ pathname }} />

@ -1,2 +1,3 @@
export const TOKEN_METHOD_TRANSFER = 'transfer' export const TOKEN_METHOD_TRANSFER = 'transfer'
export const TOKEN_METHOD_APPROVE = 'approve' export const TOKEN_METHOD_APPROVE = 'approve'
export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'

@ -8,6 +8,7 @@ import ConfirmSendEther from '../confirm-send-ether'
import ConfirmSendToken from '../confirm-send-token' import ConfirmSendToken from '../confirm-send-token'
import ConfirmDeployContract from '../confirm-deploy-contract' import ConfirmDeployContract from '../confirm-deploy-contract'
import ConfirmApprove from '../confirm-approve' import ConfirmApprove from '../confirm-approve'
import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
import ConfTx from '../../../conf-tx' import ConfTx from '../../../conf-tx'
import { import {
DEFAULT_ROUTE, DEFAULT_ROUTE,
@ -16,6 +17,7 @@ import {
CONFIRM_SEND_ETHER_PATH, CONFIRM_SEND_ETHER_PATH,
CONFIRM_SEND_TOKEN_PATH, CONFIRM_SEND_TOKEN_PATH,
CONFIRM_APPROVE_PATH, CONFIRM_APPROVE_PATH,
CONFIRM_TRANSFER_FROM_PATH,
CONFIRM_TOKEN_METHOD_PATH, CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH, SIGNATURE_REQUEST_PATH,
} from '../../../routes' } from '../../../routes'
@ -137,6 +139,11 @@ export default class ConfirmTransaction extends Component {
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`} path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`}
component={ConfirmApprove} component={ConfirmApprove}
/> />
<Route
exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`}
component={ConfirmTokenTransactionBase}
/>
<Route <Route
exact exact
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`} path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`}

@ -3,5 +3,3 @@
@import './add-token/index'; @import './add-token/index';
@import './confirm-add-token/index'; @import './confirm-add-token/index';
@import './confirm-send-token/index';

@ -1,358 +0,0 @@
const { Component } = require('react')
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const PropTypes = require('prop-types')
const actions = require('../../actions')
const clone = require('clone')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
const { conversionUtil } = require('../../conversion-util')
const SenderToRecipient = require('../sender-to-recipient')
const NetworkDisplay = require('../network-display')
const { MIN_GAS_PRICE_HEX } = require('../send/send.constants')
class ConfirmDeployContract extends Component {
constructor (props) {
super(props)
this.state = {
valid: false,
submitting: false,
}
}
onSubmit (event) {
event.preventDefault()
const txMeta = this.gatherTxMeta()
const valid = this.checkValidity()
this.setState({ valid, submitting: true })
if (valid && this.verifyGasParams()) {
this.props.sendTransaction(txMeta, event)
} else {
this.props.displayWarning(this.context.t('invalidGasParams'))
this.setState({ submitting: false })
}
}
cancel (event, txMeta) {
event.preventDefault()
this.props.cancelTransaction(txMeta)
}
checkValidity () {
const form = this.getFormEl()
const valid = form.checkValidity()
return valid
}
getFormEl () {
const form = document.querySelector('form#pending-tx-form')
// Stub out form for unit tests:
if (!form) {
return { checkValidity () { return true } }
}
return form
}
// After a customizable state value has been updated,
gatherTxMeta () {
const props = this.props
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
// log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
return txData
}
verifyGasParams () {
// We call this in case the gas has not been modified at all
if (!this.state) { return true }
return (
this._notZeroOrEmptyString(this.state.gas) &&
this._notZeroOrEmptyString(this.state.gasPrice)
)
}
_notZeroOrEmptyString (obj) {
return obj !== '' && obj !== '0x0'
}
bnMultiplyByFraction (targetBN, numerator, denominator) {
const numBN = new BN(numerator)
const denomBN = new BN(denominator)
return targetBN.mul(numBN).div(denomBN)
}
getData () {
const { identities } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
return {
from: {
address: txParams.from,
name: identities[txParams.from].name,
},
memo: txParams.memo || '',
}
}
getAmount () {
const { conversionRate, currentCurrency } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
const FIAT = conversionUtil(txParams.value, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromCurrency: 'ETH',
toCurrency: currentCurrency,
numberOfDecimals: 2,
fromDenomination: 'WEI',
conversionRate,
})
const ETH = conversionUtil(txParams.value, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromCurrency: 'ETH',
toCurrency: 'ETH',
fromDenomination: 'WEI',
conversionRate,
numberOfDecimals: 6,
})
return {
fiat: Number(FIAT),
token: Number(ETH),
}
}
getGasFee () {
const { conversionRate, currentCurrency } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
// Gas
const gas = txParams.gas
const gasBn = hexToBn(gas)
// Gas Price
const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
const gasPriceBn = hexToBn(gasPrice)
const txFeeBn = gasBn.mul(gasPriceBn)
const FIAT = conversionUtil(txFeeBn, {
fromNumericBase: 'BN',
toNumericBase: 'dec',
fromDenomination: 'WEI',
fromCurrency: 'ETH',
toCurrency: currentCurrency,
numberOfDecimals: 2,
conversionRate,
})
const ETH = conversionUtil(txFeeBn, {
fromNumericBase: 'BN',
toNumericBase: 'dec',
fromDenomination: 'WEI',
fromCurrency: 'ETH',
toCurrency: 'ETH',
numberOfDecimals: 6,
conversionRate,
})
return {
fiat: Number(FIAT),
eth: Number(ETH),
}
}
renderGasFee () {
const { currentCurrency } = this.props
const { fiat: fiatGas, eth: ethGas } = this.getGasFee()
return (
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', `${fiatGas} ${currentCurrency.toUpperCase()}`),
h(
'div.confirm-screen-row-detail',
`${ethGas} ETH`
),
]),
])
)
}
renderHeroAmount () {
const { currentCurrency } = this.props
const { fiat: fiatAmount } = this.getAmount()
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
const { memo = '' } = txParams
return (
h('div.confirm-send-token__hero-amount-wrapper', [
h('h3.flex-center.confirm-screen-send-amount', `${fiatAmount}`),
h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency.toUpperCase()),
h('div.flex-center.confirm-memo-wrapper', [
h('h3.confirm-screen-send-memo', memo),
]),
])
)
}
renderTotalPlusGas () {
const { currentCurrency } = this.props
const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
const { fiat: fiatGas, eth: ethGas } = this.getGasFee()
return (
h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
h('div.confirm-screen-section-column', [
h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', `${fiatAmount + fiatGas} ${currentCurrency.toUpperCase()}`),
h('div.confirm-screen-row-detail', `${tokenAmount + ethGas} ETH`),
]),
])
)
}
render () {
const { backToAccountDetail, selectedAddress } = this.props
const txMeta = this.gatherTxMeta()
const {
from: {
address: fromAddress,
name: fromName,
},
} = this.getData()
this.inputs = []
return (
h('.page-container', [
h('.page-container__header', [
h('.page-container__header-row', [
h('span.page-container__back-button', {
onClick: () => backToAccountDetail(selectedAddress),
}, this.context.t('back')),
window.METAMASK_UI_TYPE === 'notification' && h(NetworkDisplay),
]),
h('.page-container__title', this.context.t('confirmContract')),
h('.page-container__subtitle', this.context.t('pleaseReviewTransaction')),
]),
// Main Send token Card
h('.page-container__content', [
h(SenderToRecipient, {
senderName: fromName,
senderAddress: fromAddress,
}),
// h('h3.flex-center.confirm-screen-sending-to-message', {
// style: {
// textAlign: 'center',
// fontSize: '16px',
// },
// }, [
// `You're deploying a new contract.`,
// ]),
this.renderHeroAmount(),
h('div.confirm-screen-rows', [
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', fromName),
h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
]),
]),
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', this.context.t('newContract')),
]),
]),
this.renderGasFee(),
this.renderTotalPlusGas(),
]),
]),
h('form#pending-tx-form', {
onSubmit: event => this.onSubmit(event),
}, [
h('.page-container__footer', [
// Cancel Button
h('button.btn-cancel.page-container__footer-button.allcaps', {
onClick: event => this.cancel(event, txMeta),
}, this.context.t('cancel')),
// Accept Button
h('button.btn-confirm.page-container__footer-button.allcaps', {
onClick: event => this.onSubmit(event),
}, this.context.t('confirm')),
]),
]),
])
)
}
}
ConfirmDeployContract.propTypes = {
sendTransaction: PropTypes.func,
cancelTransaction: PropTypes.func,
backToAccountDetail: PropTypes.func,
displayWarning: PropTypes.func,
identities: PropTypes.object,
conversionRate: PropTypes.number,
currentCurrency: PropTypes.string,
selectedAddress: PropTypes.string,
t: PropTypes.func,
}
const mapStateToProps = state => {
const {
conversionRate,
identities,
currentCurrency,
} = state.metamask
const accounts = state.metamask.accounts
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
return {
currentCurrency,
conversionRate,
identities,
selectedAddress,
}
}
const mapDispatchToProps = dispatch => {
return {
backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
displayWarning: warning => actions.displayWarning(warning),
}
}
ConfirmDeployContract.contextTypes = {
t: PropTypes.func,
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmDeployContract)

@ -1,692 +0,0 @@
const Component = require('react').Component
const { withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const PropTypes = require('prop-types')
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const inherits = require('util').inherits
const actions = require('../../actions')
const clone = require('clone')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
const classnames = require('classnames')
const {
conversionUtil,
addCurrencies,
multiplyCurrencies,
} = require('../../conversion-util')
const {
calcGasTotal,
isBalanceSufficient,
} = require('../send/send.utils')
const GasFeeDisplay = require('../send/send-content/send-gas-row/gas-fee-display/').default
const SenderToRecipient = require('../sender-to-recipient')
const NetworkDisplay = require('../network-display')
const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies')
const { MIN_GAS_PRICE_HEX } = require('../send/send.constants')
const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
const {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
} = require('../../../../app/scripts/lib/enums')
import {
updateSendErrors,
} from '../../ducks/send.duck'
ConfirmSendEther.contextTypes = {
t: PropTypes.func,
}
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(ConfirmSendEther)
function mapStateToProps (state) {
const {
conversionRate,
identities,
currentCurrency,
send,
} = state.metamask
const accounts = state.metamask.accounts
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
const { balance } = accounts[selectedAddress]
return {
conversionRate,
identities,
selectedAddress,
currentCurrency,
send,
balance,
}
}
function mapDispatchToProps (dispatch) {
return {
clearSend: () => dispatch(actions.clearSend()),
editTransaction: txMeta => {
const { id, txParams } = txMeta
const {
gas: gasLimit,
gasPrice,
to,
value: amount,
} = txParams
dispatch(actions.updateSend({
gasLimit,
gasPrice,
gasTotal: null,
to,
amount,
errors: { to: null, amount: null },
editingTransactionId: id,
}))
},
cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
showCustomizeGasModal: (txMeta, sendGasLimit, sendGasPrice, sendGasTotal) => {
const { id, txParams, lastGasPrice } = txMeta
const { gas: txGasLimit, gasPrice: txGasPrice } = txParams
let forceGasMin
if (lastGasPrice) {
forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
multiplierBase: 10,
toNumericBase: 'hex',
fromDenomination: 'WEI',
}))
}
dispatch(actions.updateSend({
gasLimit: sendGasLimit || txGasLimit,
gasPrice: sendGasPrice || txGasPrice,
editingTransactionId: id,
gasTotal: sendGasTotal,
forceGasMin,
}))
dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
},
updateSendErrors: error => dispatch(updateSendErrors(error)),
}
}
inherits(ConfirmSendEther, Component)
function ConfirmSendEther () {
Component.call(this)
this.state = {}
this.onSubmit = this.onSubmit.bind(this)
}
ConfirmSendEther.prototype.updateComponentSendErrors = function (prevProps) {
const {
balance: oldBalance,
conversionRate: oldConversionRate,
} = prevProps
const {
updateSendErrors,
balance,
conversionRate,
send: {
errors: {
simulationFails,
},
},
} = this.props
const txMeta = this.gatherTxMeta()
const shouldUpdateBalanceSendErrors = balance && [
balance !== oldBalance,
conversionRate !== oldConversionRate,
].some(x => Boolean(x))
if (shouldUpdateBalanceSendErrors) {
const balanceIsSufficient = this.isBalanceSufficient(txMeta)
updateSendErrors({
insufficientFunds: balanceIsSufficient ? false : 'insufficientFunds',
})
}
const shouldUpdateSimulationSendError = Boolean(txMeta.simulationFails) !== Boolean(simulationFails)
if (shouldUpdateSimulationSendError) {
updateSendErrors({
simulationFails: !txMeta.simulationFails ? false : 'transactionError',
})
}
}
ConfirmSendEther.prototype.componentWillMount = function () {
this.updateComponentSendErrors({})
}
ConfirmSendEther.prototype.componentDidUpdate = function (prevProps) {
this.updateComponentSendErrors(prevProps)
}
ConfirmSendEther.prototype.getAmount = function () {
const { conversionRate, currentCurrency } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
const FIAT = conversionUtil(txParams.value, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromCurrency: 'ETH',
toCurrency: currentCurrency,
numberOfDecimals: 2,
fromDenomination: 'WEI',
conversionRate,
})
const ETH = conversionUtil(txParams.value, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromCurrency: 'ETH',
toCurrency: 'ETH',
fromDenomination: 'WEI',
conversionRate,
numberOfDecimals: 6,
})
return {
FIAT,
ETH,
}
}
ConfirmSendEther.prototype.getGasFee = function () {
const { conversionRate, currentCurrency } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
// Gas
const gas = txParams.gas
const gasBn = hexToBn(gas)
// From latest master
// const gasLimit = new BN(parseInt(blockGasLimit))
// const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20)
// const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20)
// const safeGasLimit = safeGasLimitBN.toString(10)
// Gas Price
const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
const gasPriceBn = hexToBn(gasPrice)
const txFeeBn = gasBn.mul(gasPriceBn)
const FIAT = conversionUtil(txFeeBn, {
fromNumericBase: 'BN',
toNumericBase: 'dec',
fromDenomination: 'WEI',
fromCurrency: 'ETH',
toCurrency: currentCurrency,
numberOfDecimals: 2,
conversionRate,
})
const ETH = conversionUtil(txFeeBn, {
fromNumericBase: 'BN',
toNumericBase: 'dec',
fromDenomination: 'WEI',
fromCurrency: 'ETH',
toCurrency: 'ETH',
numberOfDecimals: 6,
conversionRate,
})
return {
FIAT,
ETH,
gasFeeInHex: txFeeBn.toString(16),
}
}
ConfirmSendEther.prototype.getData = function () {
const { identities } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
const account = identities ? identities[txParams.from] || {} : {}
const { FIAT: gasFeeInFIAT, ETH: gasFeeInETH, gasFeeInHex } = this.getGasFee()
const { FIAT: amountInFIAT, ETH: amountInETH } = this.getAmount()
const totalInFIAT = addCurrencies(gasFeeInFIAT, amountInFIAT, {
toNumericBase: 'dec',
numberOfDecimals: 2,
})
const totalInETH = addCurrencies(gasFeeInETH, amountInETH, {
toNumericBase: 'dec',
numberOfDecimals: 6,
})
return {
from: {
address: txParams.from,
name: account.name,
},
to: {
address: txParams.to,
name: identities[txParams.to] ? identities[txParams.to].name : this.context.t('newRecipient'),
},
memo: txParams.memo || '',
gasFeeInFIAT,
gasFeeInETH,
amountInFIAT,
amountInETH,
totalInFIAT,
totalInETH,
gasFeeInHex,
}
}
ConfirmSendEther.prototype.convertToRenderableCurrency = function (value, currencyCode) {
const upperCaseCurrencyCode = currencyCode.toUpperCase()
return currencies.find(currency => currency.code === upperCaseCurrencyCode)
? currencyFormatter.format(Number(value), {
code: upperCaseCurrencyCode,
})
: value
}
ConfirmSendEther.prototype.editTransaction = function () {
const { editTransaction, history } = this.props
const txMeta = this.gatherTxMeta()
editTransaction(txMeta)
history.push(SEND_ROUTE)
}
ConfirmSendEther.prototype.renderHeaderRow = function (isTxReprice) {
const windowType = window.METAMASK_UI_TYPE
const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
windowType !== ENVIRONMENT_TYPE_POPUP
if (isTxReprice && isFullScreen) {
return null
}
return (
h('.page-container__header-row', [
h('span.page-container__back-button', {
onClick: () => this.editTransaction(),
style: {
visibility: isTxReprice ? 'hidden' : 'initial',
},
}, 'Edit'),
!isFullScreen && h(NetworkDisplay),
])
)
}
ConfirmSendEther.prototype.renderHeader = function (isTxReprice) {
const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm')
const subtitle = isTxReprice
? this.context.t('speedUpSubtitle')
: this.context.t('pleaseReviewTransaction')
return (
h('.page-container__header', [
this.renderHeaderRow(isTxReprice),
h('.page-container__title', title),
h('.page-container__subtitle', subtitle),
])
)
}
ConfirmSendEther.prototype.render = function () {
const {
currentCurrency,
clearSend,
conversionRate,
currentCurrency: convertedCurrency,
showCustomizeGasModal,
send: {
gasTotal,
gasLimit: sendGasLimit,
gasPrice: sendGasPrice,
errors,
},
} = this.props
const txMeta = this.gatherTxMeta()
const isTxReprice = Boolean(txMeta.lastGasPrice)
const txParams = txMeta.txParams || {}
const {
from: {
address: fromAddress,
name: fromName,
},
to: {
address: toAddress,
name: toName,
},
memo,
gasFeeInHex,
amountInFIAT,
totalInFIAT,
totalInETH,
} = this.getData()
const convertedAmountInFiat = this.convertToRenderableCurrency(amountInFIAT, currentCurrency)
const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency)
// This is from the latest master
// It handles some of the errors that we are not currently handling
// Leaving as comments fo reference
// const balanceBn = hexToBn(balance)
// const insufficientBalance = balanceBn.lt(maxCost)
// const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting
// const showRejectAll = props.unconfTxListLength > 1
// const dangerousGasLimit = gasBn.gte(saferGasLimitBN)
// const gasLimitSpecified = txMeta.gasLimitSpecified
this.inputs = []
return (
// Main Send token Card
h('.page-container', [
this.renderHeader(isTxReprice),
h('.page-container__content', [
h(SenderToRecipient, {
senderName: fromName,
senderAddress: fromAddress,
recipientName: toName,
recipientAddress: txParams.to,
}),
// h('h3.flex-center.confirm-screen-sending-to-message', {
// style: {
// textAlign: 'center',
// fontSize: '16px',
// },
// }, [
// `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`,
// ]),
h('h3.flex-center.confirm-screen-send-amount', [`${convertedAmountInFiat}`]),
h('h3.flex-center.confirm-screen-send-amount-currency', [ currentCurrency.toUpperCase() ]),
h('div.flex-center.confirm-memo-wrapper', [
h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
]),
h('div.confirm-screen-rows', [
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', fromName),
h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
]),
]),
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', toName),
h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`),
]),
]),
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]),
h('div.confirm-screen-section-column', [
h(GasFeeDisplay, {
gasTotal: gasTotal || gasFeeInHex,
conversionRate,
convertedCurrency,
onClick: () => showCustomizeGasModal(txMeta, sendGasLimit, sendGasPrice, gasTotal),
}),
]),
]),
h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
h('div', {
className: classnames({
'confirm-screen-section-column--with-error': errors['insufficientFunds'],
'confirm-screen-section-column': !errors['insufficientFunds'],
}),
}, [
h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', `${convertedTotalInFiat} ${currentCurrency.toUpperCase()}`),
h('div.confirm-screen-row-detail', `${totalInETH} ETH`),
]),
this.renderErrorMessage('insufficientFunds'),
]),
]),
// These are latest errors handling from master
// Leaving as comments as reference when we start implementing error handling
// h('style', `
// .conf-buttons button {
// margin-left: 10px;
// text-transform: uppercase;
// }
// `),
// txMeta.simulationFails ?
// h('.error', {
// style: {
// marginLeft: 50,
// fontSize: '0.9em',
// },
// }, 'Transaction Error. Exception thrown in contract code.')
// : null,
// !isValidAddress ?
// h('.error', {
// style: {
// marginLeft: 50,
// fontSize: '0.9em',
// },
// }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.')
// : null,
// insufficientBalance ?
// h('span.error', {
// style: {
// marginLeft: 50,
// fontSize: '0.9em',
// },
// }, 'Insufficient balance for transaction')
// : null,
// // send + cancel
// h('.flex-row.flex-space-around.conf-buttons', {
// style: {
// display: 'flex',
// justifyContent: 'flex-end',
// margin: '14px 25px',
// },
// }, [
// h('button', {
// onClick: (event) => {
// this.resetGasFields()
// event.preventDefault()
// },
// }, 'Reset'),
// // Accept Button or Buy Button
// insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') :
// h('input.confirm.btn-green', {
// type: 'submit',
// value: 'SUBMIT',
// style: { marginLeft: '10px' },
// disabled: buyDisabled,
// }),
// h('button.cancel.btn-red', {
// onClick: props.cancelTransaction,
// }, 'Reject'),
// ]),
// showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', {
// style: {
// display: 'flex',
// justifyContent: 'flex-end',
// margin: '14px 25px',
// },
// }, [
// h('button.cancel.btn-red', {
// onClick: props.cancelAllTransactions,
// }, 'Reject All'),
// ]) : null,
// ]),
// ])
// )
// }
]),
h('form#pending-tx-form', {
className: 'confirm-screen-form',
onSubmit: this.onSubmit,
}, [
this.renderErrorMessage('simulationFails'),
h('.page-container__footer', [
// Cancel Button
h('button.btn-cancel.page-container__footer-button.allcaps', {
onClick: (event) => {
clearSend()
this.cancel(event, txMeta)
},
}, this.context.t('cancel')),
// Accept Button
h('button.btn-confirm.page-container__footer-button.allcaps', {
onClick: event => this.onSubmit(event),
}, this.context.t('confirm')),
]),
]),
])
)
}
ConfirmSendEther.prototype.renderErrorMessage = function (message) {
const { send: { errors } } = this.props
return errors[message]
? h('div.confirm-screen-error', [ errors[message] ])
: null
}
ConfirmSendEther.prototype.onSubmit = function (event) {
event.preventDefault()
const { updateSendErrors } = this.props
const txMeta = this.gatherTxMeta()
const valid = this.checkValidity()
const balanceIsSufficient = this.isBalanceSufficient(txMeta)
this.setState({ valid, submitting: true })
if (valid && this.verifyGasParams() && balanceIsSufficient) {
this.props.sendTransaction(txMeta, event)
} else if (!balanceIsSufficient) {
updateSendErrors({ insufficientFunds: 'insufficientFunds' })
} else {
updateSendErrors({ invalidGasParams: 'invalidGasParams' })
this.setState({ submitting: false })
}
}
ConfirmSendEther.prototype.cancel = function (event, txMeta) {
event.preventDefault()
const { cancelTransaction } = this.props
cancelTransaction(txMeta)
.then(() => this.props.history.push(DEFAULT_ROUTE))
}
ConfirmSendEther.prototype.isBalanceSufficient = function (txMeta) {
const {
balance,
conversionRate,
} = this.props
const {
txParams: {
gas,
gasPrice,
value: amount,
},
} = txMeta
const gasTotal = calcGasTotal(gas, gasPrice)
return isBalanceSufficient({
amount,
gasTotal,
balance,
conversionRate,
})
}
ConfirmSendEther.prototype.checkValidity = function () {
const form = this.getFormEl()
const valid = form.checkValidity()
return valid
}
ConfirmSendEther.prototype.getFormEl = function () {
const form = document.querySelector('form#pending-tx-form')
// Stub out form for unit tests:
if (!form) {
return { checkValidity () { return true } }
}
return form
}
// After a customizable state value has been updated,
ConfirmSendEther.prototype.gatherTxMeta = function () {
const props = this.props
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
const {
lastGasPrice,
txParams: {
gasPrice: txGasPrice,
gas: txGasLimit,
},
} = txData
let forceGasMin
if (lastGasPrice) {
forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
multiplierBase: 10,
toNumericBase: 'hex',
}))
}
txData.txParams.gasPrice = sendGasPrice || forceGasMin || txGasPrice
txData.txParams.gas = sendGasLimit || txGasLimit
// log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
return txData
}
ConfirmSendEther.prototype.verifyGasParams = function () {
// We call this in case the gas has not been modified at all
if (!this.state) { return true }
return (
this._notZeroOrEmptyString(this.state.gas) &&
this._notZeroOrEmptyString(this.state.gasPrice)
)
}
ConfirmSendEther.prototype._notZeroOrEmptyString = function (obj) {
return obj !== '' && obj !== '0x0'
}
ConfirmSendEther.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
const numBN = new BN(numerator)
const denomBN = new BN(denominator)
return targetBN.mul(numBN).div(denomBN)
}

@ -1,696 +0,0 @@
const Component = require('react').Component
const { withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const PropTypes = require('prop-types')
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const inherits = require('util').inherits
const tokenAbi = require('human-standard-token-abi')
const abiDecoder = require('abi-decoder')
abiDecoder.addABI(tokenAbi)
const actions = require('../../actions')
const clone = require('clone')
const Identicon = require('../identicon')
const GasFeeDisplay = require('../send/send-content/send-gas-row/gas-fee-display/').default
const NetworkDisplay = require('../network-display')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const {
conversionUtil,
multiplyCurrencies,
addCurrencies,
} = require('../../conversion-util')
const {
calcGasTotal,
isBalanceSufficient,
} = require('../send/send.utils')
const {
calcTokenAmount,
} = require('../../token-util')
const classnames = require('classnames')
const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies')
const { MIN_GAS_PRICE_HEX } = require('../send/send.constants')
const {
getTokenExchangeRate,
getSelectedAddress,
getSelectedTokenContract,
} = require('../../selectors')
const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
import {
updateSendErrors,
} from '../../ducks/send.duck'
const {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
} = require('../../../../app/scripts/lib/enums')
ConfirmSendToken.contextTypes = {
t: PropTypes.func,
}
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(ConfirmSendToken)
function mapStateToProps (state, ownProps) {
const { token: { address }, txData } = ownProps
const { txParams } = txData || {}
const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const {
conversionRate,
identities,
currentCurrency,
} = state.metamask
const accounts = state.metamask.accounts
const selectedAddress = getSelectedAddress(state)
const tokenExchangeRate = getTokenExchangeRate(state, address)
const { balance } = accounts[selectedAddress]
return {
conversionRate,
identities,
selectedAddress,
tokenExchangeRate,
tokenData: tokenData || {},
currentCurrency: currentCurrency.toUpperCase(),
send: state.metamask.send,
tokenContract: getSelectedTokenContract(state),
balance,
}
}
function mapDispatchToProps (dispatch, ownProps) {
return {
backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
editTransaction: txMeta => {
const { token: { address } } = ownProps
const { txParams = {}, id } = txMeta
const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data) || {}
const { params = [] } = tokenData
const { value: to } = params[0] || {}
const { value: tokenAmountInDec } = params[1] || {}
const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
})
const {
gas: gasLimit,
gasPrice,
} = txParams
dispatch(actions.setSelectedToken(address))
dispatch(actions.updateSend({
gasLimit,
gasPrice,
gasTotal: null,
to,
amount: tokenAmountInHex,
errors: { to: null, amount: null },
editingTransactionId: id && id.toString(),
token: ownProps.token,
}))
dispatch(actions.showSendTokenPage())
},
showCustomizeGasModal: (txMeta, sendGasLimit, sendGasPrice, sendGasTotal) => {
const { id, txParams, lastGasPrice } = txMeta
const { gas: txGasLimit, gasPrice: txGasPrice } = txParams
const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const { params = [] } = tokenData
const { value: to } = params[0] || {}
const { value: tokenAmountInDec } = params[1] || {}
const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
})
let forceGasMin
if (lastGasPrice) {
forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
multiplierBase: 10,
toNumericBase: 'hex',
fromDenomination: 'WEI',
}))
}
dispatch(actions.updateSend({
gasLimit: sendGasLimit || txGasLimit,
gasPrice: sendGasPrice || txGasPrice,
editingTransactionId: id,
gasTotal: sendGasTotal,
to,
amount: tokenAmountInHex,
forceGasMin,
}))
dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
},
updateSendErrors: error => dispatch(updateSendErrors(error)),
}
}
inherits(ConfirmSendToken, Component)
function ConfirmSendToken () {
Component.call(this)
this.state = {}
this.onSubmit = this.onSubmit.bind(this)
}
ConfirmSendToken.prototype.editTransaction = function (txMeta) {
const { editTransaction, history } = this.props
editTransaction(txMeta)
history.push(SEND_ROUTE)
}
ConfirmSendToken.prototype.updateComponentSendErrors = function (prevProps) {
const {
balance: oldBalance,
conversionRate: oldConversionRate,
} = prevProps
const {
updateSendErrors,
balance,
conversionRate,
send: {
errors: {
simulationFails,
},
},
} = this.props
const txMeta = this.gatherTxMeta()
const shouldUpdateBalanceSendErrors = balance && [
balance !== oldBalance,
conversionRate !== oldConversionRate,
].some(x => Boolean(x))
if (shouldUpdateBalanceSendErrors) {
const balanceIsSufficient = this.isBalanceSufficient(txMeta)
updateSendErrors({
insufficientFunds: balanceIsSufficient ? false : this.context.t('insufficientFunds'),
})
}
const shouldUpdateSimulationSendError = Boolean(txMeta.simulationFails) !== Boolean(simulationFails)
if (shouldUpdateSimulationSendError) {
updateSendErrors({
simulationFails: !txMeta.simulationFails ? false : this.context.t('transactionError'),
})
}
}
ConfirmSendToken.prototype.componentWillMount = function () {
const { tokenContract, selectedAddress } = this.props
tokenContract && tokenContract
.balanceOf(selectedAddress)
.then(usersToken => {
})
this.updateComponentSendErrors({})
}
ConfirmSendToken.prototype.componentDidUpdate = function (prevProps) {
this.updateComponentSendErrors(prevProps)
}
ConfirmSendToken.prototype.getAmount = function () {
const {
conversionRate,
tokenExchangeRate,
token,
tokenData,
send: { amount, editingTransactionId },
} = this.props
const { params = [] } = tokenData
let { value } = params[1] || {}
const { decimals } = token
if (editingTransactionId) {
value = conversionUtil(amount, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
})
}
const sendTokenAmount = calcTokenAmount(value, decimals)
return {
fiat: tokenExchangeRate
? +(sendTokenAmount * tokenExchangeRate * conversionRate).toFixed(2)
: null,
token: typeof value === 'undefined'
? this.context.t('unknown')
: +sendTokenAmount.toFixed(decimals),
}
}
ConfirmSendToken.prototype.getGasFee = function () {
const { conversionRate, tokenExchangeRate, token, currentCurrency } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
const { decimals } = token
const gas = txParams.gas
const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_HEX
const gasTotal = multiplyCurrencies(gas, gasPrice, {
multiplicandBase: 16,
multiplierBase: 16,
})
const FIAT = conversionUtil(gasTotal, {
fromNumericBase: 'BN',
toNumericBase: 'dec',
fromDenomination: 'WEI',
fromCurrency: 'ETH',
toCurrency: currentCurrency,
numberOfDecimals: 2,
conversionRate,
})
const ETH = conversionUtil(gasTotal, {
fromNumericBase: 'BN',
toNumericBase: 'dec',
fromDenomination: 'WEI',
fromCurrency: 'ETH',
toCurrency: 'ETH',
numberOfDecimals: 6,
conversionRate,
})
const tokenGas = multiplyCurrencies(gas, gasPrice, {
toNumericBase: 'dec',
multiplicandBase: 16,
multiplierBase: 16,
toCurrency: 'BAT',
conversionRate: tokenExchangeRate,
invertConversionRate: true,
fromDenomination: 'WEI',
numberOfDecimals: decimals || 4,
})
return {
fiat: +Number(FIAT).toFixed(2),
eth: ETH,
token: tokenExchangeRate
? tokenGas
: null,
gasFeeInHex: gasTotal.toString(16),
}
}
ConfirmSendToken.prototype.getData = function () {
const { identities, tokenData } = this.props
const { params = [] } = tokenData
const { value } = params[0] || {}
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
return {
from: {
address: txParams.from,
name: identities[txParams.from].name,
},
to: {
address: value,
name: identities[value] ? identities[value].name : this.context.t('newRecipient'),
},
memo: txParams.memo || '',
}
}
ConfirmSendToken.prototype.renderHeroAmount = function () {
const { token: { symbol }, currentCurrency } = this.props
const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
const { memo = '' } = txParams
const convertedAmountInFiat = this.convertToRenderableCurrency(fiatAmount, currentCurrency)
return fiatAmount
? (
h('div.confirm-send-token__hero-amount-wrapper', [
h('h3.flex-center.confirm-screen-send-amount', `${convertedAmountInFiat}`),
h('h3.flex-center.confirm-screen-send-amount-currency', currentCurrency),
h('div.flex-center.confirm-memo-wrapper', [
h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
]),
])
)
: (
h('div.confirm-send-token__hero-amount-wrapper', [
h('h3.flex-center.confirm-screen-send-amount', tokenAmount),
h('h3.flex-center.confirm-screen-send-amount-currency', symbol),
h('div.flex-center.confirm-memo-wrapper', [
h('h3.confirm-screen-send-memo', [ memo ? `"${memo}"` : '' ]),
]),
])
)
}
ConfirmSendToken.prototype.renderGasFee = function () {
const {
currentCurrency: convertedCurrency,
conversionRate,
send: { gasTotal, gasLimit: sendGasLimit, gasPrice: sendGasPrice },
showCustomizeGasModal,
} = this.props
const txMeta = this.gatherTxMeta()
const { gasFeeInHex } = this.getGasFee()
return (
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('gasFee') ]),
h('div.confirm-screen-section-column', [
h(GasFeeDisplay, {
gasTotal: gasTotal || gasFeeInHex,
conversionRate,
convertedCurrency,
onClick: () => showCustomizeGasModal(txMeta, sendGasLimit, sendGasPrice, gasTotal),
}),
]),
])
)
}
ConfirmSendToken.prototype.renderTotalPlusGas = function () {
const { token: { symbol }, currentCurrency, send: { errors } } = this.props
const { fiat: fiatAmount, token: tokenAmount } = this.getAmount()
const { fiat: fiatGas, token: tokenGas } = this.getGasFee()
const totalInFIAT = fiatAmount && fiatGas && addCurrencies(fiatAmount, fiatGas)
const convertedTotalInFiat = this.convertToRenderableCurrency(totalInFIAT, currentCurrency)
return fiatAmount && fiatGas
? (
h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
h('div.confirm-screen-section-column', [
h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', `${convertedTotalInFiat} ${currentCurrency}`),
h('div.confirm-screen-row-detail', `${addCurrencies(tokenAmount, tokenGas || '0')} ${symbol}`),
]),
])
)
: (
h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
h('div', {
className: classnames({
'confirm-screen-section-column--with-error': errors['insufficientFunds'],
'confirm-screen-section-column': !errors['insufficientFunds'],
}),
}, [
h('span.confirm-screen-label', [ this.context.t('total') + ' ' ]),
h('div.confirm-screen-total-box__subtitle', [ this.context.t('amountPlusGas') ]),
]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', `${tokenAmount} ${symbol}`),
h('div.confirm-screen-row-detail', `+ ${fiatGas} ${currentCurrency} ${this.context.t('gas')}`),
]),
this.renderErrorMessage('insufficientFunds'),
])
)
}
ConfirmSendToken.prototype.renderErrorMessage = function (message) {
const { send: { errors } } = this.props
return errors[message]
? h('div.confirm-screen-error', [ errors[message] ])
: null
}
ConfirmSendToken.prototype.convertToRenderableCurrency = function (value, currencyCode) {
const upperCaseCurrencyCode = currencyCode.toUpperCase()
return currencies.find(currency => currency.code === upperCaseCurrencyCode)
? currencyFormatter.format(Number(value), {
code: upperCaseCurrencyCode,
})
: value
}
ConfirmSendToken.prototype.renderHeaderRow = function (isTxReprice) {
const windowType = window.METAMASK_UI_TYPE
const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
windowType !== ENVIRONMENT_TYPE_POPUP
if (isTxReprice && isFullScreen) {
return null
}
return (
h('.page-container__header-row', [
h('span.page-container__back-button', {
onClick: () => this.editTransaction(),
style: {
visibility: isTxReprice ? 'hidden' : 'initial',
},
}, 'Edit'),
!isFullScreen && h(NetworkDisplay),
])
)
}
ConfirmSendToken.prototype.renderHeader = function (isTxReprice) {
const title = isTxReprice ? this.context.t('speedUpTitle') : this.context.t('confirm')
const subtitle = isTxReprice
? this.context.t('speedUpSubtitle')
: this.context.t('pleaseReviewTransaction')
return (
h('.page-container__header', [
this.renderHeaderRow(isTxReprice),
h('.page-container__title', title),
h('.page-container__subtitle', subtitle),
])
)
}
ConfirmSendToken.prototype.render = function () {
const txMeta = this.gatherTxMeta()
const {
from: {
address: fromAddress,
name: fromName,
},
to: {
address: toAddress,
name: toName,
},
} = this.getData()
const isTxReprice = Boolean(txMeta.lastGasPrice)
return (
h('div.confirm-screen-container.confirm-send-token', [
// Main Send token Card
h('div.page-container', [
this.renderHeader(isTxReprice),
h('.page-container__content', [
h('div.flex-row.flex-center.confirm-screen-identicons', [
h('div.confirm-screen-account-wrapper', [
h(
Identicon,
{
address: fromAddress,
diameter: 60,
},
),
h('span.confirm-screen-account-name', fromName),
// h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)),
]),
h('i.fa.fa-arrow-right.fa-lg'),
h('div.confirm-screen-account-wrapper', [
h(
Identicon,
{
address: toAddress,
diameter: 60,
},
),
h('span.confirm-screen-account-name', toName),
// h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)),
]),
]),
// h('h3.flex-center.confirm-screen-sending-to-message', {
// style: {
// textAlign: 'center',
// fontSize: '16px',
// },
// }, [
// `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`,
// ]),
this.renderHeroAmount(),
h('div.confirm-screen-rows', [
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('from') ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', fromName),
h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
]),
]),
toAddress && h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ this.context.t('to') ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', toName),
h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`),
]),
]),
this.renderGasFee(),
this.renderTotalPlusGas(),
]),
]),
h('form#pending-tx-form', {
className: 'confirm-screen-form',
onSubmit: this.onSubmit,
}, [
this.renderErrorMessage('simulationFails'),
h('.page-container__footer', [
// Cancel Button
h('button.btn-cancel.page-container__footer-button.allcaps', {
onClick: (event) => this.cancel(event, txMeta),
}, this.context.t('cancel')),
// Accept Button
h('button.btn-confirm.page-container__footer-button.allcaps', {
onClick: event => this.onSubmit(event),
}, [this.context.t('confirm')]),
]),
]),
]),
])
)
}
ConfirmSendToken.prototype.onSubmit = function (event) {
event.preventDefault()
const { updateSendErrors } = this.props
const txMeta = this.gatherTxMeta()
const valid = this.checkValidity()
const balanceIsSufficient = this.isBalanceSufficient(txMeta)
this.setState({ valid, submitting: true })
if (valid && this.verifyGasParams() && balanceIsSufficient) {
this.props.sendTransaction(txMeta, event)
} else if (!balanceIsSufficient) {
updateSendErrors({ insufficientFunds: 'insufficientFunds' })
} else {
updateSendErrors({ invalidGasParams: 'invalidGasParams' })
this.setState({ submitting: false })
}
}
ConfirmSendToken.prototype.isBalanceSufficient = function (txMeta) {
const {
balance,
conversionRate,
} = this.props
const {
txParams: {
gas,
gasPrice,
},
} = txMeta
const gasTotal = calcGasTotal(gas, gasPrice)
return isBalanceSufficient({
amount: '0',
gasTotal,
balance,
conversionRate,
})
}
ConfirmSendToken.prototype.cancel = function (event, txMeta) {
event.preventDefault()
const { cancelTransaction } = this.props
cancelTransaction(txMeta)
.then(() => this.props.history.push(DEFAULT_ROUTE))
}
ConfirmSendToken.prototype.checkValidity = function () {
const form = this.getFormEl()
const valid = form.checkValidity()
return valid
}
ConfirmSendToken.prototype.getFormEl = function () {
const form = document.querySelector('form#pending-tx-form')
// Stub out form for unit tests:
if (!form) {
return { checkValidity () { return true } }
}
return form
}
// After a customizable state value has been updated,
ConfirmSendToken.prototype.gatherTxMeta = function () {
const props = this.props
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
const {
lastGasPrice,
txParams: {
gasPrice: txGasPrice,
gas: txGasLimit,
},
} = txData
let forceGasMin
if (lastGasPrice) {
forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
multiplierBase: 10,
toNumericBase: 'hex',
}))
}
txData.txParams.gasPrice = sendGasPrice || forceGasMin || txGasPrice
txData.txParams.gas = sendGasLimit || txGasLimit
// log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
return txData
}
ConfirmSendToken.prototype.verifyGasParams = function () {
// We call this in case the gas has not been modified at all
if (!this.state) { return true }
return (
this._notZeroOrEmptyString(this.state.gas) &&
this._notZeroOrEmptyString(this.state.gasPrice)
)
}
ConfirmSendToken.prototype._notZeroOrEmptyString = function (obj) {
return obj !== '' && obj !== '0x0'
}
ConfirmSendToken.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
const numBN = new BN(numerator)
const denomBN = new BN(denominator)
return targetBN.mul(numBN).div(denomBN)
}

@ -1,165 +0,0 @@
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const PropTypes = require('prop-types')
const clone = require('clone')
const abi = require('human-standard-token-abi')
const abiDecoder = require('abi-decoder')
abiDecoder.addABI(abi)
const inherits = require('util').inherits
const actions = require('../../actions')
const { getSymbolAndDecimals } = require('../../token-util')
const ConfirmSendEther = require('./confirm-send-ether')
const ConfirmSendToken = require('./confirm-send-token')
const ConfirmDeployContract = require('./confirm-deploy-contract')
const Loading = require('../loading-screen')
const TX_TYPES = {
DEPLOY_CONTRACT: 'deploy_contract',
SEND_ETHER: 'send_ether',
SEND_TOKEN: 'send_token',
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(PendingTx)
function mapStateToProps (state) {
const {
conversionRate,
identities,
tokens: existingTokens,
} = state.metamask
const accounts = state.metamask.accounts
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
return {
conversionRate,
identities,
selectedAddress,
existingTokens,
}
}
function mapDispatchToProps (dispatch) {
return {
backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
}
}
inherits(PendingTx, Component)
function PendingTx () {
Component.call(this)
this.state = {
isFetching: true,
transactionType: '',
tokenAddress: '',
tokenSymbol: '',
tokenDecimals: '',
}
}
PendingTx.prototype.componentDidMount = function () {
this.setTokenData()
}
PendingTx.prototype.componentDidUpdate = function (prevProps, prevState) {
if (prevState.isFetching) {
this.setTokenData()
}
}
PendingTx.prototype.setTokenData = async function () {
const { existingTokens } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
if (txMeta.loadingDefaults) {
return
}
if (!txParams.to) {
return this.setState({
transactionType: TX_TYPES.DEPLOY_CONTRACT,
isFetching: false,
})
}
// inspect tx data for supported special confirmation screens
let isTokenTransaction = false
if (txParams.data) {
const tokenData = abiDecoder.decodeMethod(txParams.data)
const { name: tokenMethodName } = tokenData || {}
isTokenTransaction = (tokenMethodName === 'transfer')
}
if (isTokenTransaction) {
const { symbol, decimals } = await getSymbolAndDecimals(txParams.to, existingTokens)
this.setState({
transactionType: TX_TYPES.SEND_TOKEN,
tokenAddress: txParams.to,
tokenSymbol: symbol,
tokenDecimals: decimals,
isFetching: false,
})
} else {
this.setState({
transactionType: TX_TYPES.SEND_ETHER,
isFetching: false,
})
}
}
PendingTx.prototype.gatherTxMeta = function () {
const props = this.props
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
return txData
}
PendingTx.prototype.render = function () {
const {
isFetching,
transactionType,
tokenAddress,
tokenSymbol,
tokenDecimals,
} = this.state
const { sendTransaction } = this.props
if (isFetching) {
return h(Loading, {
loadingMessage: this.context.t('generatingTransaction'),
})
}
switch (transactionType) {
case TX_TYPES.SEND_ETHER:
return h(ConfirmSendEther, {
txData: this.gatherTxMeta(),
sendTransaction,
})
case TX_TYPES.SEND_TOKEN:
return h(ConfirmSendToken, {
txData: this.gatherTxMeta(),
sendTransaction,
token: {
address: tokenAddress,
symbol: tokenSymbol,
decimals: tokenDecimals,
},
})
case TX_TYPES.DEPLOY_CONTRACT:
return h(ConfirmDeployContract, {
txData: this.gatherTxMeta(),
sendTransaction,
})
default:
return h(Loading)
}
}
PendingTx.contextTypes = {
t: PropTypes.func,
}

@ -9,11 +9,7 @@ const txHelper = require('../lib/tx-helper')
const log = require('loglevel') const log = require('loglevel')
const R = require('ramda') const R = require('ramda')
const PendingTx = require('./components/pending-tx')
const SignatureRequest = require('./components/signature-request') const SignatureRequest = require('./components/signature-request')
// const PendingMsg = require('./components/pending-msg')
// const PendingPersonalMsg = require('./components/pending-personal-msg')
// const PendingTypedMsg = require('./components/pending-typed-msg')
const Loading = require('./components/loading-screen') const Loading = require('./components/loading-screen')
const { DEFAULT_ROUTE } = require('./routes') const { DEFAULT_ROUTE } = require('./routes')
@ -151,101 +147,32 @@ ConfirmTxScreen.prototype.render = function () {
currentCurrency, currentCurrency,
conversionRate, conversionRate,
blockGasLimit, blockGasLimit,
// provider,
// computedBalances,
} = props } = props
var txData = this.getTxData() || {} var txData = this.getTxData() || {}
var txParams = txData.params || {} const { msgParams } = txData
log.debug('msgParams detected, rendering pending msg')
// var isNotification = isPopupOrNotification() === 'notification'
/* return msgParams
Client is using the flag above to render the following in conf screen ? h(SignatureRequest, {
// subtitle and nav // Properties
h('.section-title.flex-row.flex-center', [ txData: txData,
!isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { key: txData.id,
onClick: this.goHome.bind(this), selectedAddress: props.selectedAddress,
}) : null, accounts: props.accounts,
h('h2.page-subtitle', 'Confirm Transaction'), identities: props.identities,
isNotification ? h(NetworkIndicator, { conversionRate,
network: network, currentCurrency,
provider: provider, blockGasLimit,
}) : null, // Actions
]), signMessage: this.signMessage.bind(this, txData),
*/ signPersonalMessage: this.signPersonalMessage.bind(this, txData),
signTypedMessage: this.signTypedMessage.bind(this, txData),
return currentTxView({ cancelMessage: this.cancelMessage.bind(this, txData),
// Properties cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
txData: txData, cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
key: txData.id, })
selectedAddress: props.selectedAddress, : h(Loading)
accounts: props.accounts,
identities: props.identities,
conversionRate,
currentCurrency,
blockGasLimit,
// Actions
buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress),
sendTransaction: this.sendTransaction.bind(this),
cancelTransaction: this.cancelTransaction.bind(this, txData),
signMessage: this.signMessage.bind(this, txData),
signPersonalMessage: this.signPersonalMessage.bind(this, txData),
signTypedMessage: this.signTypedMessage.bind(this, txData),
cancelMessage: this.cancelMessage.bind(this, txData),
cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
})
}
function currentTxView (opts) {
log.info('rendering current tx view')
const { txData } = opts
const { txParams, msgParams } = txData
if (txParams) {
log.debug('txParams detected, rendering pending tx')
return h(PendingTx, opts)
} else if (msgParams) {
log.debug('msgParams detected, rendering pending msg')
return h(SignatureRequest, opts)
// if (type === 'eth_sign') {
// log.debug('rendering eth_sign message')
// return h(PendingMsg, opts)
// } else if (type === 'personal_sign') {
// log.debug('rendering personal_sign message')
// return h(PendingPersonalMsg, opts)
// } else if (type === 'eth_signTypedData') {
// log.debug('rendering eth_signTypedData message')
// return h(PendingTypedMsg, opts)
// }
}
return h(Loading)
}
ConfirmTxScreen.prototype.buyEth = function (address, event) {
event.preventDefault()
this.props.dispatch(actions.buyEthView(address))
}
ConfirmTxScreen.prototype.sendTransaction = function (txData, event) {
this.stopPropagation(event)
this.props.dispatch(actions.updateAndApproveTx(txData))
.then(() => this.props.history.push(DEFAULT_ROUTE))
}
ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) {
this.stopPropagation(event)
event.preventDefault()
this.props.dispatch(actions.cancelTx(txData))
}
ConfirmTxScreen.prototype.cancelAllTransactions = function (unconfTxList, event) {
this.stopPropagation(event)
event.preventDefault()
this.props.dispatch(actions.cancelAllTx(unconfTxList))
} }
ConfirmTxScreen.prototype.signMessage = function (msgData, event) { ConfirmTxScreen.prototype.signMessage = function (msgData, event) {
@ -295,20 +222,3 @@ ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) {
this.stopPropagation(event) this.stopPropagation(event)
return this.props.dispatch(actions.cancelTypedMsg(msgData)) return this.props.dispatch(actions.cancelTypedMsg(msgData))
} }
ConfirmTxScreen.prototype.goHome = function (event) {
this.stopPropagation(event)
this.props.dispatch(actions.goHome())
}
// function warningIfExists (warning) {
// if (warning &&
// // Do not display user rejections on this screen:
// warning.indexOf('User denied transaction signature') === -1) {
// return h('.error', {
// style: {
// margin: 'auto',
// },
// }, warning)
// }
// }

@ -114,3 +114,20 @@ export function formatCurrency (value, currencyCode) {
? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode }) ? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode })
: value : value
} }
export function convertTokenToFiat ({
value,
toCurrency,
conversionRate,
contractExchangeRate,
}) {
const totalExchangeRate = conversionRate * contractExchangeRate
return conversionUtil(value, {
fromNumericBase: 'dec',
toNumericBase: 'dec',
toCurrency,
numberOfDecimals: 2,
conversionRate: totalExchangeRate,
})
}

@ -26,6 +26,7 @@ const CONFIRM_SEND_ETHER_PATH = '/send-ether'
const CONFIRM_SEND_TOKEN_PATH = '/send-token' const CONFIRM_SEND_TOKEN_PATH = '/send-token'
const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract' const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract'
const CONFIRM_APPROVE_PATH = '/approve' const CONFIRM_APPROVE_PATH = '/approve'
const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from'
const CONFIRM_TOKEN_METHOD_PATH = '/token-method' const CONFIRM_TOKEN_METHOD_PATH = '/token-method'
const SIGNATURE_REQUEST_PATH = '/signature-request' const SIGNATURE_REQUEST_PATH = '/signature-request'
@ -57,6 +58,7 @@ module.exports = {
CONFIRM_SEND_TOKEN_PATH, CONFIRM_SEND_TOKEN_PATH,
CONFIRM_DEPLOY_CONTRACT_PATH, CONFIRM_DEPLOY_CONTRACT_PATH,
CONFIRM_APPROVE_PATH, CONFIRM_APPROVE_PATH,
CONFIRM_TRANSFER_FROM_PATH,
CONFIRM_TOKEN_METHOD_PATH, CONFIRM_TOKEN_METHOD_PATH,
SIGNATURE_REQUEST_PATH, SIGNATURE_REQUEST_PATH,
} }

@ -1,5 +1,6 @@
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import txHelper from '../../lib/tx-helper' import txHelper from '../../lib/tx-helper'
import { calcTokenAmount } from '../token-util'
const unapprovedTxsSelector = state => state.metamask.unapprovedTxs const unapprovedTxsSelector = state => state.metamask.unapprovedTxs
const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs
@ -63,3 +64,101 @@ export const unconfirmedTransactionsHashSelector = createSelector(
export const currentCurrencySelector = state => state.metamask.currentCurrency export const currentCurrencySelector = state => state.metamask.currentCurrency
export const conversionRateSelector = state => state.metamask.conversionRate export const conversionRateSelector = state => state.metamask.conversionRate
const txDataSelector = state => state.confirmTransaction.txData
const tokenDataSelector = state => state.confirmTransaction.tokenData
const tokenPropsSelector = state => state.confirmTransaction.tokenProps
const contractExchangeRatesSelector = state => state.metamask.contractExchangeRates
const tokenDecimalsSelector = createSelector(
tokenPropsSelector,
tokenProps => tokenProps && tokenProps.tokenDecimals
)
const tokenDataParamsSelector = createSelector(
tokenDataSelector,
tokenData => tokenData && tokenData.params || []
)
const txParamsSelector = createSelector(
txDataSelector,
txData => txData && txData.txParams || {}
)
export const tokenAddressSelector = createSelector(
txParamsSelector,
txParams => txParams && txParams.to
)
const TOKEN_PARAM_SPENDER = '_spender'
const TOKEN_PARAM_TO = '_to'
const TOKEN_PARAM_VALUE = '_value'
export const tokenAmountAndToAddressSelector = createSelector(
tokenDataParamsSelector,
params => {
let toAddress = ''
let tokenAmount = 0
if (params && params.length) {
const toParam = params.find(param => param.name === TOKEN_PARAM_TO)
const valueParam = params.find(param => param.name === TOKEN_PARAM_VALUE)
toAddress = toParam ? toParam.value : params[0].value
tokenAmount = valueParam ? Number(valueParam.value) : Number(params[1].value)
}
return {
toAddress,
tokenAmount,
}
}
)
export const approveTokenAmountAndToAddressSelector = createSelector(
tokenDataParamsSelector,
params => {
let toAddress = ''
let tokenAmount = 0
if (params && params.length) {
toAddress = params.find(param => param.name === TOKEN_PARAM_SPENDER).value
tokenAmount = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
}
return {
toAddress,
tokenAmount,
}
}
)
export const sendTokenTokenAmountAndToAddressSelector = createSelector(
tokenDataParamsSelector,
tokenDecimalsSelector,
(params, tokenDecimals) => {
let toAddress = ''
let tokenAmount = 0
if (params && params.length) {
toAddress = params.find(param => param.name === TOKEN_PARAM_TO).value
tokenAmount = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
if (tokenDecimals) {
tokenAmount = calcTokenAmount(tokenAmount, tokenDecimals)
}
}
return {
toAddress,
tokenAmount,
}
}
)
export const contractExchangeRateSelector = createSelector(
contractExchangeRatesSelector,
tokenAddressSelector,
(contractExchangeRates, tokenAddress) => contractExchangeRates[tokenAddress]
)

Loading…
Cancel
Save