diff --git a/js/package.json b/js/package.json index d09f865d7..24df869f3 100644 --- a/js/package.json +++ b/js/package.json @@ -152,6 +152,7 @@ "isomorphic-fetch": "2.2.1", "js-sha3": "0.5.5", "lodash": "4.17.2", + "loglevel": "1.4.1", "marked": "0.3.6", "material-ui": "0.16.5", "material-ui-chip-input": "0.11.1", diff --git a/js/src/config.js b/js/src/config.js new file mode 100644 index 000000000..87fedecb8 --- /dev/null +++ b/js/src/config.js @@ -0,0 +1,28 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import LogLevel from 'loglevel'; + +export const LOG_KEYS = { + TransferModalStore: { + path: 'modals/Transfer/store', + desc: 'Transfer Modal MobX Store' + } +}; + +export const getLogger = (LOG_KEY) => { + return LogLevel.getLogger(LOG_KEY.path); +}; diff --git a/js/src/modals/Transfer/store.js b/js/src/modals/Transfer/store.js index d2796b098..338e1f255 100644 --- a/js/src/modals/Transfer/store.js +++ b/js/src/modals/Transfer/store.js @@ -25,6 +25,9 @@ import ERRORS from './errors'; import { ERROR_CODES } from '~/api/transport/error'; import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants'; import GasPriceStore from '~/ui/GasPriceEditor/store'; +import { getLogger, LOG_KEYS } from '~/config'; + +const log = getLogger(LOG_KEYS.TransferModalStore); const TITLES = { transfer: 'transfer details', @@ -332,13 +335,12 @@ export default class TransferStore { }); } - @action recalculateGas = () => { + @action recalculateGas = (redo = true) => { if (!this.isValid) { - this.gasStore.setGas('0'); - return this.recalculate(); + return this.recalculate(redo); } - this + return this .estimateGas() .then((gasEst) => { let gas = gasEst; @@ -351,76 +353,215 @@ export default class TransferStore { this.gasStore.setEstimated(gasEst.toFixed(0)); this.gasStore.setGas(gas.toFixed(0)); - this.recalculate(); + this.recalculate(redo); }); }) .catch((error) => { console.warn('etimateGas', error); - this.recalculate(); + this.recalculate(redo); }); } - @action recalculate = () => { - const { account } = this; - - if (!account || !this.balance) { - return; + getBalance (forceSender = false) { + if (this.isWallet && !forceSender) { + return this.balance; } const balance = this.senders ? this.sendersBalances[this.sender] : this.balance; + return balance; + } + + getToken (tag = this.tag, forceSender = false) { + const balance = this.getBalance(forceSender); + + if (!balance) { + return null; + } + + const _tag = tag.toLowerCase(); + const token = balance.tokens.find((b) => b.token.tag.toLowerCase() === _tag); + + return token; + } + + /** + * Return the balance of the selected token + * (in WEI for ETH, without formating for other tokens) + */ + getTokenBalance (tag = this.tag, forceSender = false) { + const token = this.getToken(tag, forceSender); + + if (!token) { + return new BigNumber(0); + } + + const value = new BigNumber(token.value || 0); + + return value; + } + + getTokenValue (tag = this.tag, value = this.value, inverse = false) { + const token = this.getToken(tag); + + if (!token) { + return new BigNumber(0); + } + + const format = token.token + ? new BigNumber(token.token.format || 1) + : new BigNumber(1); + + let _value; + + try { + _value = new BigNumber(value || 0); + } catch (error) { + _value = new BigNumber(0); + } + + if (token.token && token.token.tag.toLowerCase() === 'eth') { + if (inverse) { + return this.api.util.fromWei(_value); + } + + return this.api.util.toWei(_value); + } + + if (inverse) { + return _value.div(format); + } + + return _value.mul(format); + } + + getValues (_gasTotal) { + const gasTotal = new BigNumber(_gasTotal || 0); + const { valueAll, isEth, isWallet } = this; + + if (!valueAll) { + const value = this.getTokenValue(); + + // If it's a token or a wallet, eth is the estimated gas, + // and value is the user input + if (!isEth || isWallet) { + return { + eth: gasTotal, + token: value + }; + } + + // Otherwise, eth is the sum of the gas and the user input + const totalEthValue = gasTotal.plus(value); + + return { + eth: totalEthValue, + token: value + }; + } + + // If it's the total balance that needs to be sent, send the total balance + // if it's not a proper ETH transfer + if (!isEth || isWallet) { + const tokenBalance = this.getTokenBalance(); + + return { + eth: gasTotal, + token: tokenBalance + }; + } + + // Otherwise, substract the gas estimate + const availableEth = this.getTokenBalance('ETH'); + const totalEthValue = availableEth.gt(gasTotal) + ? availableEth.minus(gasTotal) + : new BigNumber(0); + + return { + eth: totalEthValue.plus(gasTotal), + token: totalEthValue + }; + } + + getFormattedTokenValue (tokenValue) { + const token = this.getToken(); + + if (!token) { + return new BigNumber(0); + } + + const tag = token.token && token.token.tag || ''; + + return this.getTokenValue(tag, tokenValue, true); + } + + @action recalculate = (redo = false) => { + const { account } = this; + + if (!account || !this.balance) { + return; + } + + const balance = this.getBalance(); + if (!balance) { return; } - const { tag, valueAll, isEth, isWallet } = this; - const gasTotal = new BigNumber(this.gasStore.price || 0).mul(new BigNumber(this.gasStore.gas || 0)); - const availableEth = new BigNumber(balance.tokens[0].value); + const ethBalance = this.getTokenBalance('ETH', true); + const tokenBalance = this.getTokenBalance(); + const { eth, token } = this.getValues(gasTotal); - const senderBalance = this.balance.tokens.find((b) => tag === b.token.tag); - const format = new BigNumber(senderBalance.token.format || 1); - const available = new BigNumber(senderBalance.value).div(format); - - let { value, valueError } = this; let totalEth = gasTotal; let totalError = null; + let valueError = null; - if (valueAll) { - if (isEth && !isWallet) { - const bn = this.api.util.fromWei(availableEth.minus(gasTotal)); - value = (bn.lt(0) ? new BigNumber(0.0) : bn).toString(); - } else if (isEth) { - value = (available.lt(0) ? new BigNumber(0.0) : available).toString(); - } else { - value = available.toString(); - } - } - - if (isEth && !isWallet) { - totalEth = totalEth.plus(this.api.util.toWei(value || 0)); - } - - if (new BigNumber(value || 0).gt(available)) { - valueError = ERRORS.largeAmount; - } else if (valueError === ERRORS.largeAmount) { - valueError = null; - } - - if (totalEth.gt(availableEth)) { + if (eth.gt(ethBalance)) { totalError = ERRORS.largeAmount; } + if (token && token.gt(tokenBalance)) { + valueError = ERRORS.largeAmount; + } + + log.debug('@recalculate', { + eth: eth.toFormat(), + token: token.toFormat(), + ethBalance: ethBalance.toFormat(), + tokenBalance: tokenBalance.toFormat(), + gasTotal: gasTotal.toFormat() + }); + transaction(() => { - this.total = this.api.util.fromWei(totalEth).toFixed(); this.totalError = totalError; - this.value = value; this.valueError = valueError; this.gasStore.setErrorTotal(totalError); this.gasStore.setEthValue(totalEth); + + this.total = this.api.util.fromWei(eth).toFixed(); + + const nextValue = this.getFormattedTokenValue(token); + let prevValue; + + try { + prevValue = new BigNumber(this.value || 0); + } catch (error) { + prevValue = new BigNumber(0); + } + + // Change the input only if necessary + if (!nextValue.eq(prevValue)) { + this.value = nextValue.toString(); + } + + // Re Calculate gas once more to be sure + if (redo) { + return this.recalculateGas(false); + } }); } @@ -485,8 +626,10 @@ export default class TransferStore { options.gas = MAX_GAS_ESTIMATION; } + const { token } = this.getValues(options.gas); + if (isEth && !isWallet && !forceToken) { - options.value = this.api.util.toWei(this.value || 0); + options.value = token; options.data = this._getData(gas); return { options, values: [] }; @@ -494,7 +637,7 @@ export default class TransferStore { if (isWallet && !forceToken) { const to = isEth ? this.recipient : this.token.contract.address; - const value = isEth ? this.api.util.toWei(this.value || 0) : new BigNumber(0); + const value = isEth ? token : new BigNumber(0); const values = [ to, value, @@ -506,7 +649,7 @@ export default class TransferStore { const values = [ this.recipient, - new BigNumber(this.value || 0).mul(this.token.format).toFixed(0) + token.toFixed(0) ]; return { options, values }; diff --git a/js/src/ui/Form/Select/select.js b/js/src/ui/Form/Select/select.js index f79cae58c..fb0940415 100644 --- a/js/src/ui/Form/Select/select.js +++ b/js/src/ui/Form/Select/select.js @@ -15,7 +15,7 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; -import { SelectField } from 'material-ui'; +import { MenuItem, SelectField } from 'material-ui'; import { nodeOrStringProptype } from '~/util/proptypes'; @@ -42,11 +42,12 @@ export default class Select extends Component { onChange: PropTypes.func, onKeyDown: PropTypes.func, type: PropTypes.string, - value: PropTypes.any + value: PropTypes.any, + values: PropTypes.array } render () { - const { children, className, disabled, error, hint, label, onBlur, onChange, onKeyDown, value } = this.props; + const { className, disabled, error, hint, label, onBlur, onChange, onKeyDown, value } = this.props; return ( - { children } + value={ value } + > + { this.renderChildren() } ); } + + renderChildren () { + const { children, values } = this.props; + + if (children) { + return children; + } + + if (!values) { + return null; + } + + return values.map((data, index) => { + const { name = index, value = index } = data; + + return ( + + { name } + + ); + }); + } } diff --git a/js/src/views/Settings/Parity/parity.js b/js/src/views/Settings/Parity/parity.js index 978ef296f..c52d713cd 100644 --- a/js/src/views/Settings/Parity/parity.js +++ b/js/src/views/Settings/Parity/parity.js @@ -17,7 +17,9 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { MenuItem } from 'material-ui'; +import LogLevel from 'loglevel'; +import { LOG_KEYS } from '~/config'; import { Select, Container, LanguageSelector } from '~/ui'; import layout from '../layout.css'; @@ -25,14 +27,54 @@ import layout from '../layout.css'; export default class Parity extends Component { static contextTypes = { api: PropTypes.object.isRequired - } + }; state = { - mode: 'active' - } + loglevels: {}, + mode: 'active', + selectValues: [] + }; componentWillMount () { this.loadMode(); + this.loadLogLevels(); + this.setSelectValues(); + } + + loadLogLevels () { + if (process.env.NODE_ENV === 'production') { + return null; + } + + const nextState = { ...this.state.logLevels }; + + Object.keys(LOG_KEYS).map((logKey) => { + const log = LOG_KEYS[logKey]; + + const logger = LogLevel.getLogger(log.path); + const level = logger.getLevel(); + + nextState[logKey] = { level, log }; + }); + + this.setState({ logLevels: nextState }); + } + + setSelectValues () { + if (process.env.NODE_ENV === 'production') { + return null; + } + + const selectValues = Object.keys(LogLevel.levels).map((levelName) => { + const value = LogLevel.levels[levelName]; + + return { + name: levelName, + value + }; + }); + + this.setState({ selectValues }); } render () { @@ -45,7 +87,8 @@ export default class Parity extends Component {
+ defaultMessage='Control the Parity node settings and mode of operation via this interface.' + />
@@ -53,10 +96,64 @@ export default class Parity extends Component { { this.renderModes() }
+ + { this.renderLogsConfig() } ); } + renderLogsConfig () { + if (process.env.NODE_ENV === 'production') { + return null; + } + + return ( +
+
+
+ +
+
+
+ { this.renderLogsLevels() } +
+
+ ); + } + + renderLogsLevels () { + if (process.env.NODE_ENV === 'production') { + return null; + } + + const { logLevels, selectValues } = this.state; + + return Object.keys(logLevels).map((logKey) => { + const { level, log } = logLevels[logKey]; + const { path, desc } = log; + + const onChange = (_, index) => { + const nextLevel = Object.values(selectValues)[index].value; + LogLevel.getLogger(path).setLevel(nextLevel); + this.loadLogLevels(); + }; + + return ( +
+

{ desc }

+