diff --git a/CHANGELOG.md b/CHANGELOG.md index b77ceb864..0b521584b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ - Removed support for old, lightwallet based vault. Users who have not opened app in over a month will need to recover with their seed phrase. This will allow Firefox support sooner. - Fixed bug where spinner wouldn't disappear on incorrect password submission on seed word reveal. - Polish the private key UI. +- Enforce minimum values for gas price and gas limit. +- Fix bug where total gas was sometimes not live-updated. +- Fix bug where editing gas value could have some abrupt behaviors (#1233) - Add Kovan as an option on our network list. ## 3.4.0 2017-3-8 diff --git a/ui/app/actions.js b/ui/app/actions.js index d02b7dcaa..7288db256 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -397,7 +397,6 @@ function signTx (txData) { dispatch(actions.hideLoadingIndication()) if (err) return dispatch(actions.displayWarning(err.message)) dispatch(actions.hideWarning()) - dispatch(actions.goHome()) }) dispatch(this.showConfTxPage()) } diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js index c89ed0416..e37aaa8c3 100644 --- a/ui/app/components/hex-as-decimal-input.js +++ b/ui/app/components/hex-as-decimal-input.js @@ -9,6 +9,7 @@ module.exports = HexAsDecimalInput inherits(HexAsDecimalInput, Component) function HexAsDecimalInput () { + this.state = { invalid: null } Component.call(this) } @@ -23,49 +24,120 @@ function HexAsDecimalInput () { HexAsDecimalInput.prototype.render = function () { const props = this.props - const { value, onChange } = props + const state = this.state + + const { value, onChange, min, max } = props + const toEth = props.toEth const suffix = props.suffix const decimalValue = decimalize(value, toEth) const style = props.style return ( - h('.flex-row', { - style: { - alignItems: 'flex-end', - lineHeight: '13px', - fontFamily: 'Montserrat Light', - textRendering: 'geometricPrecision', - }, - }, [ - h('input.ether-balance.ether-balance-amount', { - type: 'number', - style: extend({ - display: 'block', - textAlign: 'right', - backgroundColor: 'transparent', - border: '1px solid #bdbdbd', - - }, style), - value: decimalValue, - onChange: (event) => { - const hexString = (event.target.value === '') ? '' : hexify(event.target.value) - onChange(hexString) - }, - }), - h('div', { + h('.flex-column', [ + h('.flex-row', { style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', }, - }, suffix), + }, [ + h('input.hex-input', { + type: 'number', + required: true, + min: min, + max: max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: parseInt(decimalValue), + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const hexString = (event.target.value === '') ? '' : hexify(event.target.value) + onChange(hexString) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, ]) ) } +HexAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +HexAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + if (valid) { + this.setState({ invalid: null }) + } +} + +HexAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + function hexify (decimalString) { const hexBN = new BN(decimalString, 10) return '0x' + hexBN.toString('hex') diff --git a/ui/app/components/pending-tx-details.js b/ui/app/components/pending-tx-details.js deleted file mode 100644 index e92ce575f..000000000 --- a/ui/app/components/pending-tx-details.js +++ /dev/null @@ -1,344 +0,0 @@ -const Component = require('react').Component -const h = require('react-hyperscript') -const inherits = require('util').inherits -const extend = require('xtend') -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN - -const MiniAccountPanel = require('./mini-account-panel') -const EthBalance = require('./eth-balance') -const util = require('../util') -const addressSummary = util.addressSummary -const nameForAddress = require('../../lib/contract-namer') -const HexInput = require('./hex-as-decimal-input') - -module.exports = PendingTxDetails - -inherits(PendingTxDetails, Component) -function PendingTxDetails () { - Component.call(this) -} - -const PTXP = PendingTxDetails.prototype - -PTXP.render = function () { - var props = this.props - var state = this.state || {} - var txData = state.txMeta || props.txData - - var txParams = txData.txParams || {} - var address = txParams.from || props.selectedAddress - var identity = props.identities[address] || { address: address } - var account = props.accounts[address] - var balance = account ? account.balance : '0x0' - - const gas = (state.gas === undefined) ? txParams.gas : state.gas - const gasPrice = (state.gasPrice === undefined) ? txData.gasPrice : state.gasPrice - - var txFee = state.txFee || txData.txFee || '' - var maxCost = state.maxCost || txData.maxCost || '' - var dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 - var imageify = props.imageifyIdenticons === undefined ? true : props.imageifyIdenticons - - log.debug(`rendering gas: ${gas}, gasPrice: ${gasPrice}, txFee: ${txFee}, maxCost: ${maxCost}`) - - return ( - h('div', [ - - h('.flex-row.flex-center', { - style: { - maxWidth: '100%', - }, - }, [ - - h(MiniAccountPanel, { - imageSeed: address, - imageifyIdenticons: imageify, - picOrder: 'right', - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, identity.name), - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(address, 6, 4, false)), - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, [ - h(EthBalance, { - value: balance, - inline: true, - labelColor: '#F7861C', - }), - ]), - - ]), - - forwardCarrat(), - - this.miniAccountPanelForRecipient(), - ]), - - h('style', ` - .table-box { - margin: 7px 0px 0px 0px; - width: 100%; - } - .table-box .row { - margin: 0px; - background: rgb(236,236,236); - display: flex; - justify-content: space-between; - font-family: Montserrat Light, sans-serif; - font-size: 13px; - padding: 5px 25px; - } - .table-box .row .value { - font-family: Montserrat Regular; - } - `), - - h('.table-box', [ - - // Ether Value - // Currently not customizable, but easily modified - // in the way that gas and gasLimit currently are. - h('.row', [ - h('.cell.label', 'Amount'), - h(EthBalance, { value: txParams.value }), - ]), - - // Gas Limit (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Limit'), - h('.cell.value', { - }, [ - h(HexInput, { - value: gas, - suffix: 'UNITS', - style: { - position: 'relative', - top: '5px', - }, - onChange: (newHex) => { - log.info(`Gas limit changed to ${newHex}`) - this.setState({ gas: newHex }) - }, - }), - ]), - ]), - - // Gas Price (customizable) - h('.cell.row', [ - h('.cell.label', 'Gas Price'), - h('.cell.value', { - }, [ - h(HexInput, { - value: gasPrice, - suffix: 'WEI', - style: { - position: 'relative', - top: '5px', - }, - onChange: (newHex) => { - log.info(`Gas price changed to: ${newHex}`) - this.setState({ gasPrice: newHex }) - }, - }), - ]), - ]), - - // Max Transaction Fee (calculated) - h('.cell.row', [ - h('.cell.label', 'Max Transaction Fee'), - h(EthBalance, { value: txFee.toString(16) }), - ]), - - h('.cell.row', { - style: { - fontFamily: 'Montserrat Regular', - background: 'white', - padding: '10px 25px', - }, - }, [ - h('.cell.label', 'Max Total'), - h('.cell.value', { - style: { - display: 'flex', - alignItems: 'center', - }, - }, [ - h(EthBalance, { - value: maxCost.toString(16), - inline: true, - labelColor: 'black', - fontSize: '16px', - }), - ]), - ]), - - // Data size row: - h('.cell.row', { - style: { - background: '#f7f7f7', - paddingBottom: '0px', - }, - }, [ - h('.cell.label'), - h('.cell.value', { - style: { - fontFamily: 'Montserrat Light', - fontSize: '11px', - }, - }, `Data included: ${dataLength} bytes`), - ]), - ]), // End of Table - - ]) - ) -} - -PTXP.miniAccountPanelForRecipient = function () { - var props = this.props - var txData = props.txData - var txParams = txData.txParams || {} - var isContractDeploy = !('to' in txParams) - var imageify = props.imageifyIdenticons === undefined ? true : props.imageifyIdenticons - - // If it's not a contract deploy, send to the account - if (!isContractDeploy) { - return h(MiniAccountPanel, { - imageSeed: txParams.to, - imageifyIdenticons: imageify, - picOrder: 'left', - }, [ - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, nameForAddress(txParams.to, props.identities)), - h('span.font-small', { - style: { - fontFamily: 'Montserrat Light, Montserrat, sans-serif', - }, - }, addressSummary(txParams.to, 6, 4, false)), - ]) - } else { - return h(MiniAccountPanel, { - imageifyIdenticons: imageify, - picOrder: 'left', - }, [ - - h('span.font-small', { - style: { - fontFamily: 'Montserrat Bold, Montserrat, sans-serif', - }, - }, 'New Contract'), - - ]) - } -} - -PTXP.componentDidUpdate = function (prevProps, previousState) { - log.debug(`pending-tx-details componentDidUpdate`) - const state = this.state || {} - const prevState = previousState || {} - const { gas, gasPrice } = state - - // Only if gas or gasPrice changed: - if (!prevState || - (gas !== prevState.gas || - gasPrice !== prevState.gasPrice)) { - log.debug(`recalculating gas since prev state change: ${JSON.stringify({ prevState, state })}`) - this.calculateGas() - } -} - -PTXP.calculateGas = function () { - const txMeta = this.gatherParams() - log.debug(`pending-tx-details calculating gas for ${JSON.stringify(txMeta)}`) - - var txParams = txMeta.txParams - var gasCost = new BN(ethUtil.stripHexPrefix(txParams.gas || txMeta.estimatedGas), 16) - var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice || '0x4a817c800'), 16) - var txFee = gasCost.mul(gasPrice) - var txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16) - var maxCost = txValue.add(txFee) - - const txFeeHex = '0x' + txFee.toString('hex') - const maxCostHex = '0x' + maxCost.toString('hex') - const gasPriceHex = '0x' + gasPrice.toString('hex') - - txMeta.txFee = txFeeHex - txMeta.maxCost = maxCostHex - txMeta.txParams.gasPrice = gasPriceHex - - this.setState({ - txFee: '0x' + txFee.toString('hex'), - maxCost: '0x' + maxCost.toString('hex'), - }) - - if (this.props.onTxChange) { - this.props.onTxChange(txMeta) - } -} - -PTXP.resetGasFields = function () { - log.debug(`pending-tx-details#resetGasFields`) - const txData = this.props.txData - this.setState({ - gas: txData.txParams.gas, - gasPrice: txData.gasPrice, - }) -} - -// After a customizable state value has been updated, -PTXP.gatherParams = function () { - log.debug(`pending-tx-details#gatherParams`) - const props = this.props - const state = this.state || {} - const txData = state.txData || props.txData - const txParams = txData.txParams - const gas = state.gas || txParams.gas - const gasPrice = state.gasPrice || txParams.gasPrice - const resultTx = extend(txParams, { - gas, - gasPrice, - }) - const resultTxMeta = extend(txData, { - txParams: resultTx, - }) - log.debug(`UI has computed tx params ${JSON.stringify(resultTx)}`) - return resultTxMeta -} - -PTXP.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) -} - -PTXP._notZeroOrEmptyString = function (obj) { - return obj !== '' && obj !== '0x0' -} - -function forwardCarrat () { - return ( - - h('img', { - src: 'images/forward-carrat.svg', - style: { - padding: '5px 6px 0px 10px', - height: '37px', - }, - }) - - ) -} diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 2ab6f25a9..5bb088af9 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -2,27 +2,64 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') const inherits = require('util').inherits -const PendingTxDetails = require('./pending-tx-details') const extend = require('xtend') const actions = require('../actions') +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN + +const MiniAccountPanel = require('./mini-account-panel') +const EthBalance = require('./eth-balance') +const util = require('../util') +const addressSummary = util.addressSummary +const nameForAddress = require('../../lib/contract-namer') +const HexInput = require('./hex-as-decimal-input') + +const MIN_GAS_PRICE_BN = new BN(20000000) +const MIN_GAS_LIMIT_BN = new BN(21000) + module.exports = connect(mapStateToProps)(PendingTx) function mapStateToProps (state) { - return { - - } + return {} } inherits(PendingTx, Component) function PendingTx () { Component.call(this) + this.state = { + valid: true, + gas: null, + gasPrice: null, + txData: null, + } } PendingTx.prototype.render = function () { const props = this.props - const newProps = extend(props, {ref: 'details'}) - const txData = props.txData + const state = this.state + + const txData = state.txData || props.txData + const txParams = txData.txParams || {} + + const address = txParams.from || props.selectedAddress + const identity = props.identities[address] || { address: address } + const account = props.accounts[address] + const balance = account ? account.balance : '0x0' + + const gas = state.gas || txParams.gas + const gasPrice = state.gasPrice || txData.gasPrice + const gasBn = new BN(gas, 16) + const gasPriceBn = new BN(gasPrice, 16) + + const txFeeBn = gasBn.mul(gasPriceBn) + const valueBn = new BN(ethUtil.stripHexPrefix(txParams.value), 16) + const maxCost = txFeeBn.add(valueBn) + + const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 + const imageify = props.imageifyIdenticons === undefined ? true : props.imageifyIdenticons + + this.inputs = [] return ( @@ -30,70 +67,409 @@ PendingTx.prototype.render = function () { key: txData.id, }, [ - // tx info - h(PendingTxDetails, newProps), + h('form#pending-tx-form', { + onSubmit: (event) => { + event.preventDefault() + const form = document.querySelector('form#pending-tx-form') + const valid = form.checkValidity() + this.setState({ valid }) - h('style', ` - .conf-buttons button { - margin-left: 10px; - text-transform: uppercase; - } - `), - - txData.simulationFails ? - h('.error', { - style: { - marginLeft: 50, - fontSize: '0.9em', - }, - }, 'Transaction Error. Exception thrown in contract code.') - : null, - - props.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', + if (valid && this.verifyGasParams()) { + props.sendTransaction(txData, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + } }, }, [ - props.insufficientBalance ? - h('button', { - onClick: props.buyEth, - }, 'Buy Ether') + // tx info + h('div', [ + + h('.flex-row.flex-center', { + style: { + maxWidth: '100%', + }, + }, [ + + h(MiniAccountPanel, { + imageSeed: address, + imageifyIdenticons: imageify, + picOrder: 'right', + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, identity.name), + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(address, 6, 4, false)), + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, [ + h(EthBalance, { + value: balance, + inline: true, + labelColor: '#F7861C', + }), + ]), + ]), + + forwardCarrat(), + + this.miniAccountPanelForRecipient(), + ]), + + h('style', ` + .table-box { + margin: 7px 0px 0px 0px; + width: 100%; + } + .table-box .row { + margin: 0px; + background: rgb(236,236,236); + display: flex; + justify-content: space-between; + font-family: Montserrat Light, sans-serif; + font-size: 13px; + padding: 5px 25px; + } + .table-box .row .value { + font-family: Montserrat Regular; + } + `), + + h('.table-box', [ + + // Ether Value + // Currently not customizable, but easily modified + // in the way that gas and gasLimit currently are. + h('.row', [ + h('.cell.label', 'Amount'), + h(EthBalance, { value: txParams.value }), + ]), + + // Gas Limit (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Limit'), + h('.cell.value', { + }, [ + h(HexInput, { + name: 'Gas Limit', + value: gas, + min: MIN_GAS_LIMIT_BN.toString(10), // The hard lower limit for gas. + suffix: 'UNITS', + style: { + position: 'relative', + top: '5px', + }, + onChange: (newHex) => { + log.info(`Gas limit changed to ${newHex}`) + this.setState({ gas: newHex }) + }, + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Gas Price (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Price'), + h('.cell.value', { + }, [ + h(HexInput, { + name: 'Gas Price', + value: gasPrice, + suffix: 'WEI', + min: MIN_GAS_PRICE_BN.toString(10), + style: { + position: 'relative', + top: '5px', + }, + onChange: (newHex) => { + log.info(`Gas price changed to: ${newHex}`) + this.setState({ gasPrice: newHex }) + }, + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Max Transaction Fee (calculated) + h('.cell.row', [ + h('.cell.label', 'Max Transaction Fee'), + h(EthBalance, { value: txFeeBn.toString(16) }), + ]), + + h('.cell.row', { + style: { + fontFamily: 'Montserrat Regular', + background: 'white', + padding: '10px 25px', + }, + }, [ + h('.cell.label', 'Max Total'), + h('.cell.value', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h(EthBalance, { + value: maxCost.toString(16), + inline: true, + labelColor: 'black', + fontSize: '16px', + }), + ]), + ]), + + // Data size row: + h('.cell.row', { + style: { + background: '#f7f7f7', + paddingBottom: '0px', + }, + }, [ + h('.cell.label'), + h('.cell.value', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '11px', + }, + }, `Data included: ${dataLength} bytes`), + ]), + ]), // End of Table + + ]), + + h('style', ` + .conf-buttons button { + margin-left: 10px; + text-transform: uppercase; + } + `), + + txData.simulationFails ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Transaction Error. Exception thrown in contract code.') : null, - h('button', { - onClick: () => { - this.refs.details.resetGasFields() - }, - }, 'Reset'), + props.insufficientBalance ? + h('span.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Insufficient balance for transaction') + : null, - h('button.confirm.btn-green', { - disabled: props.insufficientBalance, - onClick: (txData, event) => { - if (this.refs.details.verifyGasParams()) { - props.sendTransaction(txData, event) - } else { - this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) - } + // send + cancel + h('.flex-row.flex-space-around.conf-buttons', { + style: { + display: 'flex', + justifyContent: 'flex-end', + margin: '14px 25px', }, - }, 'Accept'), + }, [ - h('button.cancel.btn-red', { - onClick: props.cancelTransaction, - }, 'Reject'), + + props.insufficientBalance ? + h('button', { + onClick: props.buyEth, + }, 'Buy Ether') + : null, + + h('button', { + onClick: (event) => { + this.resetGasFields() + event.preventDefault() + }, + }, 'Reset'), + + // Accept Button + h('input.confirm.btn-green', { + type: 'submit', + value: 'ACCEPT', + style: { marginLeft: '10px' }, + disabled: props.insufficientBalance || !this.state.valid, + }), + + h('button.cancel.btn-red', { + onClick: props.cancelTransaction, + }, 'Reject'), + ]), ]), ]) ) } + +PendingTx.prototype.validChanged = function (newValid) { + this.setState({ valid: newValid }) +} + +PendingTx.prototype.miniAccountPanelForRecipient = function () { + const props = this.props + const txData = props.txData + const txParams = txData.txParams || {} + const isContractDeploy = !('to' in txParams) + const imageify = props.imageifyIdenticons === undefined ? true : props.imageifyIdenticons + + // If it's not a contract deploy, send to the account + if (!isContractDeploy) { + return h(MiniAccountPanel, { + imageSeed: txParams.to, + imageifyIdenticons: imageify, + picOrder: 'left', + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, nameForAddress(txParams.to, props.identities)), + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(txParams.to, 6, 4, false)), + ]) + } else { + return h(MiniAccountPanel, { + imageifyIdenticons: imageify, + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, 'New Contract'), + + ]) + } +} + +PendingTx.prototype.componentDidUpdate = function (prevProps, previousState) { + log.debug(`pending-tx componentDidUpdate`) + const state = this.state || {} + const prevState = previousState || {} + const { gas, gasPrice } = state + + // Only if gas or gasPrice changed: + if (!prevState || + (gas !== prevState.gas || + gasPrice !== prevState.gasPrice)) { + log.debug(`recalculating gas since prev state change: ${JSON.stringify({ prevState, state })}`) + this.calculateGas() + } +} + +PendingTx.prototype.calculateGas = function () { + const state = this.state + const props = this.props + const txData = props.txData + + const txMeta = this.gatherParams() + log.debug(`pending-tx calculating gas for ${JSON.stringify(txMeta)}`) + + const txParams = txMeta.txParams + const gasLimit = new BN(ethUtil.stripHexPrefix(txParams.gas || txMeta.estimatedGas), 16) + const gasPriceHex = state.gasPrice || txData.gasPrice + const gasPrice = new BN(ethUtil.stripHexPrefix(gasPriceHex), 16) + + const valid = !gasPrice.lt(MIN_GAS_PRICE_BN) && !gasLimit.lt(MIN_GAS_LIMIT_BN) + this.validChanged(valid) + + const txFee = gasLimit.mul(gasPrice) + const txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16) + const maxCost = txValue.add(txFee) + + const txFeeHex = '0x' + txFee.toString('hex') + const maxCostHex = '0x' + maxCost.toString('hex') + + txMeta.txFee = txFeeHex + txMeta.maxCost = maxCostHex + txMeta.txParams.gasPrice = gasPriceHex + + const newState = { + txFee: '0x' + txFee.toString('hex'), + maxCost: '0x' + maxCost.toString('hex'), + } + log.info(`tx form updating local state with ${JSON.stringify(newState)}`) + this.setState(newState) + + if (this.props.onTxChange) { + this.props.onTxChange(txMeta) + } +} + +PendingTx.prototype.resetGasFields = function () { + log.debug(`pending-tx resetGasFields`) + const txData = this.props.txData + + this.inputs.forEach((hexInput) => { + if (hexInput) { + hexInput.setValid() + } + }) + + this.setState({ + gas: txData.txParams.gas, + gasPrice: txData.gasPrice, + valid: true, + }) +} + +// After a customizable state value has been updated, +PendingTx.prototype.gatherParams = function () { + log.debug(`pending-tx gatherParams`) + const props = this.props + const state = this.state || {} + const txData = state.txData || props.txData + const txParams = txData.txParams + const gas = state.gas || txParams.gas + const gasPrice = state.gasPrice || txParams.gasPrice + const resultTx = extend(txParams, { + gas, + gasPrice, + }) + const resultTxMeta = extend(txData, { + txParams: resultTx, + }) + log.debug(`UI has computed tx params ${JSON.stringify(resultTx)}`) + return resultTxMeta +} + +PendingTx.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) +} + +PendingTx.prototype._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + +function forwardCarrat () { + return ( + + h('img', { + src: 'images/forward-carrat.svg', + style: { + padding: '5px 6px 0px 10px', + height: '37px', + }, + }) + + ) +} + diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 07985094c..54c171a8a 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -43,8 +43,8 @@ ConfirmTxScreen.prototype.render = function () { unapprovedMsgs, unapprovedPersonalMsgs } = props var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) - var index = props.index !== undefined && unconfTxList[index] ? props.index : 0 - var txData = unconfTxList[index] || {} + + var txData = unconfTxList[props.index] || {} var txParams = txData.params || {} var isNotification = isPopupOrNotification() === 'notification' @@ -158,7 +158,7 @@ ConfirmTxScreen.prototype.checkBalanceAgainstTx = function (txData) { } ConfirmTxScreen.prototype.buyEth = function (address, event) { - event.stopPropagation() + this.stopPropagation(event) this.props.dispatch(actions.buyEthView(address)) } @@ -172,14 +172,15 @@ ConfirmTxScreen.prototype.onTxChange = function (txData) { // Must default to any local state txData, // to allow manual override of gas calculations. ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { - event.stopPropagation() + this.stopPropagation(event) const state = this.state || {} const txMeta = state.txData this.props.dispatch(actions.updateAndApproveTx(txMeta || txData)) } ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { - event.stopPropagation() + this.stopPropagation(event) + event.preventDefault() this.props.dispatch(actions.cancelTx(txData)) } @@ -187,32 +188,38 @@ ConfirmTxScreen.prototype.signMessage = function (msgData, event) { log.info('conf-tx.js: signing message') var params = msgData.msgParams params.metamaskId = msgData.id - event.stopPropagation() + this.stopPropagation(event) this.props.dispatch(actions.signMsg(params)) } +ConfirmTxScreen.prototype.stopPropagation = function (event) { + if (event.stopPropagation) { + event.stopPropagation() + } +} + ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { log.info('conf-tx.js: signing personal message') var params = msgData.msgParams params.metamaskId = msgData.id - event.stopPropagation() + this.stopPropagation(event) this.props.dispatch(actions.signPersonalMsg(params)) } ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { log.info('canceling message') - event.stopPropagation() + this.stopPropagation(event) this.props.dispatch(actions.cancelMsg(msgData)) } ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { log.info('canceling personal message') - event.stopPropagation() + this.stopPropagation(event) this.props.dispatch(actions.cancelPersonalMsg(msgData)) } ConfirmTxScreen.prototype.goHome = function (event) { - event.stopPropagation() + this.stopPropagation(event) this.props.dispatch(actions.goHome()) } diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 3ec0ac5c5..de8ae0e92 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -32,7 +32,7 @@ input:focus, textarea:focus { height: 500px; } -button { +button, input[type="submit"] { font-family: 'Montserrat Bold'; outline: none; cursor: pointer; @@ -46,17 +46,17 @@ button { box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); } -button.btn-green { +.btn-green, input[type="submit"].btn-green { background: rgba(106, 195, 96, 1); box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); } -button.btn-red { +.btn-red { background: rgba(254, 35, 17, 1); box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); } -button[disabled] { +button[disabled], input[type="submit"][disabled] { cursor: not-allowed; background: rgba(197, 197, 197, 1); box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); @@ -66,10 +66,10 @@ button.spaced { margin: 2px; } -button:not([disabled]):hover { +button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { transform: scale(1.1); } -button:not([disabled]):active { +button:not([disabled]):active, input[type="submit"]:not([disabled]):active { transform: scale(0.95); } diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index b9e3f7b16..3a6baca91 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -592,8 +592,9 @@ function hasPendingTxs (state) { function indexForPending (state, txId) { var unapprovedTxs = state.metamask.unapprovedTxs var unapprovedMsgs = state.metamask.unapprovedMsgs + var unapprovedPersonalMsgs = state.metamask.unapprovedPersonalMsgs var network = state.metamask.network - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, network) + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) let idx unconfTxList.forEach((tx, i) => { if (tx.id === txId) {