GasEditor component (#3750)
* Initial split of component (WIP) * GasPriceEditor externalised * Fix lint
This commit is contained in:
parent
befcc9cc1a
commit
2582514b58
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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 = () => {
|
49
js/src/ui/GasPriceEditor/gasPriceEditor.css
Normal file
49
js/src/ui/GasPriceEditor/gasPriceEditor.css
Normal 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;
|
||||||
|
}
|
98
js/src/ui/GasPriceEditor/gasPriceEditor.js
Normal file
98
js/src/ui/GasPriceEditor/gasPriceEditor.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
17
js/src/ui/GasPriceEditor/index.js
Normal file
17
js/src/ui/GasPriceEditor/index.js
Normal 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';
|
105
js/src/ui/GasPriceEditor/store.js
Normal file
105
js/src/ui/GasPriceEditor/store.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user