GasEditor component (#3750)

* Initial split of component (WIP)

* GasPriceEditor externalised

* Fix lint
This commit is contained in:
Jaco Greeff 2016-12-09 13:44:35 +01:00 committed by GitHub
parent befcc9cc1a
commit 2582514b58
13 changed files with 440 additions and 277 deletions

View File

@ -16,96 +16,35 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import Form, { Input } from '~/ui/Form'; import { GasPriceEditor, Form, Input } from '~/ui';
import GasPriceSelector from '../GasPriceSelector';
import styles from '../transfer.css';
export default class Extras extends Component { export default class Extras extends Component {
static propTypes = { static propTypes = {
isEth: PropTypes.bool, isEth: PropTypes.bool,
data: PropTypes.string, data: PropTypes.string,
dataError: PropTypes.string, dataError: PropTypes.string,
gas: PropTypes.string,
gasEst: PropTypes.string,
gasError: PropTypes.string,
gasPrice: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
]),
gasPriceDefault: PropTypes.string,
gasPriceError: PropTypes.string,
gasPriceHistogram: PropTypes.object,
total: PropTypes.string, total: PropTypes.string,
totalError: PropTypes.string, totalError: PropTypes.string,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired,
gasStore: PropTypes.object.isRequired
} }
render () { render () {
const { gas, gasPrice, gasError, gasEst, gasPriceDefault, gasPriceError, gasPriceHistogram, total, totalError } = this.props; const { gasStore, onChange, total, totalError } = this.props;
const gasLabel = `gas amount (estimated: ${gasEst})`;
const priceLabel = `gas price (current: ${gasPriceDefault})`;
return ( return (
<Form> <Form>
{ this.renderData() } { this.renderData() }
<GasPriceEditor
<div className={ styles.columns }> store={ gasStore }
<div style={ { flex: 65 } }> onChange={ onChange }>
<GasPriceSelector <Input
gasPriceHistogram={ gasPriceHistogram } disabled
gasPrice={ gasPrice } label='total transaction amount'
onChange={ this.onEditGasPrice } hint='the total amount of the transaction'
/> error={ totalError }
</div> value={ `${total} ETH` } />
</GasPriceEditor>
<div
className={ styles.row }
style={ {
flex: 35, paddingLeft: '1rem',
justifyContent: 'space-around',
paddingBottom: 12
} }
>
<div className={ styles.row }>
<Input
label={ gasLabel }
hint='the amount of gas to use for the transaction'
error={ gasError }
value={ gas }
onChange={ this.onEditGas } />
<Input
label={ priceLabel }
hint='the price of gas to use for the transaction'
error={ gasPriceError }
value={ (gasPrice || '').toString() }
onChange={ this.onEditGasPrice } />
</div>
<div className={ styles.row }>
<Input
disabled
label='total transaction amount'
hint='the total amount of the transaction'
error={ totalError }
value={ `${total} ETH` } />
</div>
</div>
</div>
<div>
<p className={ styles.gasPriceDesc }>
You can choose the gas price based on the
distribution of recent included transactions' gas prices.
The lower the gas price is, the cheaper the transaction will
be. The higher the gas price is, the faster it should
get mined by the network.
</p>
</div>
</Form> </Form>
); );
} }
@ -129,14 +68,6 @@ export default class Extras extends Component {
); );
} }
onEditGas = (event) => {
this.props.onChange('gas', event.target.value);
}
onEditGasPrice = (event, value) => {
this.props.onChange('gasPrice', value);
}
onEditData = (event) => { onEditData = (event) => {
this.props.onChange('data', event.target.value); this.props.onChange('data', event.target.value);
} }

View File

