diff options
author | Alexander Tseung <alextsg@users.noreply.github.com> | 2018-07-18 09:17:54 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-07-18 09:17:54 +0800 |
commit | f6ca06f775a739c8b449405c5e96b03151a6a33c (patch) | |
tree | c81242106d9e6c718e54ab338e99b5e10c665089 /ui | |
parent | 89911dda170e33d8d9ca2f29f2705ed446d4c0f2 (diff) | |
parent | 301ae305b88d0a7e3a3cbca9a500f4b39753dc52 (diff) | |
download | tangerine-wallet-browser-f6ca06f775a739c8b449405c5e96b03151a6a33c.tar tangerine-wallet-browser-f6ca06f775a739c8b449405c5e96b03151a6a33c.tar.gz tangerine-wallet-browser-f6ca06f775a739c8b449405c5e96b03151a6a33c.tar.bz2 tangerine-wallet-browser-f6ca06f775a739c8b449405c5e96b03151a6a33c.tar.lz tangerine-wallet-browser-f6ca06f775a739c8b449405c5e96b03151a6a33c.tar.xz tangerine-wallet-browser-f6ca06f775a739c8b449405c5e96b03151a6a33c.tar.zst tangerine-wallet-browser-f6ca06f775a739c8b449405c5e96b03151a6a33c.zip |
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
Diffstat (limited to 'ui')
23 files changed, 337 insertions, 2146 deletions
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js index 631cf5803..f0703dde2 100644 --- a/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js +++ b/ui/app/components/confirm-page-container/confirm-detail-row/confirm-detail-row.component.js @@ -5,10 +5,10 @@ import classnames from 'classnames' const ConfirmDetailRow = props => { const { label, - fiatFee, - ethFee, + fiatText, + ethText, onHeaderClick, - fiatFeeColor, + fiatTextColor, headerText, headerTextClassName, } = props @@ -27,12 +27,12 @@ const ConfirmDetailRow = props => { </div> <div className="confirm-detail-row__fiat" - style={{ color: fiatFeeColor }} + style={{ color: fiatTextColor }} > - { fiatFee } + { fiatText } </div> <div className="confirm-detail-row__eth"> - { `\u2666 ${ethFee}` } + { ethText } </div> </div> </div> @@ -41,9 +41,9 @@ const ConfirmDetailRow = props => { ConfirmDetailRow.propTypes = { label: PropTypes.string, - fiatFee: PropTypes.string, - ethFee: PropTypes.string, - fiatFeeColor: PropTypes.string, + fiatText: PropTypes.string, + ethText: PropTypes.string, + fiatTextColor: PropTypes.string, onHeaderClick: PropTypes.func, headerText: PropTypes.string, headerTextClassName: PropTypes.string, diff --git a/ui/app/components/network-display/index.scss b/ui/app/components/network-display/index.scss index e82d0e70c..2085cff67 100644 --- a/ui/app/components/network-display/index.scss +++ b/ui/app/components/network-display/index.scss @@ -9,7 +9,7 @@ height: 25px; &--mainnet { - background-color: lighten($blue-lagoon, 45%); + background-color: lighten($blue-lagoon, 68%); } &--ropsten { @@ -17,11 +17,11 @@ } &--kovan { - background-color: lighten($purple, 45%); + background-color: lighten($purple, 65%); } &--rinkeby { - background-color: lighten($tulip-tree, 45%); + background-color: lighten($tulip-tree, 35%); } } diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.component.js b/ui/app/components/pages/confirm-approve/confirm-approve.component.js index d775b0362..b71eaa1d4 100644 --- a/ui/app/components/pages/confirm-approve/confirm-approve.component.js +++ b/ui/app/components/pages/confirm-approve/confirm-approve.component.js @@ -1,29 +1,20 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import ConfirmTransactionBase from '../confirm-transaction-base' +import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' export default class ConfirmApprove extends Component { - static contextTypes = { - t: PropTypes.func, - } - static propTypes = { - tokenAddress: PropTypes.string, - toAddress: PropTypes.string, - tokenAmount: PropTypes.string, + tokenAmount: PropTypes.number, tokenSymbol: PropTypes.string, } render () { - const { toAddress, tokenAddress, tokenAmount, tokenSymbol } = this.props + const { tokenAmount, tokenSymbol } = this.props return ( - <ConfirmTransactionBase - toAddress={toAddress} - identiconAddress={tokenAddress} - title={`${tokenAmount} ${tokenSymbol}`} + <ConfirmTokenTransactionBase + tokenAmount={tokenAmount} warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`} - hideSubtitle /> ) } diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.container.js b/ui/app/components/pages/confirm-approve/confirm-approve.container.js index 040e499ae..4ef9f4ced 100644 --- a/ui/app/components/pages/confirm-approve/confirm-approve.container.js +++ b/ui/app/components/pages/confirm-approve/confirm-approve.container.js @@ -1,25 +1,12 @@ import { connect } from 'react-redux' import ConfirmApprove from './confirm-approve.component' +import { approveTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction' const mapStateToProps = state => { - const { confirmTransaction } = state - const { - 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 - } + const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state + const { tokenAmount } = approveTokenAmountAndToAddressSelector(state) return { - toAddress, - tokenAddress, tokenAmount, tokenSymbol, } diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js index 46ad9ccab..cb39e3d7b 100644 --- a/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js +++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js @@ -1,20 +1,13 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import ConfirmTransactionBase from '../confirm-transaction-base' +import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' import { SEND_ROUTE } from '../../../routes' export default class ConfirmSendToken extends Component { - static contextTypes = { - t: PropTypes.func, - } - static propTypes = { history: PropTypes.object, - tokenAddress: PropTypes.string, - toAddress: PropTypes.string, - numberOfTokens: PropTypes.number, - tokenSymbol: PropTypes.string, editTransaction: PropTypes.func, + tokenAmount: PropTypes.number, } handleEdit (confirmTransactionData) { @@ -24,15 +17,12 @@ export default class ConfirmSendToken extends Component { } render () { - const { toAddress, tokenAddress, tokenSymbol, numberOfTokens } = this.props + const { tokenAmount } = this.props return ( - <ConfirmTransactionBase - toAddress={toAddress} - identiconAddress={tokenAddress} - title={`${numberOfTokens} ${tokenSymbol}`} + <ConfirmTokenTransactionBase onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)} - hideSubtitle + tokenAmount={tokenAmount} /> ) } diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js index 2d7efeed6..d60911e59 100644 --- a/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js +++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js @@ -2,36 +2,16 @@ import { connect } from 'react-redux' import { compose } from 'recompose' import { withRouter } from 'react-router-dom' import ConfirmSendToken from './confirm-send-token.component' -import { calcTokenAmount } from '../../../token-util' import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck' import { setSelectedToken, updateSend, showSendTokenPage } from '../../../actions' import { conversionUtil } from '../../../conversion-util' +import { sendTokenTokenAmountAndToAddressSelector } from '../../../selectors/confirm-transaction' const mapStateToProps = state => { - const { confirmTransaction } = 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 + const { tokenAmount } = sendTokenTokenAmountAndToAddressSelector(state) return { - toAddress, - tokenAddress, - tokenSymbol, - numberOfTokens, + tokenAmount, } } diff --git a/ui/app/components/pages/confirm-send-token/index.scss b/ui/app/components/pages/confirm-send-token/index.scss deleted file mode 100644 index 0476749f6..000000000 --- a/ui/app/components/pages/confirm-send-token/index.scss +++ /dev/null @@ -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; - } -} diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js new file mode 100644 index 000000000..365ae216e --- /dev/null +++ b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js @@ -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} + /> + ) + } +} diff --git a/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js new file mode 100644 index 000000000..be38acdb0 --- /dev/null +++ b/ui/app/components/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js @@ -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) diff --git a/ui/app/components/pages/confirm-token-transaction-base/index.js b/ui/app/components/pages/confirm-token-transaction-base/index.js new file mode 100644 index 000000000..e15c5d56b --- /dev/null +++ b/ui/app/components/pages/confirm-token-transaction-base/index.js @@ -0,0 +1,2 @@ +export { default } from './confirm-token-transaction-base.container' +export { default as ConfirmTokenTransactionBase } from './confirm-token-transaction-base.component' diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js index 5327b116f..e1bf2210f 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -54,6 +54,8 @@ export default class ConfirmTransactionBase extends Component { detailsComponent: PropTypes.node, errorKey: PropTypes.string, errorMessage: PropTypes.string, + ethTotalTextOverride: PropTypes.string, + fiatTotalTextOverride: PropTypes.string, hideData: PropTypes.bool, hideDetails: PropTypes.bool, hideSubtitle: PropTypes.bool, @@ -146,6 +148,8 @@ export default class ConfirmTransactionBase extends Component { currentCurrency, fiatTransactionTotal, ethTransactionTotal, + fiatTotalTextOverride, + ethTotalTextOverride, hideDetails, } = this.props @@ -153,14 +157,16 @@ export default class ConfirmTransactionBase extends Component { return null } + const formattedCurrency = formatCurrency(fiatTransactionTotal, currentCurrency) + return ( detailsComponent || ( <div className="confirm-page-container-content__details"> <div className="confirm-page-container-content__gas-fee"> <ConfirmDetailRow label="Gas Fee" - fiatFee={formatCurrency(fiatTransactionFee, currentCurrency)} - ethFee={ethTransactionFee} + fiatText={formatCurrency(fiatTransactionFee, currentCurrency)} + ethText={`\u2666 ${ethTransactionFee}`} headerText="Edit" headerTextClassName="confirm-detail-row__header-text--edit" onHeaderClick={() => this.handleEditGas()} @@ -169,11 +175,11 @@ export default class ConfirmTransactionBase extends Component { <div> <ConfirmDetailRow label="Total" - fiatFee={formatCurrency(fiatTransactionTotal, currentCurrency)} - ethFee={ethTransactionTotal} + fiatText={fiatTotalTextOverride || formattedCurrency} + ethText={ethTotalTextOverride || `\u2666 ${ethTransactionTotal}`} headerText="Amount + Gas Fee" headerTextClassName="confirm-detail-row__header-text--total" - fiatFeeColor="#2f9ae0" + fiatTextColor="#2f9ae0" /> </div> </div> @@ -206,17 +212,21 @@ export default class ConfirmTransactionBase extends Component { <div className="confirm-page-container-content__data-box-label"> {`${t('functionType')}:`} <span className="confirm-page-container-content__function-type"> - { name } + { name || t('notFound') } </span> </div> - <div className="confirm-page-container-content__data-box"> - <div className="confirm-page-container-content__data-field-label"> - { `${t('parameters')}:` } - </div> - <div> - <pre>{ JSON.stringify(params, null, 2) }</pre> - </div> - </div> + { + params && ( + <div className="confirm-page-container-content__data-box"> + <div className="confirm-page-container-content__data-field-label"> + { `${t('parameters')}:` } + </div> + <div> + <pre>{ JSON.stringify(params, null, 2) }</pre> + </div> + </div> + ) + } <div className="confirm-page-container-content__data-box-label"> {`${t('hexData')}:`} </div> @@ -297,7 +307,7 @@ export default class ConfirmTransactionBase extends Component { toName={toName} toAddress={toAddress} showEdit={onEdit && !isTxReprice} - action={action || name} + action={action || name || this.context.t('unknownFunction')} title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`} subtitle={subtitle || `\u2666 ${ethTransactionAmount}`} hideSubtitle={hideSubtitle} diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js index 25259b98c..0280f73c6 100644 --- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js +++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js @@ -8,11 +8,16 @@ import { CONFIRM_SEND_ETHER_PATH, CONFIRM_SEND_TOKEN_PATH, CONFIRM_APPROVE_PATH, + CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, } from '../../../routes' 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 { static propTypes = { @@ -27,8 +32,7 @@ export default class ConfirmTransactionSwitch extends Component { methodData: { name }, fetchingMethodData, } = this.props - const { id } = txData - + const { id, txParams: { data } = {} } = txData if (isConfirmDeployContract(txData)) { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}` @@ -39,10 +43,10 @@ export default class ConfirmTransactionSwitch extends Component { return <Loading /> } - if (name) { - const methodName = name.toLowerCase() + if (data) { + const methodName = name && name.toLowerCase() - switch (methodName.toLowerCase()) { + switch (methodName) { case TOKEN_METHOD_TRANSFER: { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}` return <Redirect to={{ pathname }} /> @@ -51,6 +55,10 @@ export default class ConfirmTransactionSwitch extends Component { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}` return <Redirect to={{ pathname }} /> } + case TOKEN_METHOD_TRANSFER_FROM: { + const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}` + return <Redirect to={{ pathname }} /> + } default: { const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}` return <Redirect to={{ pathname }} /> diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js index 622d2a37a..9db4a2f96 100644 --- a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js +++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js @@ -1,2 +1,3 @@ export const TOKEN_METHOD_TRANSFER = 'transfer' export const TOKEN_METHOD_APPROVE = 'approve' +export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom' diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js index 874a89fd2..3ac656d73 100644 --- a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js +++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js @@ -8,6 +8,7 @@ import ConfirmSendEther from '../confirm-send-ether' import ConfirmSendToken from '../confirm-send-token' import ConfirmDeployContract from '../confirm-deploy-contract' import ConfirmApprove from '../confirm-approve' +import ConfirmTokenTransactionBase from '../confirm-token-transaction-base' import ConfTx from '../../../conf-tx' import { DEFAULT_ROUTE, @@ -16,6 +17,7 @@ import { CONFIRM_SEND_ETHER_PATH, CONFIRM_SEND_TOKEN_PATH, CONFIRM_APPROVE_PATH, + CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, } from '../../../routes' @@ -139,6 +141,11 @@ export default class ConfirmTransaction extends Component { /> <Route exact + path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`} + component={ConfirmTokenTransactionBase} + /> + <Route + exact path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`} component={ConfTx} /> diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss index 8b333b6a8..b15c59863 100644 --- a/ui/app/components/pages/index.scss +++ b/ui/app/components/pages/index.scss @@ -3,5 +3,3 @@ @import './add-token/index'; @import './confirm-add-token/index'; - -@import './confirm-send-token/index'; diff --git a/ui/app/components/pending-tx/confirm-deploy-contract.js b/ui/app/components/pending-tx/confirm-deploy-contract.js deleted file mode 100644 index a059d1a32..000000000 --- a/ui/app/components/pending-tx/confirm-deploy-contract.js +++ /dev/null @@ -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) diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js deleted file mode 100644 index 67f54aa26..000000000 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ /dev/null @@ -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) -} diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js deleted file mode 100644 index 818853882..000000000 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ /dev/null @@ -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) -} diff --git a/ui/app/components/pending-tx/index.js b/ui/app/components/pending-tx/index.js deleted file mode 100644 index 3f8cd8823..000000000 --- a/ui/app/components/pending-tx/index.js +++ /dev/null @@ -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, -} diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 4e8aaa07d..112ea6bca 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -9,11 +9,7 @@ const txHelper = require('../lib/tx-helper') const log = require('loglevel') const R = require('ramda') -const PendingTx = require('./components/pending-tx') 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 { DEFAULT_ROUTE } = require('./routes') @@ -151,101 +147,32 @@ ConfirmTxScreen.prototype.render = function () { currentCurrency, conversionRate, blockGasLimit, - // provider, - // computedBalances, } = props var txData = this.getTxData() || {} - var txParams = txData.params || {} - - // var isNotification = isPopupOrNotification() === 'notification' - /* - Client is using the flag above to render the following in conf screen - // subtitle and nav - h('.section-title.flex-row.flex-center', [ - !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { - onClick: this.goHome.bind(this), - }) : null, - h('h2.page-subtitle', 'Confirm Transaction'), - isNotification ? h(NetworkIndicator, { - network: network, - provider: provider, - }) : null, - ]), - */ - - return currentTxView({ - // Properties - txData: txData, - key: txData.id, - selectedAddress: props.selectedAddress, - 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)) + const { msgParams } = txData + log.debug('msgParams detected, rendering pending msg') + + return msgParams + ? h(SignatureRequest, { + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + 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), + }) + : h(Loading) } ConfirmTxScreen.prototype.signMessage = function (msgData, event) { @@ -295,20 +222,3 @@ ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) { this.stopPropagation(event) 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) -// } -// } diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js index ad247a348..1373d28df 100644 --- a/ui/app/helpers/confirm-transaction/util.js +++ b/ui/app/helpers/confirm-transaction/util.js @@ -114,3 +114,20 @@ export function formatCurrency (value, currencyCode) { ? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode }) : value } + +export function convertTokenToFiat ({ + value, + toCurrency, + conversionRate, + contractExchangeRate, +}) { + const totalExchangeRate = conversionRate * contractExchangeRate + + return conversionUtil(value, { + fromNumericBase: 'dec', + toNumericBase: 'dec', + toCurrency, + numberOfDecimals: 2, + conversionRate: totalExchangeRate, + }) +} diff --git a/ui/app/routes.js b/ui/app/routes.js index 7ac606b1a..9c219115b 100644 --- a/ui/app/routes.js +++ b/ui/app/routes.js @@ -26,6 +26,7 @@ const CONFIRM_SEND_ETHER_PATH = '/send-ether' const CONFIRM_SEND_TOKEN_PATH = '/send-token' const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract' const CONFIRM_APPROVE_PATH = '/approve' +const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from' const CONFIRM_TOKEN_METHOD_PATH = '/token-method' const SIGNATURE_REQUEST_PATH = '/signature-request' @@ -57,6 +58,7 @@ module.exports = { CONFIRM_SEND_TOKEN_PATH, CONFIRM_DEPLOY_CONTRACT_PATH, CONFIRM_APPROVE_PATH, + CONFIRM_TRANSFER_FROM_PATH, CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, } diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js index cde83804d..54016a30e 100644 --- a/ui/app/selectors/confirm-transaction.js +++ b/ui/app/selectors/confirm-transaction.js @@ -1,5 +1,6 @@ import { createSelector } from 'reselect' import txHelper from '../../lib/tx-helper' +import { calcTokenAmount } from '../token-util' const unapprovedTxsSelector = state => state.metamask.unapprovedTxs const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs @@ -63,3 +64,101 @@ export const unconfirmedTransactionsHashSelector = createSelector( export const currentCurrencySelector = state => state.metamask.currentCurrency 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] +) |