diff options
49 files changed, 561 insertions, 2287 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml index 7063f3113..c0262fcfa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -448,4 +448,4 @@ jobs: steps: - run: name: All Tests Passed - command: echo 'weew - everything passed!' + command: echo 'weew - everything passed!'
\ No newline at end of file diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 08d1d33ae..8d65bc596 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -647,6 +647,9 @@ "noTransactions": { "message": "No Transactions" }, + "notFound": { + "message": "Not Found" + }, "notStarted": { "message": "Not Started" }, @@ -1074,6 +1077,9 @@ "unknown": { "message": "Unknown" }, + "unknownFunction": { + "message": "Unknown Function" + }, "unknownNetwork": { "message": "Unknown Private Network" }, diff --git a/app/manifest.json b/app/manifest.json index a226adfb0..b67cef025 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -62,7 +62,8 @@ "https://*.infura.io/", "activeTab", "webRequest", - "*://*.eth/" + "*://*.eth/", + "*://*.test/" ], "web_accessible_resources": [ "inpage.js" diff --git a/app/scripts/background.js b/app/scripts/background.js index 1479d9f72..7eb7b1255 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -44,8 +44,8 @@ const notificationManager = new NotificationManager() global.METAMASK_NOTIFIER = notificationManager // setup sentry error reporting -const release = platform.getVersion() -const raven = setupRaven({ release }) +const releaseVersion = platform.getVersion() +const raven = setupRaven({ releaseVersion }) // browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser // Internet Explorer 6-11 @@ -53,6 +53,7 @@ const isIE = !!document.documentMode // Edge 20+ const isEdge = !isIE && !!window.StyleMedia +let ipfsHandle let popupIsOpen = false let notificationIsOpen = false const openMetamaskTabsIDs = {} @@ -158,7 +159,7 @@ async function initialize () { const initLangCode = await getFirstPreferredLangCode() await setupController(initState, initLangCode) log.debug('MetaMask initialization complete.') - ipfsContent(initState.NetworkController.provider) + ipfsHandle = ipfsContent(initState.NetworkController.provider) } // @@ -262,6 +263,10 @@ function setupController (initState, initLangCode) { }) global.metamaskController = controller + controller.networkController.on('networkDidChange', () => { + ipfsHandle && ipfsHandle.remove() + ipfsHandle = ipfsContent(controller.networkController.providerStore.getState()) + }) // report failed transactions to Sentry controller.txController.on(`tx:status-update`, (txId, status) => { diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 04dd51b01..7c775fb04 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -178,6 +178,7 @@ function blacklistedDomainCheck () { 'adyen.com', 'gravityforms.com', 'harbourair.com', + 'blueskybooking.com', ] var currentUrl = window.location.href var currentRegex diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js index a6b99b2f9..5222151ea 100644 --- a/app/scripts/lib/ipfsContent.js +++ b/app/scripts/lib/ipfsContent.js @@ -2,39 +2,43 @@ const extension = require('extensionizer') const resolver = require('./resolver.js') module.exports = function (provider) { - extension.webRequest.onBeforeRequest.addListener(details => { - const urlhttpreplace = details.url.replace(/\w+?:\/\//, '') - const url = urlhttpreplace.replace(/[\\/].*/g, '') // eslint-disable-line no-useless-escape - let domainhtml = urlhttpreplace.match(/[\\/].*/g) // eslint-disable-line no-useless-escape - let clearTime = null - const name = url.replace(/\/$/g, '') - if (domainhtml === null) domainhtml = [''] - extension.tabs.getSelected(null, tab => { - extension.tabs.update(tab.id, { url: 'loading.html' }) + function ipfsContent (details) { + const name = details.url.substring(7, details.url.length - 1) + let clearTime = null + extension.tabs.getSelected(null, tab => { + extension.tabs.update(tab.id, { url: 'loading.html' }) - clearTime = setTimeout(() => { - return extension.tabs.update(tab.id, { url: '404.html' }) - }, 60000) + clearTime = setTimeout(() => { + return extension.tabs.update(tab.id, { url: '404.html' }) + }, 60000) - resolver.resolve(name, provider).then(ipfsHash => { - clearTimeout(clearTime) - let url = 'https://ipfs.infura.io/ipfs/' + ipfsHash + domainhtml[0] - return fetch(url, { method: 'HEAD' }).then(response => response.status).then(statusCode => { - if (statusCode !== 200) return extension.tabs.update(tab.id, { url: '404.html' }) - extension.tabs.update(tab.id, { url: url }) - }) - .catch(err => { - url = 'https://ipfs.infura.io/ipfs/' + ipfsHash + domainhtml[0] - extension.tabs.update(tab.id, {url: url}) - return err - }) - }) - .catch(err => { - clearTimeout(clearTime) - const url = err === 'unsupport' ? 'unsupport' : 'error' - extension.tabs.update(tab.id, {url: `${url}.html?name=${name}`}) - }) - }) - return { cancel: true } - }, {urls: ['*://*.eth/', '*://*.eth/*']}) + resolver.resolve(name, provider).then(ipfsHash => { + clearTimeout(clearTime) + let url = 'https://ipfs.infura.io/ipfs/' + ipfsHash + return fetch(url, { method: 'HEAD' }).then(response => response.status).then(statusCode => { + if (statusCode !== 200) return extension.tabs.update(tab.id, { url: '404.html' }) + extension.tabs.update(tab.id, { url: url }) + }) + .catch(err => { + url = 'https://ipfs.infura.io/ipfs/' + ipfsHash + extension.tabs.update(tab.id, {url: url}) + return err + }) + }) + .catch(err => { + clearTimeout(clearTime) + const url = err === 'unsupport' ? 'unsupport' : 'error' + extension.tabs.update(tab.id, {url: `${url}.html?name=${name}`}) + }) + }) + return { cancel: true } + } + + extension.webRequest.onBeforeRequest.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']}) + + return { + remove () { + extension.webRequest.onBeforeRequest.removeListener(ipfsContent) + }, + } } diff --git a/app/scripts/lib/resolver.js b/app/scripts/lib/resolver.js index 6786929d8..ff0fed161 100644 --- a/app/scripts/lib/resolver.js +++ b/app/scripts/lib/resolver.js @@ -60,8 +60,8 @@ function getRegistrar (type) { module.exports.resolve = function (name, provider) { const path = name.split('.') - const tld = path[path.length - 1] - if (tld === 'eth') { + const topLevelDomain = path[path.length - 1] + if (topLevelDomain === 'eth' || topLevelDomain === 'test') { return ens(name, provider) } else { return new Promise((resolve, reject) => { diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js index 3f69fb3bb..e657e278f 100644 --- a/app/scripts/lib/setupRaven.js +++ b/app/scripts/lib/setupRaven.js @@ -8,8 +8,10 @@ module.exports = setupRaven // Setup raven / sentry remote error reporting function setupRaven (opts) { - const { release } = opts + const { releaseVersion } = opts let ravenTarget + // detect brave + const isBrave = Boolean(window.chrome.ipcRenderer) if (METAMASK_DEBUG) { console.log('Setting up Sentry Remote Error Reporting: DEV') @@ -20,9 +22,11 @@ function setupRaven (opts) { } const client = Raven.config(ravenTarget, { - release, + releaseVersion, transport: function (opts) { + opts.data.extra.isBrave = isBrave const report = opts.data + try { // handle error-like non-error exceptions rewriteErrorLikeExceptions(report) 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/actions.js b/ui/app/actions.js index 4557b5961..6c947fc35 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -174,6 +174,7 @@ var actions = { UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL', UPDATE_SEND_FROM: 'UPDATE_SEND_FROM', + UPDATE_SEND_HEX_DATA: 'UPDATE_SEND_HEX_DATA', UPDATE_SEND_TOKEN_BALANCE: 'UPDATE_SEND_TOKEN_BALANCE', UPDATE_SEND_TO: 'UPDATE_SEND_TO', UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT', @@ -193,6 +194,7 @@ var actions = { setSendTokenBalance, updateSendTokenBalance, updateSendFrom, + updateSendHexData, updateSendTo, updateSendAmount, updateSendMemo, @@ -950,6 +952,13 @@ function updateSendFrom (from) { } } +function updateSendHexData (value) { + return { + type: actions.UPDATE_SEND_HEX_DATA, + value, + } +} + function updateSendTo (to, nickname = '') { return { type: actions.UPDATE_SEND_TO, 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/components/selected-account/selected-account.component.js b/ui/app/components/selected-account/selected-account.component.js index 3386a4196..79f0cb93a 100644 --- a/ui/app/components/selected-account/selected-account.component.js +++ b/ui/app/components/selected-account/selected-account.component.js @@ -5,11 +5,11 @@ import copyToClipboard from 'copy-to-clipboard' const Tooltip = require('../tooltip-v2.js') const addressStripper = (address = '') => { - if (address.length < 4) { + if (address.length < 11) { return address } - return `${address.slice(0, 4)}...${address.slice(-4)}` + return `${address.slice(0, 6)}...${address.slice(-4)}` } class SelectedAccount extends Component { diff --git a/ui/app/components/send/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js index adc114c0e..7a0b1a18e 100644 --- a/ui/app/components/send/send-content/send-content.component.js +++ b/ui/app/components/send/send-content/send-content.component.js @@ -4,6 +4,7 @@ import PageContainerContent from '../../page-container/page-container-content.co import SendAmountRow from './send-amount-row/' import SendFromRow from './send-from-row/' import SendGasRow from './send-gas-row/' +import SendHexDataRow from './send-hex-data-row' import SendToRow from './send-to-row/' export default class SendContent extends Component { @@ -20,6 +21,7 @@ export default class SendContent extends Component { <SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} /> <SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} /> <SendGasRow /> + <SendHexDataRow /> </div> </PageContainerContent> ) diff --git a/ui/app/components/send/send-content/send-hex-data-row/index.js b/ui/app/components/send/send-content/send-hex-data-row/index.js new file mode 100644 index 000000000..08c341067 --- /dev/null +++ b/ui/app/components/send/send-content/send-hex-data-row/index.js @@ -0,0 +1 @@ +export { default } from './send-hex-data-row.container' diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js new file mode 100644 index 000000000..063930db3 --- /dev/null +++ b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.component.js @@ -0,0 +1,40 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SendRowWrapper from '../send-row-wrapper' + +export default class SendHexDataRow extends Component { + static propTypes = { + data: PropTypes.string, + inError: PropTypes.bool, + updateSendHexData: PropTypes.func.isRequired, + }; + + static contextTypes = { + t: PropTypes.func, + }; + + onInput = (event) => { + const {updateSendHexData} = this.props + event.target.value = event.target.value.replace(/\n/g, '') + updateSendHexData(event.target.value || null) + } + + render () { + const {inError} = this.props + const {t} = this.context + + return ( + <SendRowWrapper + label={`${t('hexData')}:`} + showError={inError} + errorType={'amount'} + > + <textarea + onInput={this.onInput} + placeholder="Optional" + className="send-v2__hex-data__input" + /> + </SendRowWrapper> + ) + } +} diff --git a/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js new file mode 100644 index 000000000..df554ca5f --- /dev/null +++ b/ui/app/components/send/send-content/send-hex-data-row/send-hex-data-row.container.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux' +import { + updateSendHexData, +} from '../../../../actions' +import SendHexDataRow from './send-hex-data-row.component' + +export default connect(mapStateToProps, mapDispatchToProps)(SendHexDataRow) + +function mapStateToProps (state) { + return { + data: state.metamask.send.data, + } +} + +function mapDispatchToProps (dispatch) { + return { + updateSendHexData (data) { + return dispatch(updateSendHexData(data)) + }, + } +} diff --git a/ui/app/components/send/send-footer/send-footer.component.js b/ui/app/components/send/send-footer/send-footer.component.js index 2085f1dce..518cff06e 100644 --- a/ui/app/components/send/send-footer/send-footer.component.js +++ b/ui/app/components/send/send-footer/send-footer.component.js @@ -8,6 +8,7 @@ export default class SendFooter extends Component { static propTypes = { addToAddressBookIfNew: PropTypes.func, amount: PropTypes.string, + data: PropTypes.string, clearSend: PropTypes.func, disabled: PropTypes.bool, editingTransactionId: PropTypes.string, @@ -41,6 +42,7 @@ export default class SendFooter extends Component { const { addToAddressBookIfNew, amount, + data, editingTransactionId, from: {address: from}, gasLimit: gas, @@ -68,6 +70,7 @@ export default class SendFooter extends Component { const promise = editingTransactionId ? update({ amount, + data, editingTransactionId, from, gas, @@ -76,7 +79,7 @@ export default class SendFooter extends Component { to, unapprovedTxs, }) - : sign({ selectedToken, to, amount, from, gas, gasPrice }) + : sign({ data, selectedToken, to, amount, from, gas, gasPrice }) Promise.resolve(promise) .then(() => history.push(CONFIRM_TRANSACTION_ROUTE)) diff --git a/ui/app/components/send/send-footer/send-footer.container.js b/ui/app/components/send/send-footer/send-footer.container.js index 0af6fcfa1..60de4d030 100644 --- a/ui/app/components/send/send-footer/send-footer.container.js +++ b/ui/app/components/send/send-footer/send-footer.container.js @@ -18,6 +18,7 @@ import { getSendFromObject, getSendTo, getSendToAccounts, + getSendHexData, getTokenBalance, getUnapprovedTxs, } from '../send.selectors' @@ -35,6 +36,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(SendFooter) function mapStateToProps (state) { return { amount: getSendAmount(state), + data: getSendHexData(state), editingTransactionId: getSendEditingTransactionId(state), from: getSendFromObject(state), gasLimit: getGasLimit(state), @@ -52,9 +54,10 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { clearSend: () => dispatch(clearSend()), - sign: ({ selectedToken, to, amount, from, gas, gasPrice }) => { + sign: ({ selectedToken, to, amount, from, gas, gasPrice, data }) => { const txParams = constructTxParams({ amount, + data, from, gas, gasPrice, @@ -68,6 +71,7 @@ function mapDispatchToProps (dispatch) { }, update: ({ amount, + data, editingTransactionId, from, gas, @@ -78,6 +82,7 @@ function mapDispatchToProps (dispatch) { }) => { const editingTx = constructUpdatedTx({ amount, + data, editingTransactionId, from, gas, diff --git a/ui/app/components/send/send-footer/send-footer.utils.js b/ui/app/components/send/send-footer/send-footer.utils.js index 875e7d948..f82ff1e9b 100644 --- a/ui/app/components/send/send-footer/send-footer.utils.js +++ b/ui/app/components/send/send-footer/send-footer.utils.js @@ -8,8 +8,9 @@ function addHexPrefixToObjectValues (obj) { }, {}) } -function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice }) { +function constructTxParams ({ selectedToken, data, to, amount, from, gas, gasPrice }) { const txParams = { + data, from, value: '0', gas, @@ -21,13 +22,12 @@ function constructTxParams ({ selectedToken, to, amount, from, gas, gasPrice }) txParams.to = to } - const hexPrefixedTxParams = addHexPrefixToObjectValues(txParams) - - return hexPrefixedTxParams + return addHexPrefixToObjectValues(txParams) } function constructUpdatedTx ({ amount, + data, editingTransactionId, from, gas, @@ -36,9 +36,21 @@ function constructUpdatedTx ({ to, unapprovedTxs, }) { + const unapprovedTx = unapprovedTxs[editingTransactionId] + const txParamsData = unapprovedTx.txParams.data ? unapprovedTx.txParams.data : data const editingTx = { - ...unapprovedTxs[editingTransactionId], - txParams: addHexPrefixToObjectValues({ from, gas, gasPrice }), + ...unapprovedTx, + txParams: Object.assign( + unapprovedTx.txParams, + addHexPrefixToObjectValues({ + data: txParamsData, + to, + from, + gas, + gasPrice, + value: amount, + }) + ), } if (selectedToken) { @@ -52,18 +64,10 @@ function constructUpdatedTx ({ to: selectedToken.address, data, })) - } else { - const { data } = unapprovedTxs[editingTransactionId].txParams - - Object.assign(editingTx.txParams, addHexPrefixToObjectValues({ - value: amount, - to, - data, - })) + } - if (typeof editingTx.txParams.data === 'undefined') { - delete editingTx.txParams.data - } + if (typeof editingTx.txParams.data === 'undefined') { + delete editingTx.txParams.data } return editingTx diff --git a/ui/app/components/send/send-footer/tests/send-footer-component.test.js b/ui/app/components/send/send-footer/tests/send-footer-component.test.js index 4b2cd327d..65e4bb654 100644 --- a/ui/app/components/send/send-footer/tests/send-footer-component.test.js +++ b/ui/app/components/send/send-footer/tests/send-footer-component.test.js @@ -129,6 +129,7 @@ describe('SendFooter Component', function () { assert.deepEqual( propsMethodSpies.update.getCall(0).args[0], { + data: undefined, amount: 'mockAmount', editingTransactionId: 'mockEditingTransactionId', from: 'mockAddress', @@ -152,6 +153,7 @@ describe('SendFooter Component', function () { assert.deepEqual( propsMethodSpies.sign.getCall(0).args[0], { + data: undefined, amount: 'mockAmount', from: 'mockAddress', gas: 'mockGasLimit', diff --git a/ui/app/components/send/send-footer/tests/send-footer-container.test.js b/ui/app/components/send/send-footer/tests/send-footer-container.test.js index 39d6a7686..cf4c893ee 100644 --- a/ui/app/components/send/send-footer/tests/send-footer-container.test.js +++ b/ui/app/components/send/send-footer/tests/send-footer-container.test.js @@ -38,6 +38,7 @@ proxyquire('../send-footer.container.js', { getSendTo: (s) => `mockTo:${s}`, getSendToAccounts: (s) => `mockToAccounts:${s}`, getTokenBalance: (s) => `mockTokenBalance:${s}`, + getSendHexData: (s) => `mockHexData:${s}`, getUnapprovedTxs: (s) => `mockUnapprovedTxs:${s}`, }, './send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` }, @@ -51,6 +52,7 @@ describe('send-footer container', () => { it('should map the correct properties to props', () => { assert.deepEqual(mapStateToProps('mockState'), { amount: 'mockAmount:mockState', + data: 'mockHexData:mockState', selectedToken: 'mockSelectedToken:mockState', editingTransactionId: 'mockEditingTransactionId:mockState', from: 'mockFromObject:mockState', @@ -100,6 +102,7 @@ describe('send-footer container', () => { assert.deepEqual( utilsStubs.constructTxParams.getCall(0).args[0], { + data: undefined, selectedToken: { address: '0xabc', }, @@ -129,6 +132,7 @@ describe('send-footer container', () => { assert.deepEqual( utilsStubs.constructTxParams.getCall(0).args[0], { + data: undefined, selectedToken: undefined, to: 'mockTo', amount: 'mockAmount', @@ -160,6 +164,7 @@ describe('send-footer container', () => { assert.deepEqual( utilsStubs.constructUpdatedTx.getCall(0).args[0], { + data: undefined, to: 'mockTo', amount: 'mockAmount', from: 'mockFrom', diff --git a/ui/app/components/send/send-footer/tests/send-footer-utils.test.js b/ui/app/components/send/send-footer/tests/send-footer-utils.test.js index 2d3135995..28ff0c891 100644 --- a/ui/app/components/send/send-footer/tests/send-footer-utils.test.js +++ b/ui/app/components/send/send-footer/tests/send-footer-utils.test.js @@ -65,6 +65,28 @@ describe('send-footer utils', () => { }) describe('constructTxParams()', () => { + it('should return a new txParams object with data if there data is given', () => { + assert.deepEqual( + constructTxParams({ + data: 'someData', + selectedToken: false, + to: 'mockTo', + amount: 'mockAmount', + from: 'mockFrom', + gas: 'mockGas', + gasPrice: 'mockGasPrice', + }), + { + data: '0xsomeData', + to: '0xmockTo', + value: '0xmockAmount', + from: '0xmockFrom', + gas: '0xmockGas', + gasPrice: '0xmockGasPrice', + } + ) + }) + it('should return a new txParams object with value and to properties if there is no selectedToken', () => { assert.deepEqual( constructTxParams({ @@ -76,6 +98,7 @@ describe('send-footer utils', () => { gasPrice: 'mockGasPrice', }), { + data: undefined, to: '0xmockTo', value: '0xmockAmount', from: '0xmockFrom', @@ -96,6 +119,7 @@ describe('send-footer utils', () => { gasPrice: 'mockGasPrice', }), { + data: undefined, value: '0x0', from: '0xmockFrom', gas: '0xmockGas', diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js index f910f7caf..cf07eafe1 100644 --- a/ui/app/components/send/send.selectors.js +++ b/ui/app/components/send/send.selectors.js @@ -33,6 +33,7 @@ const selectors = { getSelectedTokenExchangeRate, getSelectedTokenToFiatRate, getSendAmount, + getSendHexData, getSendEditingTransactionId, getSendErrors, getSendFrom, @@ -210,6 +211,10 @@ function getSendAmount (state) { return state.metamask.send.amount } +function getSendHexData (state) { + return state.metamask.send.data +} + function getSendEditingTransactionId (state) { return state.metamask.send.editingTransactionId } diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index da142fad8..20c2be0f1 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -175,7 +175,7 @@ WalletView.prototype.render = function () { this.setState({ copyToClipboardPressed: false }) }, }, [ - `${checksummedAddress.slice(0, 4)}...${checksummedAddress.slice(-4)}`, + `${checksummedAddress.slice(0, 6)}...${checksummedAddress.slice(-4)}`, h('i.fa.fa-clipboard', { style: { marginLeft: '8px' } }), ]), ]), 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/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss index c168242cf..e9c872ea7 100644 --- a/ui/app/css/itcss/components/send.scss +++ b/ui/app/css/itcss/components/send.scss @@ -628,7 +628,7 @@ } } - &__to-autocomplete, &__memo-text-area { + &__to-autocomplete, &__memo-text-area, &__hex-data { &__input { height: 54px; width: 100%; @@ -899,4 +899,4 @@ .sliders-icon { color: $curious-blue; -}
\ No newline at end of file +} 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/reducers/metamask.js b/ui/app/reducers/metamask.js index 6c8ac9ed7..3f1d3394f 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -222,6 +222,14 @@ function reduceMetamask (state, action) { }, }) + case actions.UPDATE_SEND_HEX_DATA: + return extend(metamaskState, { + send: { + ...metamaskState.send, + data: action.value, + }, + }) + case actions.UPDATE_SEND_FROM: return extend(metamaskState, { send: { diff --git a/ui/app/routes.js b/ui/app/routes.js index ec0cc9027..f6b2a7a55 100644 --- a/ui/app/routes.js +++ b/ui/app/routes.js @@ -27,6 +27,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' @@ -59,6 +60,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] +) |