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 | |
parent | 89911dda170e33d8d9ca2f29f2705ed446d4c0f2 (diff) | |
parent | 301ae305b88d0a7e3a3cbca9a500f4b39753dc52 (diff) | |
download | tangerine-wallet-browser-f6ca06f775a739c8b449405c5e96b03151a6a33c.tar.gz 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
26 files changed, 351 insertions, 2221 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 03f62424c..6da058786 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -593,6 +593,9 @@ "noTransactions": { "message": "No Transactions" }, + "notFound": { + "message": "Not Found" + }, "notStarted": { "message": "Not Started" }, @@ -972,6 +975,9 @@ "unknown": { "message": "Unknown" }, + "unknownFunction": { + "message": "Unknown Function" + }, "unknownNetwork": { "message": "Unknown Private Network" }, diff --git a/test/e2e/beta/metamask-beta-ui.spec.js b/test/e2e/beta/metamask-beta-ui.spec.js index caa49d450..ee1aa0ff1 100644 --- a/test/e2e/beta/metamask-beta-ui.spec.js +++ b/test/e2e/beta/metamask-beta-ui.spec.js @@ -513,7 +513,7 @@ describe('MetaMask', function () { it('displays the contract creation data', async () => { const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) dataTab.click() - await (regularDelayMs) + await delay(regularDelayMs) 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')]`)) detailsTab.click() - await (regularDelayMs) + await delay(regularDelayMs) }) it('confirms a deploy contract transaction', async () => { @@ -548,7 +548,7 @@ describe('MetaMask', function () { await delay(regularDelayMs) await driver.switchTo().window(extension) - await delay(regularDelayMs) + await delay(largeDelayMs) await findElements(driver, By.css('.tx-list-pending-item-container')) const [txListValue] = await findElements(driver, By.css('.tx-list-value')) @@ -741,7 +741,7 @@ describe('MetaMask', function () { it('displays the token transfer data', async () => { const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) dataTab.click() - await (regularDelayMs) + await delay(regularDelayMs) const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type')) const functionTypeText = await functionType.getText() @@ -753,7 +753,7 @@ describe('MetaMask', function () { const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) detailsTab.click() - await (regularDelayMs) + await delay(regularDelayMs) }) it('submits the transaction', async function () { @@ -901,7 +901,7 @@ describe('MetaMask', function () { it('displays the token approval data', async () => { const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`)) dataTab.click() - await (regularDelayMs) + await delay(regularDelayMs) const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type')) const functionTypeText = await functionType.getText() @@ -913,12 +913,12 @@ describe('MetaMask', function () { const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`)) detailsTab.click() - await (regularDelayMs) + await delay(regularDelayMs) const approvalWarning = await findElement(driver, By.css('.confirm-page-container-warning__warning')) const approvalWarningText = await approvalWarning.getText() assert(approvalWarningText.match(/By approving this/)) - await (regularDelayMs) + await delay(regularDelayMs) }) it('opens the gas edit modal', async () => { diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js deleted file mode 100644 index c68e013ac..000000000 --- a/test/unit/components/pending-tx-test.js +++ /dev/null @@ -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() - }) -}) - 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] +) |