Merge pull request #3770 from ethcore/jg-execute-gas

GasPrice selection for contract execution
This commit is contained in:
Gav Wood 2016-12-09 20:24:10 +01:00 committed by GitHub
commit 598fd42856
7 changed files with 188 additions and 70 deletions

View File

@ -15,13 +15,19 @@
// 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 { MenuItem } from 'material-ui'; import { Checkbox, MenuItem } from 'material-ui';
import { AddressSelect, Form, Input, Select, TypedInput } from '~/ui'; import { AddressSelect, Form, Input, Select, TypedInput } from '~/ui';
import { parseAbiType } from '~/util/abi'; import { parseAbiType } from '~/util/abi';
import styles from '../executeContract.css'; import styles from '../executeContract.css';
const CHECK_STYLE = {
position: 'absolute',
top: '38px',
left: '1em'
};
export default class DetailsStep extends Component { export default class DetailsStep extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
@ -31,10 +37,12 @@ export default class DetailsStep extends Component {
onAmountChange: PropTypes.func.isRequired, onAmountChange: PropTypes.func.isRequired,
fromAddress: PropTypes.string, fromAddress: PropTypes.string,
fromAddressError: PropTypes.string, fromAddressError: PropTypes.string,
gasEdit: PropTypes.bool,
onFromAddressChange: PropTypes.func.isRequired, onFromAddressChange: PropTypes.func.isRequired,
func: PropTypes.object, func: PropTypes.object,
funcError: PropTypes.string, funcError: PropTypes.string,
onFuncChange: PropTypes.func, onFuncChange: PropTypes.func,
onGasEditClick: PropTypes.func,
values: PropTypes.array.isRequired, values: PropTypes.array.isRequired,
valuesError: PropTypes.array.isRequired, valuesError: PropTypes.array.isRequired,
warning: PropTypes.string, warning: PropTypes.string,
@ -42,7 +50,7 @@ export default class DetailsStep extends Component {
} }
render () { render () {
const { accounts, amount, amountError, fromAddress, fromAddressError, onFromAddressChange, onAmountChange } = this.props; const { accounts, amount, amountError, fromAddress, fromAddressError, gasEdit, onGasEditClick, onFromAddressChange, onAmountChange } = this.props;
return ( return (
<Form> <Form>
@ -56,12 +64,23 @@ export default class DetailsStep extends Component {
onChange={ onFromAddressChange } /> onChange={ onFromAddressChange } />
{ this.renderFunctionSelect() } { this.renderFunctionSelect() }
{ this.renderParameters() } { this.renderParameters() }
<Input <div className={ styles.columns }>
label='transaction value (in ETH)' <div>
hint='the amount to send to with the transaction' <Input
value={ amount } label='transaction value (in ETH)'
error={ amountError } hint='the amount to send to with the transaction'
onSubmit={ onAmountChange } /> value={ amount }
error={ amountError }
onSubmit={ onAmountChange } />
</div>
<div>
<Checkbox
checked={ gasEdit }
label='edit gas price or value'
onCheck={ onGasEditClick }
style={ CHECK_STYLE } />
</div>
</div>
</Form> </Form>
); );
} }

View File

@ -42,3 +42,15 @@
padding: 0.75em; padding: 0.75em;
text-align: center; text-align: center;
} }
.columns {
display: flex;
flex-wrap: wrap;
position: relative;
&>div {
flex: 0 1 50%;
width: 50%;
position: relative;
}
}

View File

