Update Transfer logic + Better logging (#4098)

* Add logs and better Transfer Store logic

* Fix wallet transfer

* Fix wrong gas in Wallet

* Move log levels to Parity tab
This commit is contained in:
Nicolas Gotchac 2017-01-10 13:26:30 +01:00 committed by Jaco Greeff
parent ae7619431b
commit cee2ac43c0
5 changed files with 352 additions and 55 deletions

View File

@ -152,6 +152,7 @@
"isomorphic-fetch": "2.2.1", "isomorphic-fetch": "2.2.1",
"js-sha3": "0.5.5", "js-sha3": "0.5.5",
"lodash": "4.17.2", "lodash": "4.17.2",
"loglevel": "1.4.1",
"marked": "0.3.6", "marked": "0.3.6",
"material-ui": "0.16.5", "material-ui": "0.16.5",
"material-ui-chip-input": "0.11.1", "material-ui-chip-input": "0.11.1",

28
js/src/config.js Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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);
};

View File

@ -25,6 +25,9 @@ import ERRORS from './errors';
import { ERROR_CODES } from '~/api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants'; import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants';
import GasPriceStore from '~/ui/GasPriceEditor/store'; import GasPriceStore from '~/ui/GasPriceEditor/store';
import { getLogger, LOG_KEYS } from '~/config';
const log = getLogger(LOG_KEYS.TransferModalStore);
const TITLES = { const TITLES = {
transfer: 'transfer details', transfer: 'transfer details',
@ -332,13 +335,12 @@ export default class TransferStore {
}); });
} }
@action recalculateGas = () => { @action recalculateGas = (redo = true) => {
if (!this.isValid) { if (!this.isValid) {
this.gasStore.setGas('0'); return this.recalculate(redo);
return this.recalculate();
} }
this return this
.estimateGas() .estimateGas()
.then((gasEst) => { .then((gasEst) => {
let gas = gasEst; let gas = gasEst;
@ -351,76 +353,215 @@ export default class TransferStore {
this.gasStore.setEstimated(gasEst.toFixed(0)); this.gasStore.setEstimated(gasEst.toFixed(0));
this.gasStore.setGas(gas.toFixed(0)); this.gasStore.setGas(gas.toFixed(0));
this.recalculate(); this.recalculate(redo);
}); });
}) })
.catch((error) => { .catch((error) => {
console.warn('etimateGas', error); console.warn('etimateGas', error);
this.recalculate(); this.recalculate(redo);
}); });
} }
@action recalculate = () => { getBalance (forceSender = false) {
const { account } = this; if (this.isWallet && !forceSender) {
return this.balance;
if (!account || !this.balance) {
return;
} }
const balance = this.senders const balance = this.senders
? this.sendersBalances[this.sender] ? this.sendersBalances[this.sender]
: this.balance; : 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) { if (!balance) {
return; return;
} }
const { tag, valueAll, isEth, isWallet } = this;
const gasTotal = new BigNumber(this.gasStore.price || 0).mul(new BigNumber(this.gasStore.gas || 0)); 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 totalEth = gasTotal;
let totalError = null; let totalError = null;
let valueError = null;
if (valueAll) { if (eth.gt(ethBalance)) {
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)) {
totalError = ERRORS.largeAmount; 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(() => { transaction(() => {
this.total = this.api.util.fromWei(totalEth).toFixed();
this.totalError = totalError; this.totalError = totalError;
this.value = value;
this.valueError = valueError; this.valueError = valueError;
this.gasStore.setErrorTotal(totalError); this.gasStore.setErrorTotal(totalError);
this.gasStore.setEthValue(totalEth); 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; options.gas = MAX_GAS_ESTIMATION;
} }
const { token } = this.getValues(options.gas);
if (isEth && !isWallet && !forceToken) { if (isEth && !isWallet && !forceToken) {
options.value = this.api.util.toWei(this.value || 0); options.value = token;
options.data = this._getData(gas); options.data = this._getData(gas);
return { options, values: [] }; return { options, values: [] };
@ -494,7 +637,7 @@ export default class TransferStore {
if (isWallet && !forceToken) { if (isWallet && !forceToken) {
const to = isEth ? this.recipient : this.token.contract.address; 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 = [ const values = [
to, value, to, value,
@ -506,7 +649,7 @@ export default class TransferStore {
const values = [ const values = [
this.recipient, this.recipient,
new BigNumber(this.value || 0).mul(this.token.format).toFixed(0) token.toFixed(0)
]; ];
return { options, values }; return { options, values };

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { SelectField } from 'material-ui'; import { MenuItem, SelectField } from 'material-ui';
import { nodeOrStringProptype } from '~/util/proptypes'; import { nodeOrStringProptype } from '~/util/proptypes';
@ -42,11 +42,12 @@ export default class Select extends Component {
onChange: PropTypes.func, onChange: PropTypes.func,
onKeyDown: PropTypes.func, onKeyDown: PropTypes.func,
type: PropTypes.string, type: PropTypes.string,
value: PropTypes.any value: PropTypes.any,
values: PropTypes.array
} }
render () { 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 ( return (
<SelectField <SelectField
@ -65,9 +66,36 @@ export default class Select extends Component {
onKeyDown={ onKeyDown } onKeyDown={ onKeyDown }
underlineDisabledStyle={ UNDERLINE_DISABLED } underlineDisabledStyle={ UNDERLINE_DISABLED }
underlineStyle={ UNDERLINE_NORMAL } underlineStyle={ UNDERLINE_NORMAL }
value={ value }> value={ value }
{ children } >
{ this.renderChildren() }
</SelectField> </SelectField>
); );
} }
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 (
<MenuItem
key={ index }
label={ name }
value={ value }
>
{ name }
</MenuItem>
);
});
}
} }

View File

@ -17,7 +17,9 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { MenuItem } from 'material-ui'; import { MenuItem } from 'material-ui';
import LogLevel from 'loglevel';
import { LOG_KEYS } from '~/config';
import { Select, Container, LanguageSelector } from '~/ui'; import { Select, Container, LanguageSelector } from '~/ui';
import layout from '../layout.css'; import layout from '../layout.css';
@ -25,14 +27,54 @@ import layout from '../layout.css';
export default class Parity extends Component { export default class Parity extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
} };
state = { state = {
mode: 'active' loglevels: {},
} mode: 'active',
selectValues: []
};
componentWillMount () { componentWillMount () {
this.loadMode(); 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 () { render () {
@ -45,7 +87,8 @@ export default class Parity extends Component {
<div> <div>
<FormattedMessage <FormattedMessage
id='settings.parity.overview_0' id='settings.parity.overview_0'
defaultMessage='Control the Parity node settings and mode of operation via this interface.' /> defaultMessage='Control the Parity node settings and mode of operation via this interface.'
/>
</div> </div>
</div> </div>
<div className={ layout.details }> <div className={ layout.details }>
@ -53,10 +96,64 @@ export default class Parity extends Component {
{ this.renderModes() } { this.renderModes() }
</div> </div>
</div> </div>
{ this.renderLogsConfig() }
</Container> </Container>
); );
} }
renderLogsConfig () {
if (process.env.NODE_ENV === 'production') {
return null;
}
return (
<div className={ layout.layout }>
<div className={ layout.overview }>
<div>
<FormattedMessage
id='settings.parity.loglevels'
defaultMessage='Choose the different logs level.'
/>
</div>
</div>
<div className={ layout.details }>
{ this.renderLogsLevels() }
</div>
</div>
);
}
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 (
<div key={ logKey }>
<p>{ desc }</p>
<Select
onChange={ onChange }
value={ level }
values={ selectValues }
/>
</div>
);
});
}
renderModes () { renderModes () {
const { mode } = this.state; const { mode } = this.state;