diff options
Diffstat (limited to 'ui')
-rw-r--r-- | ui/app/components/hex-as-decimal-input.js | 113 | ||||
-rw-r--r-- | ui/app/components/pending-tx-details.js | 30 | ||||
-rw-r--r-- | ui/app/components/pending-tx.js | 133 | ||||
-rw-r--r-- | ui/app/conf-tx.js | 22 | ||||
-rw-r--r-- | ui/app/css/index.css | 12 |
5 files changed, 205 insertions, 105 deletions
diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/app/components/hex-as-decimal-input.js index c89ed0416..bbd86b254 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,101 @@ 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('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', }, - }), - h('div', { + }, [ + 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), + onChange: (event) => { + const target = event.target + const valid = target.checkValidity() + if (valid) { + this.setState({ invalid: null }) + } + 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() + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { style: { - color: ' #AEAEAE', - fontSize: '12px', - marginLeft: '5px', - marginRight: '6px', - width: '20px', + 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', }, - }, suffix), + }, state.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 index e92ce575f..375a50f22 100644 --- a/ui/app/components/pending-tx-details.js +++ b/ui/app/components/pending-tx-details.js @@ -12,11 +12,17 @@ const addressSummary = util.addressSummary const nameForAddress = require('../../lib/contract-namer') const HexInput = require('./hex-as-decimal-input') +const DEFAULT_GAS_PRICE_BN = new BN(20000000000, '10') +const DEFAULT_GAS_PRICE = DEFAULT_GAS_PRICE_BN.toString(16) + +const MIN_GAS_PRICE_BN = new BN(20000000) + module.exports = PendingTxDetails inherits(PendingTxDetails, Component) function PendingTxDetails () { Component.call(this) + this.state = { valid: true } } const PTXP = PendingTxDetails.prototype @@ -36,6 +42,7 @@ PTXP.render = function () { const gasPrice = (state.gasPrice === undefined) ? txData.gasPrice : state.gasPrice var txFee = state.txFee || txData.txFee || '' + var txFeeBn = new BN(txFee, 16) var maxCost = state.maxCost || txData.maxCost || '' var dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 var imageify = props.imageifyIdenticons === undefined ? true : props.imageifyIdenticons @@ -78,7 +85,6 @@ PTXP.render = function () { labelColor: '#F7861C', }), ]), - ]), forwardCarrat(), @@ -121,7 +127,9 @@ PTXP.render = function () { h('.cell.value', { }, [ h(HexInput, { + name: 'Gas Limit', value: gas, + min: 21000, // The hard lower limit for gas. suffix: 'UNITS', style: { position: 'relative', @@ -141,8 +149,10 @@ PTXP.render = function () { h('.cell.value', { }, [ h(HexInput, { + name: 'Gas Price', value: gasPrice, suffix: 'WEI', + min: MIN_GAS_PRICE_BN.toString(10), style: { position: 'relative', top: '5px', @@ -158,7 +168,7 @@ PTXP.render = function () { // Max Transaction Fee (calculated) h('.cell.row', [ h('.cell.label', 'Max Transaction Fee'), - h(EthBalance, { value: txFee.toString(16) }), + h(EthBalance, { value: txFeeBn.toString(16) }), ]), h('.cell.row', { @@ -261,13 +271,21 @@ PTXP.componentDidUpdate = function (prevProps, previousState) { } } +PTXP.isValid = function () { + return this.state.valid +} + 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 gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice || DEFAULT_GAS_PRICE), 16) + + const valid = !gasPrice.lt(MIN_GAS_PRICE_BN) + this.props.validChanged(valid) + var txFee = gasCost.mul(gasPrice) var txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16) var maxCost = txValue.add(txFee) @@ -280,10 +298,12 @@ PTXP.calculateGas = function () { txMeta.maxCost = maxCostHex txMeta.txParams.gasPrice = gasPriceHex - this.setState({ + 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) diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 2ab6f25a9..f6fb6f85e 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -17,11 +17,15 @@ function mapStateToProps (state) { inherits(PendingTx, Component) function PendingTx () { Component.call(this) + this.state = { valid: true } } PendingTx.prototype.render = function () { const props = this.props - const newProps = extend(props, {ref: 'details'}) + const newProps = extend(props, { + ref: 'details', + validChanged: this.validChanged.bind(this), + }) const txData = props.txData return ( @@ -30,70 +34,87 @@ PendingTx.prototype.render = function () { key: txData.id, }, [ - // tx info - h(PendingTxDetails, newProps), - - 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', + h('form#pending-tx-form', { + onSubmit: (event) => { + event.preventDefault() + const form = document.querySelector('form#pending-tx-form') + const valid = form.checkValidity() + this.setState({ valid }) + + if (valid && this.refs.details.verifyGasParams()) { + props.sendTransaction(txData, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + } }, }, [ + // tx info + h(PendingTxDetails, newProps), + + 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('button', { - onClick: props.buyEth, - }, 'Buy Ether') + h('span.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Insufficient balance for transaction') : null, - h('button', { - onClick: () => { - this.refs.details.resetGasFields() - }, - }, 'Reset'), - - 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: () => { + this.refs.details.resetGasFields() + }, + }, 'Reset'), + + 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 }) +} diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 07985094c..ce7422f9c 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -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,14 @@ 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) this.props.dispatch(actions.cancelTx(txData)) } @@ -187,32 +187,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); } |