diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c6a6f44..aea0df1fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Current Master - Now when switching networks the extension does not restart +- Cleanup decimal bugs in our gas inputs. +- Fix bug where submit button was enabled for invalid gas inputs. ## 3.7.0 2017-5-23 @@ -16,6 +18,8 @@ - Fix bug where edited gas parameters would not take effect. - Trim currency list. +- Enable decimals in our gas prices. +- Fix reset button. - Fix event filter bug introduced by newer versions of Geth. - Fix bug where decimals in gas inputs could result in strange values. diff --git a/test/unit/components/bn-as-decimal-input-test.js b/test/unit/components/bn-as-decimal-input-test.js new file mode 100644 index 000000000..b3365b6f9 --- /dev/null +++ b/test/unit/components/bn-as-decimal-input-test.js @@ -0,0 +1,51 @@ +var assert = require('assert') + +const additions = require('react-testutils-additions') +const h = require('react-hyperscript') +const ReactTestUtils = require('react-addons-test-utils') +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN + +var BnInput = require('../../../ui/app/components/bn-as-decimal-input') + +describe('BnInput', function () { + it('can tolerate a gas decimal number at a high precision', function (done) { + const renderer = ReactTestUtils.createRenderer() + + let valueStr = '20' + while (valueStr.length < 20) { + valueStr += '0' + } + const value = new BN(valueStr, 10) + + let inputStr = '2.3' + + let targetStr = '23' + while (targetStr.length < 19) { + targetStr += '0' + } + const target = new BN(targetStr, 10) + + const precision = 18 // ether precision + const scale = 18 + + const props = { + value, + scale, + precision, + onChange: (newBn) => { + assert.equal(newBn.toString(), target.toString(), 'should tolerate increase') + done() + }, + } + + const inputComponent = h(BnInput, props) + const component = additions.renderIntoDocument(inputComponent) + renderer.render(inputComponent) + const input = additions.find(component, 'input.hex-input')[0] + ReactTestUtils.Simulate.change(input, { preventDefault() {}, target: { + value: inputStr, + checkValidity() { return true } }, + }) + }) +}) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js index 9ff948604..52e5e5910 100644 --- a/test/unit/components/pending-tx-test.js +++ b/test/unit/components/pending-tx-test.js @@ -6,7 +6,6 @@ const ReactTestUtils = require('react-addons-test-utils') const ethUtil = require('ethereumjs-util') describe('PendingTx', function () { - const identities = { '0xfdea65c8e26263f6d9a1b5de9555d2931a33b826': { name: 'Main Account 1', diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/app/components/bn-as-decimal-input.js new file mode 100644 index 000000000..f3ace4720 --- /dev/null +++ b/ui/app/components/bn-as-decimal-input.js @@ -0,0 +1,174 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = BnAsDecimalInput + +inherits(BnAsDecimalInput, Component) +function BnAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Bn as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in bn string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated bn string. + */ + +BnAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, scale, precision, onChange, min, max } = props + + const suffix = props.suffix + const style = props.style + const valueString = value.toString(10) + const newValue = this.downsize(valueString, scale, precision) + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + step: 'any', + required: true, + min, + max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: newValue, + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const value = (event.target.value === '') ? '' : event.target.value + + + const scaledNumber = this.upsize(value, scale, precision) + const precisionBN = new BN(scaledNumber, 10) + onChange(precisionBN, event.target.checkValidity()) + }, + 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, + ]) + ) +} + +BnAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +BnAsDecimalInput.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 }) + } +} + +BnAsDecimalInput.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 +} + + +BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { + // if there is no scaling, simply return the number + if (scale === 0) { + return Number(number) + } else { + // if the scale is the same as the precision, account for this edge case. + var decimals = (scale === precision) ? -1 : scale - precision + return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) + } +} + +BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { + var stringArray = number.toString().split('.') + var decimalLength = stringArray[1] ? stringArray[1].length : 0 + var newString = stringArray[0] + + // If there is scaling and decimal parts exist, integrate them in. + if ((scale !== 0) && (decimalLength !== 0)) { + newString += stringArray[1].slice(0, precision) + } + + // Add 0s to account for the upscaling. + for (var i = decimalLength; i < scale; i++) { + newString += '0' + } + return newString +} diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 0d1f06ba6..d66d98dd5 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -2,6 +2,7 @@ const Component = require('react').Component 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 @@ -12,7 +13,7 @@ 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 BNInput = require('./bn-as-decimal-input') const MIN_GAS_PRICE_GWEI_BN = new BN(2) const GWEI_FACTOR = new BN(1e9) @@ -50,7 +51,6 @@ PendingTx.prototype.render = function () { // Gas Price const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) const gasPriceBn = hexToBn(gasPrice) - const gasPriceGweiBn = gasPriceBn.div(GWEI_FACTOR) const txFeeBn = gasBn.mul(gasPriceBn) const valueBn = hexToBn(txParams.value) @@ -152,9 +152,11 @@ PendingTx.prototype.render = function () { h('.cell.label', 'Gas Limit'), h('.cell.value', { }, [ - h(HexInput, { + h(BNInput, { name: 'Gas Limit', - value: gas, + value: gasBn, + precision: 0, + scale: 0, // The hard lower limit for gas. min: MIN_GAS_LIMIT_BN.toString(10), suffix: 'UNITS', @@ -174,9 +176,11 @@ PendingTx.prototype.render = function () { h('.cell.label', 'Gas Price'), h('.cell.value', { }, [ - h(HexInput, { + h(BNInput, { name: 'Gas Price', - value: gasPriceGweiBn.toString(16), + value: gasPriceBn, + precision: 9, + scale: 9, suffix: 'GWEI', min: MIN_GAS_PRICE_GWEI_BN.toString(10), style: { @@ -342,19 +346,24 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () { } } -PendingTx.prototype.gasPriceChanged = function (newHex) { - log.info(`Gas price changed to: ${newHex}`) - const inWei = hexToBn(newHex).mul(GWEI_FACTOR) +PendingTx.prototype.gasPriceChanged = function (newBN, valid) { + log.info(`Gas price changed to: ${newBN.toString(10)}`) const txMeta = this.gatherTxMeta() - txMeta.txParams.gasPrice = inWei.toString(16) - this.setState({ txData: txMeta }) + txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) } -PendingTx.prototype.gasLimitChanged = function (newHex) { - log.info(`Gas limit changed to ${newHex}`) +PendingTx.prototype.gasLimitChanged = function (newBN, valid) { + log.info(`Gas limit changed to ${newBN.toString(10)}`) const txMeta = this.gatherTxMeta() - txMeta.txParams.gas = newHex - this.setState({ txData: txMeta }) + txMeta.txParams.gas = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) } PendingTx.prototype.resetGasFields = function () { @@ -404,7 +413,7 @@ PendingTx.prototype.gatherTxMeta = function () { log.debug(`pending-tx gatherTxMeta`) const props = this.props const state = this.state - const txData = state.txData || props.txData + const txData = clone(state.txData) || clone(props.txData) log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) return txData @@ -425,7 +434,6 @@ PendingTx.prototype._notZeroOrEmptyString = function (obj) { function forwardCarrat () { return ( - h('img', { src: 'images/forward-carrat.svg', style: { @@ -433,6 +441,5 @@ function forwardCarrat () { height: '37px', }, }) - ) }