@ -23,7 +23,8 @@ import { bytesToHex } from '~/api/util/format';
import Contract from '~/api/contract'; import Contract from '~/api/contract';
import ERRORS from './errors'; import ERRORS from './errors';
import { ERROR_CODES } from '~/api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants';
import GasPriceStore from '~/ui/GasPriceEditor/store';
const TITLES = { const TITLES = {
transfer: 'transfer details', transfer: 'transfer details',
@ -48,14 +49,6 @@ export default class TransferStore {
@observable data = ''; @observable data = '';
@observable dataError = null; @observable dataError = null;
@observable gas = DEFAULT_GAS;
@observable gasError = null;
@observable gasEst = '0';
@observable gasLimitError = null;
@observable gasPrice = DEFAULT_GASPRICE;
@observable gasPriceError = null;
@observable recipient = ''; @observable recipient = '';
@observable recipientError = ERRORS.requireRecipient; @observable recipientError = ERRORS.requireRecipient;
@ -68,11 +61,8 @@ export default class TransferStore {
@observable value = '0.0'; @observable value = '0.0';
@observable valueError = null; @observable valueError = null;
gasPriceHistogram = {};
account = null; account = null;
balance = null; balance = null;
gasLimit = null;
onClose = null; onClose = null;
senders = null; senders = null;
@ -81,6 +71,8 @@ export default class TransferStore {
isWallet = false; isWallet = false;
wallet = null; wallet = null;
gasStore = null;
@computed get steps () { @computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC); const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
@ -93,7 +85,7 @@ export default class TransferStore {
@computed get isValid () { @computed get isValid () {
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError; const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
const extrasValid = !this.gasError && !this.gasPriceError && !this.totalError; const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.totalError;
const verifyValid = !this.passwordError; const verifyValid = !this.passwordError;
switch (this.stage) { switch (this.stage) {
@ -118,11 +110,12 @@ export default class TransferStore {
const { account, balance, gasLimit, senders, onClose, newError, sendersBalances } = props; const { account, balance, gasLimit, senders, onClose, newError, sendersBalances } = props;
this.account = account; this.account = account;
this.balance = balance; this.balance = balance;
this.gasLimit = gasLimit;
this.onClose = onClose; this.onClose = onClose;
this.isWallet = account && account.wallet; this.isWallet = account && account.wallet;
this.newError = newError; this.newError = newError;
this.gasStore = new GasPriceStore(api, gasLimit);
if (this.isWallet) { if (this.isWallet) {
this.wallet = props.wallet; this.wallet = props.wallet;
this.walletContract = new Contract(this.api, walletAbi); this.walletContract = new Contract(this.api, walletAbi);
@ -179,26 +172,6 @@ export default class TransferStore {
} }
} }
@action getDefaults = () => {
Promise
.all([
this.api.parity.gasPriceHistogram(),
this.api.eth.gasPrice()
])
.then(([gasPriceHistogram, gasPrice]) => {
transaction(() => {
this.gasPrice = gasPrice.toString();
this.gasPriceDefault = gasPrice.toFormat();
this.gasPriceHistogram = gasPriceHistogram;
this.recalculate();
});
})
.catch((error) => {
console.warn('getDefaults', error);
});
}
@action onSend = () => { @action onSend = () => {
this.onNext(); this.onNext();
this.sending = true; this.sending = true;
@ -281,25 +254,11 @@ export default class TransferStore {
} }
@action _onUpdateGas = (gas) => { @action _onUpdateGas = (gas) => {
const gasError = this._validatePositiveNumber(gas); this.recalculate();
transaction(() => {
this.gas = gas;
this.gasError = gasError;
this.recalculate();
});
} }
@action _onUpdateGasPrice = (gasPrice) => { @action _onUpdateGasPrice = (gasPrice) => {
const gasPriceError = this._validatePositiveNumber(gasPrice); this.recalculate();
transaction(() => {
this.gasPrice = gasPrice;
this.gasPriceError = gasPriceError;
this.recalculate();
});
} }
@action _onUpdateRecipient = (recipient) => { @action _onUpdateRecipient = (recipient) => {
@ -362,7 +321,7 @@ export default class TransferStore {
@action recalculateGas = () => { @action recalculateGas = () => {
if (!this.isValid) { if (!this.isValid) {
this.gas = 0; this.gasStore.setGas('0');
return this.recalculate(); return this.recalculate();
} }
@ -370,28 +329,20 @@ export default class TransferStore {
.estimateGas() .estimateGas()
.then((gasEst) => { .then((gasEst) => {
let gas = gasEst; 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(this.gasLimit)) {
gasLimitError = ERRORS.gasBlockLimit;
}
transaction(() => { transaction(() => {
this.gas = gas.toFixed(0); this.gasStore.setEstimated(gasEst.toFixed(0));
this.gasEst = gasEst.toFormat(); this.gasStore.setGas(gas.toFixed(0));
this.gasLimitError = gasLimitError;
this.recalculate(); this.recalculate();
}); });
}) })
.catch((error) => { .catch((error) => {
console.error('etimateGas', error); console.warn('etimateGas', error);
this.recalculate(); this.recalculate();
}); });
} }
@ -411,9 +362,9 @@ export default class TransferStore {
return; return;
} }
const { gas, gasPrice, tag, valueAll, isEth, isWallet } = this; const { tag, valueAll, isEth, isWallet } = this;
const gasTotal = new BigNumber(gasPrice || 0).mul(new BigNumber(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 availableEth = new BigNumber(balance.tokens[0].value);
@ -453,7 +404,7 @@ export default class TransferStore {
} }
transaction(() => { transaction(() => {
this.total = this.api.util.fromWei(totalEth).toString(); this.total = this.api.util.fromWei(totalEth).toFixed();
this.totalError = totalError; this.totalError = totalError;
this.value = value; this.value = value;
this.valueError = valueError; this.valueError = valueError;
@ -522,8 +473,8 @@ export default class TransferStore {
}; };
if (!gas) { if (!gas) {
options.gas = this.gas; options.gas = this.gasStore.gas;
options.gasPrice = this.gasPrice; options.gasPrice = this.gasStore.price;
} else { } else {
options.gas = MAX_GAS_ESTIMATION; options.gas = MAX_GAS_ESTIMATION;
} }

View File

@ -144,15 +144,6 @@
font-size: 1.2rem; font-size: 1.2rem;
} }
.chart {
position: absolute;
width: 100%;
}
.gasPriceDesc {
font-size: 0.9em;
}
.warning { .warning {
border-radius: 0.5em; border-radius: 0.5em;
background: #f80; background: #f80;

View File

@ -56,10 +56,6 @@ class Transfer extends Component {
store = new TransferStore(this.context.api, this.props); store = new TransferStore(this.context.api, this.props);
componentDidMount () {
this.store.getDefaults();
}
render () { render () {
const { stage, extras, steps } = this.store; const { stage, extras, steps } = this.store;
@ -186,27 +182,20 @@ class Transfer extends Component {
} }
renderExtrasPage () { renderExtrasPage () {
if (!this.store.gasPriceHistogram) { if (!this.store.gasStore.histogram) {
return null; return null;
} }
const { isEth, data, dataError, gas, gasEst, gasError, gasPrice } = this.store; const { isEth, data, dataError, total, totalError } = this.store;
const { gasPriceDefault, gasPriceError, gasPriceHistogram, total, totalError } = this.store;
return ( return (
<Extras <Extras
isEth={ isEth } isEth={ isEth }
data={ data } data={ data }
dataError={ dataError } dataError={ dataError }
gas={ gas }
gasEst={ gasEst }
gasError={ gasError }
gasPrice={ gasPrice }
gasPriceDefault={ gasPriceDefault }
gasPriceError={ gasPriceError }
gasPriceHistogram={ gasPriceHistogram }
total={ total } total={ total }
totalError={ totalError } totalError={ totalError }
gasStore={ this.store.gasStore }
onChange={ this.store.onUpdateDetails } /> onChange={ this.store.onUpdateDetails } />
); );
} }
@ -263,15 +252,15 @@ class Transfer extends Component {
} }
renderWarning () { renderWarning () {
const { gasLimitError } = this.store; const { errorEstimated } = this.store.gasStore;
if (!gasLimitError) { if (!errorEstimated) {
return null; return null;
} }
return ( return (
<div className={ styles.warning }> <div className={ styles.warning }>
{ gasLimitError } { errorEstimated }
</div> </div>
); );
} }

View File

@ -15,3 +15,13 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.chart {
position: absolute;
width: 100%;
}
.columns {
display: flex;
flex-wrap: wrap;
position: relative;
}

View File

@ -29,10 +29,7 @@ import {
import Slider from 'material-ui/Slider'; import Slider from 'material-ui/Slider';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import componentStyles from './gasPriceSelector.css'; import styles from './gasPriceSelector.css';
import mainStyles from '../transfer.css';
const styles = Object.assign({}, mainStyles, componentStyles);
const COLORS = { const COLORS = {
default: 'rgba(255, 99, 132, 0.2)', default: 'rgba(255, 99, 132, 0.2)',
@ -194,10 +191,7 @@ class CustomizedShape extends Component {
class CustomTooltip extends Component { class CustomTooltip extends Component {
static propTypes = { static propTypes = {
gasPriceHistogram: PropTypes.shape({ gasPriceHistogram: PropTypes.object.isRequired,
bucketBounds: PropTypes.array.isRequired,
counts: PropTypes.array.isRequired
}).isRequired,
type: PropTypes.string, type: PropTypes.string,
payload: PropTypes.array, payload: PropTypes.array,
label: PropTypes.number, label: PropTypes.number,
@ -231,12 +225,16 @@ class CustomTooltip extends Component {
} }
} }
const TOOL_STYLE = {
color: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(0, 0, 0, 0.75)',
padding: '0 0.5em',
fontSize: '0.75em'
};
export default class GasPriceSelector extends Component { export default class GasPriceSelector extends Component {
static propTypes = { static propTypes = {
gasPriceHistogram: PropTypes.shape({ gasPriceHistogram: PropTypes.object.isRequired,
bucketBounds: PropTypes.array.isRequired,
counts: PropTypes.array.isRequired
}).isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
gasPrice: PropTypes.oneOfType([ gasPrice: PropTypes.oneOfType([
@ -287,21 +285,23 @@ export default class GasPriceSelector extends Component {
renderSlider () { renderSlider () {
const { sliderValue } = this.state; const { sliderValue } = this.state;
return (<div className={ styles.columns }> return (
<Slider <div className={ styles.columns }>
min={ 0 } <Slider
max={ 1 } min={ 0 }
value={ sliderValue } max={ 1 }
onChange={ this.onEditGasPriceSlider } value={ sliderValue }
style={ { onChange={ this.onEditGasPriceSlider }
flex: 1, style={ {
padding: '0 0.3em' flex: 1,
} } padding: '0 0.3em'
sliderStyle={ { } }
marginBottom: 12 sliderStyle={ {
} } marginBottom: 12
/> } }
</div>); />
</div>
);
} }
renderChart () { renderChart () {
@ -316,85 +316,83 @@ export default class GasPriceSelector extends Component {
const countIndex = Math.max(0, Math.min(selectedIndex, gasPriceHistogram.counts.length - 1)); const countIndex = Math.max(0, Math.min(selectedIndex, gasPriceHistogram.counts.length - 1));
const selectedCount = countModifier(gasPriceHistogram.counts[countIndex]); const selectedCount = countModifier(gasPriceHistogram.counts[countIndex]);
return (<div className={ styles.columns }> return (
<div style={ { flex: 1, height } }> <div className={ styles.columns }>
<div className={ styles.chart }> <div style={ { flex: 1, height } }>
<ResponsiveContainer <div className={ styles.chart }>
height={ height } <ResponsiveContainer
> height={ height }
<ScatterChart
margin={ { top: 0, right: 0, left: 0, bottom: 0 } }
> >
<Scatter <ScatterChart
data={ [ margin={ { top: 0, right: 0, left: 0, bottom: 0 } }
{ x: sliderValue, y: 0 }, >
{ x: sliderValue, y: selectedCount }, <Scatter
{ x: sliderValue, y: chartData.yDomain[1] } data={ [
] } { x: sliderValue, y: 0 },
shape={ <CustomizedShape showValue={ selectedCount } /> } { x: sliderValue, y: selectedCount },
line { x: sliderValue, y: chartData.yDomain[1] }
isAnimationActive={ false } ] }
/> shape={ <CustomizedShape showValue={ selectedCount } /> }
line
isAnimationActive={ false }
/>
<XAxis <XAxis
hide hide
height={ 0 } height={ 0 }
dataKey='x' dataKey='x'
domain={ [0, 1] } domain={ [0, 1] }
/> />
<YAxis <YAxis
hide hide
width={ 0 } width={ 0 }
dataKey='y' dataKey='y'
domain={ chartData.yDomain } domain={ chartData.yDomain }
/> />
</ScatterChart> </ScatterChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
<div className={ styles.chart }> <div className={ styles.chart }>
<ResponsiveContainer <ResponsiveContainer
height={ height } height={ height }
>
<BarChart
data={ chartData.values }
margin={ { top: 0, right: 0, left: 0, bottom: 0 } }
barCategoryGap={ 1 }
ref='barChart'
> >
<Bar <BarChart
dataKey='value' data={ chartData.values }
stroke={ COLORS.line } margin={ { top: 0, right: 0, left: 0, bottom: 0 } }
onClick={ this.onClickGasPrice } barCategoryGap={ 1 }
shape={ <CustomBar selected={ selectedIndex } onClick={ this.onClickGasPrice } /> } ref='barChart'
/> >
<Bar
dataKey='value'
stroke={ COLORS.line }
onClick={ this.onClickGasPrice }
shape={ <CustomBar selected={ selectedIndex } onClick={ this.onClickGasPrice } /> }
/>
<Tooltip <Tooltip
wrapperStyle={ { wrapperStyle={ TOOL_STYLE }
backgroundColor: 'rgba(0, 0, 0, 0.75)', cursor={ this.renderCustomCursor() }
padding: '0 0.5em', content={ <CustomTooltip gasPriceHistogram={ gasPriceHistogram } /> }
fontSize: '0.9em' />
} }
cursor={ this.renderCustomCursor() }
content={ <CustomTooltip gasPriceHistogram={ gasPriceHistogram } /> }
/>
<XAxis <XAxis
hide hide
dataKey='index' dataKey='index'
type='category' type='category'
domain={ chartData.xDomain } domain={ chartData.xDomain }
/> />
<YAxis <YAxis
hide hide
type='number' type='number'
domain={ chartData.yDomain } domain={ chartData.yDomain }
/> />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</div>
</div> </div>
</div> </div>
</div>); );
} }
renderCustomCursor = () => { renderCustomCursor = () => {

View File

@ -0,0 +1,49 @@
/* 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/>.
*/
.columns {
display: flex;
flex-wrap: wrap;
position: relative;
}
.graphColumn {
flex: 65;
}
.editColumn {
flex: 35;
padding-left: 1em;
justify-ontent: space-around;
padding-bottom: 12;
display: flex;
flex-wrap: wrap;
position: relative;
flex-direction: column;
}
.gasPriceDesc {
font-size: 0.75em;
opacity: 0.5;
}
.row {
display: flex;
flex-wrap: wrap;
position: relative;
flex-direction: column;
}

View File

@ -0,0 +1,98 @@
// 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/>.
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { observer } from 'mobx-react';
import Input from '../Form/Input';
import GasPriceSelector from './GasPriceSelector';
import Store from './store';
import styles from './gasPriceEditor.css';
@observer
export default class GasPriceEditor extends Component {
static propTypes = {
children: PropTypes.node,
store: PropTypes.object.isRequired,
onChange: PropTypes.func
}
static Store = Store;
render () {
const { children, store } = this.props;
const { estimated, priceDefault, price, gas, histogram, errorGas, errorPrice } = store;
const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`;
const priceLabel = `price (current: ${new BigNumber(priceDefault).toFormat()})`;
return (
<div className={ styles.columns }>
<div className={ styles.graphColumn }>
<GasPriceSelector
gasPriceHistogram={ histogram }
gasPrice={ price }
onChange={ this.onEditGasPrice } />
<div className={ styles.gasPriceDesc }>
You can choose the gas price based on the
distribution of recent included transaction gas prices.
The lower the gas price is, the cheaper the transaction will
be. The higher the gas price is, the faster it should
get mined by the network.
</div>
</div>
<div className={ styles.editColumn }>
<div className={ styles.row }>
<Input
label={ gasLabel }
hint='the amount of gas to use for the transaction'
error={ errorGas }
value={ gas }
onChange={ this.onEditGas } />
<Input
label={ priceLabel }
hint='the price of gas to use for the transaction'
error={ errorPrice }
value={ price }
onChange={ this.onEditGasPrice } />
</div>
<div className={ styles.row }>
{ children }
</div>
</div>
</div>
);
}
onEditGas = (event, gas) => {
const { store, onChange } = this.props;
store.setGas(gas);
onChange && onChange('gas', gas);
}
onEditGasPrice = (event, price) => {
const { store, onChange } = this.props;
store.setPrice(price);
onChange && onChange('gasPrice', price);
}
}

View File

@ -0,0 +1,17 @@
// 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/>.
export default from './gasPriceEditor';

View File

@ -0,0 +1,105 @@
// 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/>.
import BigNumber from 'bignumber.js';
import { action, observable, transaction } from 'mobx';
import { ERRORS, validatePositiveNumber } from '~/util/validation';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
export default class GasPriceEditor {
@observable errorEstimated = null;
@observable errorGas = null;
@observable errorPrice = null;
@observable estimated = DEFAULT_GAS;
@observable histogram = null;
@observable price = DEFAULT_GASPRICE;
@observable priceDefault = DEFAULT_GASPRICE;
@observable gas = DEFAULT_GAS;
@observable gasLimit = 0;
constructor (api, gasLimit, loadDefaults = true) {
this._api = api;
this.gasLimit = gasLimit;
if (loadDefaults) {
this.loadDefaults();
}
}
@action setEstimated = (estimated) => {
transaction(() => {
const bn = new BigNumber(estimated);
this.estimated = estimated;
if (bn.gte(MAX_GAS_ESTIMATION)) {
this.errorEstimated = ERRORS.gasException;
} else if (bn.gte(this.gasLimit)) {
this.errorEstimated = ERRORS.gasBlockLimit;
} else {
this.errorEstimated = null;
}
});
}
@action setHistogram = (gasHistogram) => {
this.histogram = gasHistogram;
}
@action setPrice = (price) => {
transaction(() => {
this.errorPrice = validatePositiveNumber(price).numberError;
this.price = price;
});
}
@action setGas = (gas) => {
transaction(() => {
const { numberError } = validatePositiveNumber(gas);
const bn = new BigNumber(gas);
this.gas = gas;
if (numberError) {
this.errorGas = numberError;
} else if (bn.gte(this.gasLimit)) {
this.errorGas = ERRORS.gasBlockLimit;
} else {
this.errorGas = null;
}
});
}
@action loadDefaults () {
Promise
.all([
this._api.parity.gasPriceHistogram(),
this._api.eth.gasPrice()
])
.then(([gasPriceHistogram, gasPrice]) => {
transaction(() => {
this.setPrice(gasPrice.toFixed(0));
this.setHistogram(gasPriceHistogram);
this.priceDefault = gasPrice.toFixed();
});
})
.catch((error) => {
console.warn('getDefaults', error);
});
}
}

View File

@ -31,6 +31,7 @@ import CopyToClipboard from './CopyToClipboard';
import Editor from './Editor'; import Editor from './Editor';
import Errors from './Errors'; import Errors from './Errors';
import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form'; import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form';
import GasPriceEditor from './GasPriceEditor';
import IdentityIcon from './IdentityIcon'; import IdentityIcon from './IdentityIcon';
import IdentityName from './IdentityName'; import IdentityName from './IdentityName';
import Loading from './Loading'; import Loading from './Loading';
@ -67,7 +68,7 @@ export {
Errors, Errors,
Form, Form,
FormWrap, FormWrap,
TypedInput, GasPriceEditor,
Input, Input,
InputAddress, InputAddress,
InputAddressSelect, InputAddressSelect,
@ -91,5 +92,6 @@ export {
Tooltip, Tooltip,
Tooltips, Tooltips,
TxHash, TxHash,
TxList TxList,
TypedInput
}; };

View File

@ -20,6 +20,7 @@ import util from '~/api/util';
export const ERRORS = { export const ERRORS = {
invalidAddress: 'address is an invalid network address', invalidAddress: 'address is an invalid network address',
invalidAmount: 'the supplied amount should be a valid positive number',
duplicateAddress: 'the address is already in your address book', duplicateAddress: 'the address is already in your address book',
invalidChecksum: 'address has failed the checksum formatting', invalidChecksum: 'address has failed the checksum formatting',
invalidName: 'name should not be blank and longer than 2', invalidName: 'name should not be blank and longer than 2',
@ -27,7 +28,9 @@ export const ERRORS = {
invalidCode: 'code should be the compiled hex string', invalidCode: 'code should be the compiled hex string',
invalidNumber: 'invalid number format', invalidNumber: 'invalid number format',
negativeNumber: 'input number should be positive', negativeNumber: 'input number should be positive',
decimalNumber: 'input number should not contain decimals' decimalNumber: 'input number should not contain decimals',
gasException: 'the transaction will throw an exception with the current values',
gasBlockLimit: 'the transaction execution will exceed the block gas limit'
}; };
export function validateAbi (abi, api) { export function validateAbi (abi, api) {
@ -133,6 +136,25 @@ export function validateName (name) {
}; };
} }
export function validatePositiveNumber (number) {
let numberError = null;
try {
const v = new BigNumber(number);
if (v.lt(0)) {
numberError = ERRORS.invalidAmount;
}
} catch (e) {
numberError = ERRORS.invalidAmount;
}
return {
number,
numberError
};
}
export function validateUint (value) { export function validateUint (value) {
let valueError = null; let valueError = null;