Merge pull request #3512 from ethcore/jg-transact-gas-warnings

Send value & contract execute gas limit warnings
This commit is contained in:
Gav Wood 2016-11-22 17:04:13 +01:00 committed by GitHub
commit a722bac304
7 changed files with 193 additions and 14 deletions

View File

@ -36,6 +36,7 @@ export default class DetailsStep extends Component {
onFuncChange: PropTypes.func, onFuncChange: PropTypes.func,
values: PropTypes.array.isRequired, values: PropTypes.array.isRequired,
valuesError: PropTypes.array.isRequired, valuesError: PropTypes.array.isRequired,
warning: PropTypes.string,
onValueChange: PropTypes.func.isRequired onValueChange: PropTypes.func.isRequired
} }
@ -44,6 +45,7 @@ export default class DetailsStep extends Component {
return ( return (
<Form> <Form>
{ this.renderWarning() }
<AddressSelect <AddressSelect
label='from account' label='from account'
hint='the account to transact with' hint='the account to transact with'
@ -178,6 +180,20 @@ export default class DetailsStep extends Component {
}); });
} }
renderWarning () {
const { warning } = this.props;
if (!warning) {
return null;
}
return (
<div className={ styles.warning }>
{ warning }
</div>
);
}
onFuncChange = (event, index, signature) => { onFuncChange = (event, index, signature) => {
const { contract, onFuncChange } = this.props; const { contract, onFuncChange } = this.props;

View File

@ -33,3 +33,12 @@
.txhash { .txhash {
word-break: break-all; word-break: break-all;
} }
.warning {
border-radius: 0.5em;
background: #f80;
color: white;
font-size: 0.75em;
padding: 0.75em;
text-align: center;
}

View File

@ -15,17 +15,21 @@
// 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 { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '../../ui'; import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '../../ui';
import { MAX_GAS_ESTIMATION } from '../../util/constants';
import { validateAddress, validateUint } from '../../util/validation'; import { validateAddress, validateUint } from '../../util/validation';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import ERRORS from '../Transfer/errors';
import { ERROR_CODES } from '../../api/transport/error'; import { ERROR_CODES } from '../../api/transport/error';
export default class ExecuteContract extends Component { class ExecuteContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
store: PropTypes.object.isRequired store: PropTypes.object.isRequired
@ -36,6 +40,7 @@ export default class ExecuteContract extends Component {
fromAddress: PropTypes.string, fromAddress: PropTypes.string,
accounts: PropTypes.object, accounts: PropTypes.object,
contract: PropTypes.object, contract: PropTypes.object,
gasLimit: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onFromAddressChange: PropTypes.func.isRequired onFromAddressChange: PropTypes.func.isRequired
} }
@ -46,6 +51,8 @@ export default class ExecuteContract extends Component {
fromAddressError: null, fromAddressError: null,
func: null, func: null,
funcError: null, funcError: null,
gas: null,
gasLimitError: null,
values: [], values: [],
valuesError: [], valuesError: [],
step: 0, step: 0,
@ -64,6 +71,12 @@ export default class ExecuteContract extends Component {
this.onFuncChange(null, functions[0]); this.onFuncChange(null, functions[0]);
} }
componentWillReceiveProps (newProps) {
if (newProps.fromAddress !== this.props.fromAddress) {
this.estimateGas(newProps.fromAddress);
}
}
render () { render () {
const { sending } = this.state; const { sending } = this.state;
@ -119,7 +132,7 @@ export default class ExecuteContract extends Component {
renderStep () { renderStep () {
const { onFromAddressChange } = this.props; const { onFromAddressChange } = this.props;
const { step, busyState, txhash, rejected } = this.state; const { step, busyState, gasLimitError, txhash, rejected } = this.state;
if (rejected) { if (rejected) {
return ( return (
@ -135,6 +148,7 @@ export default class ExecuteContract extends Component {
<DetailsStep <DetailsStep
{ ...this.props } { ...this.props }
{ ...this.state } { ...this.state }
warning={ gasLimitError }
onAmountChange={ this.onAmountChange } onAmountChange={ this.onAmountChange }
onFromAddressChange={ onFromAddressChange } onFromAddressChange={ onFromAddressChange }
onFuncChange={ this.onFuncChange } onFuncChange={ this.onFuncChange }
@ -156,7 +170,7 @@ export default class ExecuteContract extends Component {
} }
onAmountChange = (amount) => { onAmountChange = (amount) => {
this.setState({ amount }); this.setState({ amount }, this.estimateGas);
} }
onFuncChange = (event, func) => { onFuncChange = (event, func) => {
@ -182,7 +196,7 @@ export default class ExecuteContract extends Component {
this.setState({ this.setState({
func, func,
values values
}); }, this.estimateGas);
} }
onValueChange = (event, index, _value) => { onValueChange = (event, index, _value) => {
@ -211,6 +225,46 @@ export default class ExecuteContract extends Component {
this.setState({ this.setState({
values: [].concat(values), values: [].concat(values),
valuesError: [].concat(valuesError) valuesError: [].concat(valuesError)
}, () => {
if (!valueError) {
this.estimateGas();
}
});
}
estimateGas = (_fromAddress) => {
const { api } = this.context;
const { fromAddress, gasLimit } = this.props;
const { amount, func, values } = this.state;
const options = {
gas: MAX_GAS_ESTIMATION,
from: _fromAddress || fromAddress,
value: api.util.toWei(amount || 0)
};
if (!func) {
return;
}
func
.estimateGas(options, values)
.then((gasEst) => {
const gas = gasEst.mul(1.2);
let gasLimitError = null;
if (gas.gte(MAX_GAS_ESTIMATION)) {
gasLimitError = ERRORS.gasException;
} else if (gas.gt(gasLimit)) {
gasLimitError = ERRORS.gasBlockLimit;
}
this.setState({
gas,
gasLimitError
});
})
.catch((error) => {
console.warn('estimateGas', error);
}); });
} }
@ -219,6 +273,7 @@ export default class ExecuteContract extends Component {
const { fromAddress } = this.props; const { fromAddress } = this.props;
const { amount, func, values } = this.state; const { amount, func, values } = this.state;
const options = { const options = {
gas: MAX_GAS_ESTIMATION,
from: fromAddress, from: fromAddress,
value: api.util.toWei(amount || 0) value: api.util.toWei(amount || 0)
}; };
@ -237,13 +292,13 @@ export default class ExecuteContract extends Component {
return api return api
.pollMethod('parity_checkRequest', requestId) .pollMethod('parity_checkRequest', requestId)
.catch((e) => { .catch((error) => {
if (e.code === ERROR_CODES.REQUEST_REJECTED) { if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.setState({ rejected: true }); this.setState({ rejected: true });
return false; return false;
} }
throw e; throw error;
}); });
}) })
.then((txhash) => { .then((txhash) => {
@ -255,3 +310,18 @@ export default class ExecuteContract extends Component {
}); });
} }
} }
function mapStateToProps (state) {
const { gasLimit } = state.nodeStatus;
return { gasLimit };
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ExecuteContract);

View File

@ -19,7 +19,9 @@ const ERRORS = {
invalidAddress: 'the supplied address is an invalid network address', invalidAddress: 'the supplied address is an invalid network address',
invalidAmount: 'the supplied amount should be a valid positive number', invalidAmount: 'the supplied amount should be a valid positive number',
invalidDecimals: 'the supplied amount exceeds the allowed decimals', invalidDecimals: 'the supplied amount exceeds the allowed decimals',
largeAmount: 'the transaction total is higher than the available balance' largeAmount: 'the transaction total is higher than the available balance',
gasException: 'the transaction will throw an exception with the current values',
gasBlockLimit: 'the transaction execution will exceed the block gas limit'
}; };
export default ERRORS; export default ERRORS;

View File

@ -14,6 +14,7 @@
/* You should have received a copy of the GNU General Public License /* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.info { .info {
line-height: 1.618em; line-height: 1.618em;
width: 100%; width: 100%;
@ -151,3 +152,13 @@
.gasPriceDesc { .gasPriceDesc {
font-size: 0.9em; font-size: 0.9em;
} }
.warning {
border-radius: 0.5em;
background: #f80;
color: white;
font-size: 0.75em;
margin-bottom: 1em;
padding: 0.75em;
text-align: center;
}

View File

@ -16,12 +16,15 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '../../ui'; import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '../../ui';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '../../util/constants';
import Details from './Details'; import Details from './Details';
import Extras from './Extras'; import Extras from './Extras';
@ -30,8 +33,6 @@ import styles from './transfer.css';
import { ERROR_CODES } from '../../api/transport/error'; import { ERROR_CODES } from '../../api/transport/error';
const DEFAULT_GAS = '21000';
const DEFAULT_GASPRICE = '20000000000';
const TITLES = { const TITLES = {
transfer: 'transfer details', transfer: 'transfer details',
sending: 'sending', sending: 'sending',
@ -42,7 +43,7 @@ const TITLES = {
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete]; const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.complete]; const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.complete];
export default class Transfer extends Component { class Transfer extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
store: PropTypes.object.isRequired store: PropTypes.object.isRequired
@ -52,6 +53,7 @@ export default class Transfer extends Component {
account: PropTypes.object, account: PropTypes.object,
balance: PropTypes.object, balance: PropTypes.object,
balances: PropTypes.object, balances: PropTypes.object,
gasLimit: PropTypes.object.isRequired,
images: PropTypes.object.isRequired, images: PropTypes.object.isRequired,
onClose: PropTypes.func onClose: PropTypes.func
} }
@ -64,6 +66,7 @@ export default class Transfer extends Component {
gas: DEFAULT_GAS, gas: DEFAULT_GAS,
gasEst: '0', gasEst: '0',
gasError: null, gasError: null,
gasLimitError: null,
gasPrice: DEFAULT_GASPRICE, gasPrice: DEFAULT_GASPRICE,
gasPriceHistogram: {}, gasPriceHistogram: {},
gasPriceError: null, gasPriceError: null,
@ -103,6 +106,7 @@ export default class Transfer extends Component {
visible visible
scroll scroll
> >
{ this.renderWarning() }
{ this.renderPage() } { this.renderPage() }
</Modal> </Modal>
); );
@ -264,6 +268,20 @@ export default class Transfer extends Component {
} }
} }
renderWarning () {
const { gasLimitError } = this.state;
if (!gasLimitError) {
return null;
}
return (
<div className={ styles.warning }>
{ gasLimitError }
</div>
);
}
isValid () { isValid () {
const detailsValid = !this.state.recipientError && !this.state.valueError && !this.state.totalError; const detailsValid = !this.state.recipientError && !this.state.valueError && !this.state.totalError;
const extrasValid = !this.state.gasError && !this.state.gasPriceError && !this.state.totalError; const extrasValid = !this.state.gasError && !this.state.gasPriceError && !this.state.totalError;
@ -519,6 +537,7 @@ export default class Transfer extends Component {
return token.contract.instance.transfer return token.contract.instance.transfer
.estimateGas({ .estimateGas({
gas: MAX_GAS_ESTIMATION,
from: account.address, from: account.address,
to: token.address to: token.address
}, [ }, [
@ -532,6 +551,7 @@ export default class Transfer extends Component {
const { account } = this.props; const { account } = this.props;
const { data, recipient, value } = this.state; const { data, recipient, value } = this.state;
const options = { const options = {
gas: MAX_GAS_ESTIMATION,
from: account.address, from: account.address,
to: recipient, to: recipient,
value: api.util.toWei(value || 0) value: api.util.toWei(value || 0)
@ -552,19 +572,29 @@ export default class Transfer extends Component {
return; return;
} }
const { gasLimit } = this.props;
(this.state.isEth (this.state.isEth
? this._estimateGasEth() ? this._estimateGasEth()
: this._estimateGasToken() : this._estimateGasToken()
).then((_value) => { ).then((gasEst) => {
let gas = _value; let gas = gasEst;
let gasLimitError = null;
if (gas.gt(DEFAULT_GAS)) { if (gas.gt(DEFAULT_GAS)) {
gas = gas.mul(1.2); gas = gas.mul(1.2);
} }
if (gas.gte(MAX_GAS_ESTIMATION)) {
gasLimitError = ERRORS.gasException;
} else if (gas.gt(gasLimit)) {
gasLimitError = ERRORS.gasBlockLimit;
}
this.setState({ this.setState({
gas: gas.toFixed(0), gas: gas.toFixed(0),
gasEst: _value.toFormat() gasEst: gasEst.toFormat(),
gasLimitError
}, this.recalculate); }, this.recalculate);
}) })
.catch((error) => { .catch((error) => {
@ -649,3 +679,18 @@ export default class Transfer extends Component {
store.dispatch({ type: 'newError', error }); store.dispatch({ type: 'newError', error });
} }
} }
function mapStateToProps (state) {
const { gasLimit } = state.nodeStatus;
return { gasLimit };
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({}, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Transfer);

26
js/src/util/constants.js Normal file
View File

@ -0,0 +1,26 @@
// Copyright 2015, 2016 Ethcore (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/>.
const DEFAULT_GAS = '21000';
const DEFAULT_GASPRICE = '20000000000';
const MAX_GAS_ESTIMATION = '50000000';
export {
DEFAULT_GAS,
DEFAULT_GASPRICE,
MAX_GAS_ESTIMATION
};