GasPrice selection for contract execution
This commit is contained in:
		
							parent
							
								
									1213ada59c
								
							
						
					
					
						commit
						d2494d1425
					
				| @ -15,13 +15,19 @@ | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| 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 { parseAbiType } from '~/util/abi'; | ||||
| 
 | ||||
| import styles from '../executeContract.css'; | ||||
| 
 | ||||
| const CHECK_STYLE = { | ||||
|   position: 'absolute', | ||||
|   top: '38px', | ||||
|   left: '1em' | ||||
| }; | ||||
| 
 | ||||
| export default class DetailsStep extends Component { | ||||
|   static propTypes = { | ||||
|     accounts: PropTypes.object.isRequired, | ||||
| @ -31,10 +37,12 @@ export default class DetailsStep extends Component { | ||||
|     onAmountChange: PropTypes.func.isRequired, | ||||
|     fromAddress: PropTypes.string, | ||||
|     fromAddressError: PropTypes.string, | ||||
|     gasEdit: PropTypes.bool, | ||||
|     onFromAddressChange: PropTypes.func.isRequired, | ||||
|     func: PropTypes.object, | ||||
|     funcError: PropTypes.string, | ||||
|     onFuncChange: PropTypes.func, | ||||
|     onGasEditClick: PropTypes.func, | ||||
|     values: PropTypes.array.isRequired, | ||||
|     valuesError: PropTypes.array.isRequired, | ||||
|     warning: PropTypes.string, | ||||
| @ -42,7 +50,7 @@ export default class DetailsStep extends Component { | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { accounts, amount, amountError, fromAddress, fromAddressError, onFromAddressChange, onAmountChange } = this.props; | ||||
|     const { accounts, amount, amountError, fromAddress, fromAddressError, gasEdit, onGasEditClick, onFromAddressChange, onAmountChange } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <Form> | ||||
| @ -56,12 +64,23 @@ export default class DetailsStep extends Component { | ||||
|           onChange={ onFromAddressChange } /> | ||||
|         { this.renderFunctionSelect() } | ||||
|         { this.renderParameters() } | ||||
|         <Input | ||||
|           label='transaction value (in ETH)' | ||||
|           hint='the amount to send to with the transaction' | ||||
|           value={ amount } | ||||
|           error={ amountError } | ||||
|           onSubmit={ onAmountChange } /> | ||||
|         <div className={ styles.columns }> | ||||
|           <div> | ||||
|             <Input | ||||
|               label='transaction value (in ETH)' | ||||
|               hint='the amount to send to with the transaction' | ||||
|               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> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -42,3 +42,15 @@ | ||||
|   padding: 0.75em; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .columns { | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   position: relative; | ||||
| 
 | ||||
|   &>div { | ||||
|     flex: 0 1 50%; | ||||
|     width: 50%; | ||||
|     position: relative; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -17,19 +17,36 @@ | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import { bindActionCreators } from 'redux'; | ||||
| import { observer } from 'mobx-react'; | ||||
| import ActionDoneAll from 'material-ui/svg-icons/action/done-all'; | ||||
| 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 { validateAddress, validateUint } from '~/util/validation'; | ||||
| import { parseAbiType } from '~/util/abi'; | ||||
| 
 | ||||
| import DetailsStep from './DetailsStep'; | ||||
| 
 | ||||
| import ERRORS from '../Transfer/errors'; | ||||
| 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 { | ||||
|   static contextTypes = { | ||||
|     api: PropTypes.object.isRequired, | ||||
| @ -46,21 +63,22 @@ class ExecuteContract extends Component { | ||||
|     onFromAddressChange: PropTypes.func.isRequired | ||||
|   } | ||||
| 
 | ||||
|   gasStore = new GasPriceEditor.Store(this.context.api, this.props.gasLimit); | ||||
| 
 | ||||
|   state = { | ||||
|     amount: '0', | ||||
|     amountError: null, | ||||
|     busyState: null, | ||||
|     fromAddressError: null, | ||||
|     func: null, | ||||
|     funcError: null, | ||||
|     gas: null, | ||||
|     gasLimitError: null, | ||||
|     gasEdit: false, | ||||
|     rejected: false, | ||||
|     step: STEP_DETAILS, | ||||
|     sending: false, | ||||
|     values: [], | ||||
|     valuesError: [], | ||||
|     step: 0, | ||||
|     sending: false, | ||||
|     busyState: null, | ||||
|     txhash: null, | ||||
|     rejected: false | ||||
|     txhash: null | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount () { | ||||
| @ -79,15 +97,21 @@ class ExecuteContract extends Component { | ||||
|   } | ||||
| 
 | ||||
|   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 ( | ||||
|       <Modal | ||||
|         actions={ this.renderDialogActions() } | ||||
|         title='execute function' | ||||
|         busy={ sending } | ||||
|         waiting={ [1] } | ||||
|         visible> | ||||
|         current={ step } | ||||
|         steps={ steps } | ||||
|         visible | ||||
|         waiting={ gasEdit ? [STEP_BUSY] : [STEP_BUSY_OR_GAS] }> | ||||
|         { this.renderStep() } | ||||
|       </Modal> | ||||
|     ); | ||||
| @ -95,7 +119,7 @@ class ExecuteContract extends Component { | ||||
| 
 | ||||
|   renderDialogActions () { | ||||
|     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 cancelBtn = ( | ||||
| @ -105,21 +129,44 @@ class ExecuteContract extends Component { | ||||
|         icon={ <ContentClear /> } | ||||
|         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 [ | ||||
|         cancelBtn, | ||||
|         <Button | ||||
|           key='postTransaction' | ||||
|           label='post transaction' | ||||
|           disabled={ !!(sending || hasError) } | ||||
|           icon={ <IdentityIcon address={ fromAddress } button /> } | ||||
|           onClick={ this.postTransaction } /> | ||||
|         gasEdit ? nextBtn : postBtn | ||||
|       ]; | ||||
|     } else if (step === 1) { | ||||
|     } else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) { | ||||
|       return [ | ||||
|         cancelBtn | ||||
|       ]; | ||||
|     } else if (gasEdit && (step === STEP_BUSY_OR_GAS)) { | ||||
|       return [ | ||||
|         cancelBtn, | ||||
|         prevBtn, | ||||
|         postBtn | ||||
|       ]; | ||||
|     } | ||||
| 
 | ||||
|     return [ | ||||
| @ -133,7 +180,8 @@ class ExecuteContract extends Component { | ||||
| 
 | ||||
|   renderStep () { | ||||
|     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) { | ||||
|       return ( | ||||
| @ -144,23 +192,29 @@ class ExecuteContract extends Component { | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (step === 0) { | ||||
|     if (step === STEP_DETAILS) { | ||||
|       return ( | ||||
|         <DetailsStep | ||||
|           { ...this.props } | ||||
|           { ...this.state } | ||||
|           warning={ gasLimitError } | ||||
|           warning={ errorEstimated } | ||||
|           onAmountChange={ this.onAmountChange } | ||||
|           onFromAddressChange={ onFromAddressChange } | ||||
|           onFuncChange={ this.onFuncChange } | ||||
|           onGasEditClick={ this.onGasEditClick } | ||||
|           onValueChange={ this.onValueChange } /> | ||||
|       ); | ||||
|     } else if (step === 1) { | ||||
|     } else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) { | ||||
|       return ( | ||||
|         <BusyStep | ||||
|           title='The function execution is in progress' | ||||
|           state={ busyState } /> | ||||
|       ); | ||||
|     } else if (gasEdit && (step === STEP_BUSY_OR_GAS)) { | ||||
|       return ( | ||||
|         <GasPriceEditor | ||||
|           store={ this.gasStore } /> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
| @ -171,6 +225,7 @@ class ExecuteContract extends Component { | ||||
|   } | ||||
| 
 | ||||
|   onAmountChange = (amount) => { | ||||
|     this.gasStore.setEthValue(amount); | ||||
|     this.setState({ amount }, this.estimateGas); | ||||
|   } | ||||
| 
 | ||||
| @ -221,7 +276,7 @@ class ExecuteContract extends Component { | ||||
| 
 | ||||
|   estimateGas = (_fromAddress) => { | ||||
|     const { api } = this.context; | ||||
|     const { fromAddress, gasLimit } = this.props; | ||||
|     const { fromAddress } = this.props; | ||||
|     const { amount, func, values } = this.state; | ||||
|     const options = { | ||||
|       gas: MAX_GAS_ESTIMATION, | ||||
| @ -237,18 +292,11 @@ class ExecuteContract extends Component { | ||||
|       .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; | ||||
|         } | ||||
|         console.log(`estimateGas: received ${gasEst.toFormat(0)}, adjusted to ${gas.toFormat(0)}`); | ||||
| 
 | ||||
|         this.setState({ | ||||
|           gas, | ||||
|           gasLimitError | ||||
|         }); | ||||
|         this.gasStore.setEstimated(gasEst.toFixed(0)); | ||||
|         this.gasStore.setGas(gas.toFixed(0)); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.warn('estimateGas', error); | ||||
| @ -258,22 +306,18 @@ class ExecuteContract extends Component { | ||||
|   postTransaction = () => { | ||||
|     const { api, store } = this.context; | ||||
|     const { fromAddress } = this.props; | ||||
|     const { amount, func, values } = this.state; | ||||
|     const { amount, func, gasEdit, values } = this.state; | ||||
|     const options = { | ||||
|       gas: MAX_GAS_ESTIMATION, | ||||
|       gas: this.gasStore.gas, | ||||
|       gasPrice: this.gasStore.price, | ||||
|       from: fromAddress, | ||||
|       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 | ||||
|       .estimateGas(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); | ||||
|       }) | ||||
|       .postTransaction(options, values) | ||||
|       .then((requestId) => { | ||||
|         this.setState({ busyState: 'Waiting for authorization in the Parity Signer' }); | ||||
| 
 | ||||
| @ -296,6 +340,24 @@ class ExecuteContract extends Component { | ||||
|         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) { | ||||
|  | ||||
| @ -30,21 +30,14 @@ export default class Extras extends Component { | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { gasStore, onChange, total, totalError } = this.props; | ||||
|     const { gasStore, onChange } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <Form> | ||||
|         { this.renderData() } | ||||
|         <GasPriceEditor | ||||
|           store={ gasStore } | ||||
|           onChange={ onChange }> | ||||
|           <Input | ||||
|             disabled | ||||
|             label='total transaction amount' | ||||
|             hint='the total amount of the transaction' | ||||
|             error={ totalError } | ||||
|             value={ `${total} ETH` } /> | ||||
|         </GasPriceEditor> | ||||
|           onChange={ onChange } /> | ||||
|       </Form> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -408,6 +408,8 @@ export default class TransferStore { | ||||
|       this.totalError = totalError; | ||||
|       this.value = value; | ||||
|       this.valueError = valueError; | ||||
|       this.gasStore.setErrorTotal(totalError); | ||||
|       this.gasStore.setEthValue(totalEth); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -26,8 +26,11 @@ import styles from './gasPriceEditor.css'; | ||||
| 
 | ||||
| @observer | ||||
| export default class GasPriceEditor extends Component { | ||||
|   static contextTypes = { | ||||
|     api: PropTypes.object.isRequired | ||||
|   }; | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     children: PropTypes.node, | ||||
|     store: PropTypes.object.isRequired, | ||||
|     onChange: PropTypes.func | ||||
|   } | ||||
| @ -35,9 +38,11 @@ export default class GasPriceEditor extends Component { | ||||
|   static Store = Store; | ||||
| 
 | ||||
|   render () { | ||||
|     const { children, store } = this.props; | ||||
|     const { estimated, priceDefault, price, gas, histogram, errorGas, errorPrice } = store; | ||||
|     const { api } = this.context; | ||||
|     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 priceLabel = `price (current: ${new BigNumber(priceDefault).toFormat()})`; | ||||
| 
 | ||||
| @ -75,7 +80,12 @@ export default class GasPriceEditor extends Component { | ||||
|           </div> | ||||
| 
 | ||||
|           <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> | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
| // along with Parity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| 
 | ||||
| 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 { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; | ||||
| @ -24,12 +24,14 @@ export default class GasPriceEditor { | ||||
|   @observable errorEstimated = null; | ||||
|   @observable errorGas = null; | ||||
|   @observable errorPrice = null; | ||||
|   @observable errorTotal = null; | ||||
|   @observable estimated = DEFAULT_GAS; | ||||
|   @observable histogram = null; | ||||
|   @observable price = DEFAULT_GASPRICE; | ||||
|   @observable priceDefault = DEFAULT_GASPRICE; | ||||
|   @observable gas = DEFAULT_GAS; | ||||
|   @observable gasLimit = 0; | ||||
|   @observable weiValue = '0'; | ||||
| 
 | ||||
|   constructor (api, gasLimit, loadDefaults = true) { | ||||
|     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) => { | ||||
|     transaction(() => { | ||||
|       const bn = new BigNumber(estimated); | ||||
| @ -56,6 +70,10 @@ export default class GasPriceEditor { | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @action setEthValue = (weiValue) => { | ||||
|     this.weiValue = weiValue; | ||||
|   } | ||||
| 
 | ||||
|   @action setHistogram = (gasHistogram) => { | ||||
|     this.histogram = gasHistogram; | ||||
|   } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user