@ -17,19 +17,36 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react';
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 NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '~/ui'; import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Modal, TxHash } from '~/ui';
import { MAX_GAS_ESTIMATION } from '~/util/constants'; import { MAX_GAS_ESTIMATION } from '~/util/constants';
import { validateAddress, validateUint } from '~/util/validation'; import { validateAddress, validateUint } from '~/util/validation';
import { parseAbiType } from '~/util/abi'; import { parseAbiType } from '~/util/abi';
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';
const STEP_DETAILS = 0;
const STEP_BUSY_OR_GAS = 1;
const STEP_BUSY = 2;
const TITLES = {
transfer: 'function details',
sending: 'sending',
complete: 'complete',
gas: 'gas selection',
rejected: 'rejected'
};
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
const STAGES_GAS = [TITLES.transfer, TITLES.gas, TITLES.sending, TITLES.complete];
@observer
class ExecuteContract extends Component { class ExecuteContract extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired, api: PropTypes.object.isRequired,
@ -46,21 +63,22 @@ class ExecuteContract extends Component {
onFromAddressChange: PropTypes.func.isRequired onFromAddressChange: PropTypes.func.isRequired
} }
gasStore = new GasPriceEditor.Store(this.context.api, this.props.gasLimit);
state = { state = {
amount: '0', amount: '0',
amountError: null, amountError: null,
busyState: null,
fromAddressError: null, fromAddressError: null,
func: null, func: null,
funcError: null, funcError: null,
gas: null, gasEdit: false,
gasLimitError: null, rejected: false,
step: STEP_DETAILS,
sending: false,
values: [], values: [],
valuesError: [], valuesError: [],
step: 0, txhash: null
sending: false,
busyState: null,
txhash: null,
rejected: false
} }
componentDidMount () { componentDidMount () {
@ -79,15 +97,21 @@ class ExecuteContract extends Component {
} }
render () { render () {
const { sending } = this.state; const { sending, step, gasEdit, rejected } = this.state;
const steps = gasEdit ? STAGES_GAS : STAGES_BASIC;
if (rejected) {
steps[steps.length - 1] = TITLES.rejected;
}
return ( return (
<Modal <Modal
actions={ this.renderDialogActions() } actions={ this.renderDialogActions() }
title='execute function'
busy={ sending } busy={ sending }
waiting={ [1] } current={ step }
visible> steps={ steps }
visible
waiting={ gasEdit ? [STEP_BUSY] : [STEP_BUSY_OR_GAS] }>
{ this.renderStep() } { this.renderStep() }
</Modal> </Modal>
); );
@ -95,7 +119,7 @@ class ExecuteContract extends Component {
renderDialogActions () { renderDialogActions () {
const { onClose, fromAddress } = this.props; const { onClose, fromAddress } = this.props;
const { sending, step, fromAddressError, valuesError } = this.state; const { gasEdit, sending, step, fromAddressError, valuesError } = this.state;
const hasError = fromAddressError || valuesError.find((error) => error); const hasError = fromAddressError || valuesError.find((error) => error);
const cancelBtn = ( const cancelBtn = (
@ -105,21 +129,44 @@ class ExecuteContract extends Component {
icon={ <ContentClear /> } icon={ <ContentClear /> }
onClick={ onClose } /> onClick={ onClose } />
); );
const postBtn = (
<Button
key='postTransaction'
label='post transaction'
disabled={ !!(sending || hasError) }
icon={ <IdentityIcon address={ fromAddress } button /> }
onClick={ this.postTransaction } />
);
const nextBtn = (
<Button
key='nextStep'
label='next'
icon={ <NavigationArrowForward /> }
onClick={ this.onNextClick } />
);
const prevBtn = (
<Button
key='prevStep'
label='prev'
icon={ <NavigationArrowBack /> }
onClick={ this.onPrevClick } />
);
if (step === 0) { if (step === STEP_DETAILS) {
return [ return [
cancelBtn, cancelBtn,
<Button gasEdit ? nextBtn : postBtn
key='postTransaction'
label='post transaction'
disabled={ !!(sending || hasError) }
icon={ <IdentityIcon address={ fromAddress } button /> }
onClick={ this.postTransaction } />
]; ];
} else if (step === 1) { } else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) {
return [ return [
cancelBtn cancelBtn
]; ];
} else if (gasEdit && (step === STEP_BUSY_OR_GAS)) {
return [
cancelBtn,
prevBtn,
postBtn
];
} }
return [ return [
@ -133,7 +180,8 @@ class ExecuteContract extends Component {
renderStep () { renderStep () {
const { onFromAddressChange } = this.props; const { onFromAddressChange } = this.props;
const { step, busyState, gasLimitError, txhash, rejected } = this.state; const { gasEdit, step, busyState, txhash, rejected } = this.state;
const { errorEstimated } = this.gasStore;
if (rejected) { if (rejected) {
return ( return (
@ -144,23 +192,29 @@ class ExecuteContract extends Component {
); );
} }
if (step === 0) { if (step === STEP_DETAILS) {
return ( return (
<DetailsStep <DetailsStep
{ ...this.props } { ...this.props }
{ ...this.state } { ...this.state }
warning={ gasLimitError } warning={ errorEstimated }
onAmountChange={ this.onAmountChange } onAmountChange={ this.onAmountChange }
onFromAddressChange={ onFromAddressChange } onFromAddressChange={ onFromAddressChange }
onFuncChange={ this.onFuncChange } onFuncChange={ this.onFuncChange }
onGasEditClick={ this.onGasEditClick }
onValueChange={ this.onValueChange } /> onValueChange={ this.onValueChange } />
); );
} else if (step === 1) { } else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) {
return ( return (
<BusyStep <BusyStep
title='The function execution is in progress' title='The function execution is in progress'
state={ busyState } /> state={ busyState } />
); );
} else if (gasEdit && (step === STEP_BUSY_OR_GAS)) {
return (
<GasPriceEditor
store={ this.gasStore } />
);
} }
return ( return (
@ -171,6 +225,7 @@ class ExecuteContract extends Component {
} }
onAmountChange = (amount) => { onAmountChange = (amount) => {
this.gasStore.setEthValue(amount);
this.setState({ amount }, this.estimateGas); this.setState({ amount }, this.estimateGas);
} }
@ -221,7 +276,7 @@ class ExecuteContract extends Component {
estimateGas = (_fromAddress) => { estimateGas = (_fromAddress) => {
const { api } = this.context; const { api } = this.context;
const { fromAddress, gasLimit } = 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, gas: MAX_GAS_ESTIMATION,
@ -237,18 +292,11 @@ class ExecuteContract extends Component {
.estimateGas(options, values) .estimateGas(options, values)
.then((gasEst) => { .then((gasEst) => {
const gas = gasEst.mul(1.2); const gas = gasEst.mul(1.2);
let gasLimitError = null;
if (gas.gte(MAX_GAS_ESTIMATION)) { console.log(`estimateGas: received ${gasEst.toFormat(0)}, adjusted to ${gas.toFormat(0)}`);
gasLimitError = ERRORS.gasException;
} else if (gas.gt(gasLimit)) {
gasLimitError = ERRORS.gasBlockLimit;
}
this.setState({ this.gasStore.setEstimated(gasEst.toFixed(0));
gas, this.gasStore.setGas(gas.toFixed(0));
gasLimitError
});
}) })
.catch((error) => { .catch((error) => {
console.warn('estimateGas', error); console.warn('estimateGas', error);
@ -258,22 +306,20 @@ class ExecuteContract extends Component {
postTransaction = () => { postTransaction = () => {
const { api, store } = this.context; const { api, store } = this.context;
const { fromAddress } = this.props; const { fromAddress } = this.props;
const { amount, func, values } = this.state; const { amount, func, gasEdit, values } = this.state;
const steps = gasEdit ? STAGES_GAS : STAGES_BASIC;
const finalstep = steps.length - 1;
const options = { const options = {
gas: MAX_GAS_ESTIMATION, gas: this.gasStore.gas,
gasPrice: this.gasStore.price,
from: fromAddress, from: fromAddress,
value: api.util.toWei(amount || 0) value: api.util.toWei(amount || 0)
}; };
this.setState({ sending: true, step: 1 }); this.setState({ sending: true, step: gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS });
func func
.estimateGas(options, values) .postTransaction(options, values)
.then((gas) => {
options.gas = gas.mul(1.2).toFixed(0);
console.log(`estimateGas: received ${gas.toFormat(0)}, adjusted to ${gas.mul(1.2).toFormat(0)}`);
return func.postTransaction(options, values);
})
.then((requestId) => { .then((requestId) => {
this.setState({ busyState: 'Waiting for authorization in the Parity Signer' }); this.setState({ busyState: 'Waiting for authorization in the Parity Signer' });
@ -281,7 +327,7 @@ class ExecuteContract extends Component {
.pollMethod('parity_checkRequest', requestId) .pollMethod('parity_checkRequest', requestId)
.catch((error) => { .catch((error) => {
if (error.code === ERROR_CODES.REQUEST_REJECTED) { if (error.code === ERROR_CODES.REQUEST_REJECTED) {
this.setState({ rejected: true }); this.setState({ rejected: true, step: finalstep });
return false; return false;
} }
@ -289,13 +335,31 @@ class ExecuteContract extends Component {
}); });
}) })
.then((txhash) => { .then((txhash) => {
this.setState({ sending: false, step: 2, txhash, busyState: 'Your transaction has been posted to the network' }); this.setState({ sending: false, step: finalstep, txhash, busyState: 'Your transaction has been posted to the network' });
}) })
.catch((error) => { .catch((error) => {
console.error('postTransaction', error); console.error('postTransaction', error);
store.dispatch({ type: 'newError', error }); store.dispatch({ type: 'newError', error });
}); });
} }
onGasEditClick = () => {
this.setState({
gasEdit: !this.state.gasEdit
});
}
onNextClick = () => {
this.setState({
step: this.state.step + 1
});
}
onPrevClick = () => {
this.setState({
step: this.state.step - 1
});
}
} }
function mapStateToProps (state) { function mapStateToProps (state) {

View File

@ -30,21 +30,14 @@ export default class Extras extends Component {
} }
render () { render () {
const { gasStore, onChange, total, totalError } = this.props; const { gasStore, onChange } = this.props;
return ( return (
<Form> <Form>
{ this.renderData() } { this.renderData() }
<GasPriceEditor <GasPriceEditor
store={ gasStore } store={ gasStore }
onChange={ onChange }> onChange={ onChange } />
<Input
disabled
label='total transaction amount'
hint='the total amount of the transaction'
error={ totalError }
value={ `${total} ETH` } />
</GasPriceEditor>
</Form> </Form>
); );
} }

View File

@ -408,6 +408,8 @@ export default class TransferStore {
this.totalError = totalError; this.totalError = totalError;
this.value = value; this.value = value;
this.valueError = valueError; this.valueError = valueError;
this.gasStore.setErrorTotal(totalError);
this.gasStore.setEthValue(totalEth);
}); });
} }

View File

@ -26,8 +26,11 @@ import styles from './gasPriceEditor.css';
@observer @observer
export default class GasPriceEditor extends Component { export default class GasPriceEditor extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = { static propTypes = {
children: PropTypes.node,
store: PropTypes.object.isRequired, store: PropTypes.object.isRequired,
onChange: PropTypes.func onChange: PropTypes.func
} }
@ -35,9 +38,11 @@ export default class GasPriceEditor extends Component {
static Store = Store; static Store = Store;
render () { render () {
const { children, store } = this.props; const { api } = this.context;
const { estimated, priceDefault, price, gas, histogram, errorGas, errorPrice } = store; const { store } = this.props;
const { estimated, priceDefault, price, gas, histogram, errorGas, errorPrice, errorTotal, totalValue } = store;
const eth = api.util.fromWei(totalValue).toFormat();
const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`; const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`;
const priceLabel = `price (current: ${new BigNumber(priceDefault).toFormat()})`; const priceLabel = `price (current: ${new BigNumber(priceDefault).toFormat()})`;
@ -75,7 +80,12 @@ export default class GasPriceEditor extends Component {
</div> </div>
<div className={ styles.row }> <div className={ styles.row }>
{ children } <Input
disabled
label='total transaction amount'
hint='the total amount of the transaction'
error={ errorTotal }
value={ `${eth} ETH` } />
</div> </div>
</div> </div>
</div> </div>

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 BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { action, observable, transaction } from 'mobx'; import { action, computed, observable, transaction } from 'mobx';
import { ERRORS, validatePositiveNumber } from '~/util/validation'; import { ERRORS, validatePositiveNumber } from '~/util/validation';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
@ -24,12 +24,14 @@ export default class GasPriceEditor {
@observable errorEstimated = null; @observable errorEstimated = null;
@observable errorGas = null; @observable errorGas = null;
@observable errorPrice = null; @observable errorPrice = null;
@observable errorTotal = null;
@observable estimated = DEFAULT_GAS; @observable estimated = DEFAULT_GAS;
@observable histogram = null; @observable histogram = null;
@observable price = DEFAULT_GASPRICE; @observable price = DEFAULT_GASPRICE;
@observable priceDefault = DEFAULT_GASPRICE; @observable priceDefault = DEFAULT_GASPRICE;
@observable gas = DEFAULT_GAS; @observable gas = DEFAULT_GAS;
@observable gasLimit = 0; @observable gasLimit = 0;
@observable weiValue = '0';
constructor (api, gasLimit, loadDefaults = true) { constructor (api, gasLimit, loadDefaults = true) {
this._api = api; this._api = api;
@ -40,6 +42,18 @@ export default class GasPriceEditor {
} }
} }
@computed get totalValue () {
try {
return new BigNumber(this.gas).mul(this.price).add(this.weiValue);
} catch (error) {
return new BigNumber(0);
}
}
@action setErrorTotal = (errorTotal) => {
this.errorTotal = errorTotal;
}
@action setEstimated = (estimated) => { @action setEstimated = (estimated) => {
transaction(() => { transaction(() => {
const bn = new BigNumber(estimated); const bn = new BigNumber(estimated);
@ -56,6 +70,10 @@ export default class GasPriceEditor {
}); });
} }
@action setEthValue = (weiValue) => {
this.weiValue = weiValue;
}
@action setHistogram = (gasHistogram) => { @action setHistogram = (gasHistogram) => {
this.histogram = gasHistogram; this.histogram = gasHistogram;
} }