Graphical gas price selection (#2898)
* Added gasPriceStatistics * WIP graph fas price stats (#2142) * Chart to select gas price in Extra Tx (#2142) * Gas Selection UI * Gas Price Selection: better UI (right octiles, point on graph) (#2142) * Gas Price Selection chart update using D3 (#2142) * Working UI, more fluid... (#2142) * Using the new gasPriceHistogram Call: display histogram (#2142) * Code Clean * Updated gas Selection explaination * PR grumble // Gas Price Selector (#2898) * Fixing linting issues
This commit is contained in:
parent
3b6c969398
commit
84ca3d7a7d
@ -113,6 +113,7 @@
|
|||||||
"bignumber.js": "^2.3.0",
|
"bignumber.js": "^2.3.0",
|
||||||
"blockies": "0.0.2",
|
"blockies": "0.0.2",
|
||||||
"bytes": "^2.4.0",
|
"bytes": "^2.4.0",
|
||||||
|
"chart.js": "^2.3.0",
|
||||||
"es6-promise": "^3.2.1",
|
"es6-promise": "^3.2.1",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"format-json": "^1.0.3",
|
"format-json": "^1.0.3",
|
||||||
@ -128,12 +129,14 @@
|
|||||||
"qs": "^6.3.0",
|
"qs": "^6.3.0",
|
||||||
"react": "^15.2.1",
|
"react": "^15.2.1",
|
||||||
"react-addons-css-transition-group": "^15.2.1",
|
"react-addons-css-transition-group": "^15.2.1",
|
||||||
|
"react-chartjs-2": "^1.5.0",
|
||||||
"react-dom": "^15.2.1",
|
"react-dom": "^15.2.1",
|
||||||
"react-redux": "^4.4.5",
|
"react-redux": "^4.4.5",
|
||||||
"react-router": "^2.6.1",
|
"react-router": "^2.6.1",
|
||||||
"react-router-redux": "^4.0.5",
|
"react-router-redux": "^4.0.5",
|
||||||
"react-tap-event-plugin": "^1.0.0",
|
"react-tap-event-plugin": "^1.0.0",
|
||||||
"react-tooltip": "^2.0.3",
|
"react-tooltip": "^2.0.3",
|
||||||
|
"recharts": "^0.15.2",
|
||||||
"redux": "^3.5.2",
|
"redux": "^3.5.2",
|
||||||
"redux-actions": "^0.10.1",
|
"redux-actions": "^0.10.1",
|
||||||
"redux-thunk": "^2.1.0",
|
"redux-thunk": "^2.1.0",
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
|
||||||
import Form, { Input } from '../../../ui/Form';
|
import Form, { Input } from '../../../ui/Form';
|
||||||
|
import GasPriceSelector from '../GasPriceSelector';
|
||||||
|
|
||||||
import styles from '../transfer.css';
|
import styles from '../transfer.css';
|
||||||
|
|
||||||
@ -28,42 +29,63 @@ export default class Extras extends Component {
|
|||||||
gas: PropTypes.string,
|
gas: PropTypes.string,
|
||||||
gasEst: PropTypes.string,
|
gasEst: PropTypes.string,
|
||||||
gasError: PropTypes.string,
|
gasError: PropTypes.string,
|
||||||
gasPrice: PropTypes.string,
|
gasPrice: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.object
|
||||||
|
]),
|
||||||
gasPriceDefault: PropTypes.string,
|
gasPriceDefault: PropTypes.string,
|
||||||
gasPriceError: 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
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { gas, gasError, gasEst, gasPrice, gasPriceDefault, gasPriceError, total, totalError } = this.props;
|
const { gas, gasPrice, gasError, gasEst, gasPriceDefault, gasPriceError, gasPriceHistogram, total, totalError } = this.props;
|
||||||
|
|
||||||
const gasLabel = `gas amount (estimated: ${gasEst})`;
|
const gasLabel = `gas amount (estimated: ${gasEst})`;
|
||||||
const priceLabel = `gas price (current: ${gasPriceDefault})`;
|
const priceLabel = `gas price (current: ${gasPriceDefault})`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
|
|
||||||
{ this.renderData() }
|
{ this.renderData() }
|
||||||
|
|
||||||
<div className={ styles.columns }>
|
<div className={ styles.columns }>
|
||||||
<div>
|
<div style={ { flex: 65 } }>
|
||||||
|
<GasPriceSelector
|
||||||
|
gasPriceHistogram={ gasPriceHistogram }
|
||||||
|
gasPrice={ gasPrice }
|
||||||
|
onChange={ this.onEditGasPrice }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={ styles.row }
|
||||||
|
style={ {
|
||||||
|
flex: 35, paddingLeft: '1rem',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
paddingBottom: 12
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
<div className={ styles.row }>
|
||||||
<Input
|
<Input
|
||||||
label={ gasLabel }
|
label={ gasLabel }
|
||||||
hint='the amount of gas to use for the transaction'
|
hint='the amount of gas to use for the transaction'
|
||||||
error={ gasError }
|
error={ gasError }
|
||||||
value={ gas }
|
value={ gas }
|
||||||
onChange={ this.onEditGas } />
|
onChange={ this.onEditGas } />
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Input
|
<Input
|
||||||
label={ priceLabel }
|
label={ priceLabel }
|
||||||
hint='the price of gas to use for the transaction'
|
hint='the price of gas to use for the transaction'
|
||||||
error={ gasPriceError }
|
error={ gasPriceError }
|
||||||
value={ gasPrice }
|
value={ (gasPrice || '').toString() }
|
||||||
onChange={ this.onEditGasPrice } />
|
onChange={ this.onEditGasPrice } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className={ styles.columns }>
|
<div className={ styles.row }>
|
||||||
<div>
|
|
||||||
<Input
|
<Input
|
||||||
disabled
|
disabled
|
||||||
label='total transaction amount'
|
label='total transaction amount'
|
||||||
@ -72,6 +94,18 @@ export default class Extras extends Component {
|
|||||||
value={ `${total} ETH` } />
|
value={ `${total} ETH` } />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -99,8 +133,8 @@ export default class Extras extends Component {
|
|||||||
this.props.onChange('gas', event.target.value);
|
this.props.onChange('gas', event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditGasPrice = (event) => {
|
onEditGasPrice = (event, value) => {
|
||||||
this.props.onChange('gasPrice', event.target.value);
|
this.props.onChange('gasPrice', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEditData = (event) => {
|
onEditData = (event) => {
|
||||||
|
17
js/src/modals/Transfer/GasPriceSelector/gasPriceSelector.css
Normal file
17
js/src/modals/Transfer/GasPriceSelector/gasPriceSelector.css
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/>.
|
||||||
|
*/
|
||||||
|
|
556
js/src/modals/Transfer/GasPriceSelector/gasPriceSelector.js
Normal file
556
js/src/modals/Transfer/GasPriceSelector/gasPriceSelector.js
Normal file
@ -0,0 +1,556 @@
|
|||||||
|
// 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 React, { Component, PropTypes } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Bar, BarChart,
|
||||||
|
Rectangle,
|
||||||
|
Scatter, ScatterChart,
|
||||||
|
Tooltip,
|
||||||
|
XAxis, YAxis,
|
||||||
|
Dot,
|
||||||
|
ResponsiveContainer
|
||||||
|
} from 'recharts';
|
||||||
|
|
||||||
|
import Slider from 'material-ui/Slider';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
|
import componentStyles from './gasPriceSelector.css';
|
||||||
|
import mainStyles from '../transfer.css';
|
||||||
|
|
||||||
|
const styles = Object.assign({}, mainStyles, componentStyles);
|
||||||
|
|
||||||
|
const COLORS = {
|
||||||
|
default: 'rgba(255, 99, 132, 0.2)',
|
||||||
|
selected: 'rgba(255, 99, 132, 0.5)',
|
||||||
|
hover: 'rgba(255, 99, 132, 0.15)',
|
||||||
|
grid: 'rgba(255, 99, 132, 0.5)',
|
||||||
|
line: 'rgb(255, 99, 132)',
|
||||||
|
intersection: '#fff'
|
||||||
|
};
|
||||||
|
|
||||||
|
const countModifier = (count) => {
|
||||||
|
const val = count.toNumber ? count.toNumber() : count;
|
||||||
|
return Math.log10(val + 1) + 0.1;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CustomCursor extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
x: PropTypes.number,
|
||||||
|
y: PropTypes.number,
|
||||||
|
width: PropTypes.number,
|
||||||
|
height: PropTypes.number,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
getIndex: PropTypes.func,
|
||||||
|
counts: PropTypes.array,
|
||||||
|
yDomain: PropTypes.array
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { x, y, width, height, getIndex, counts, yDomain } = this.props;
|
||||||
|
|
||||||
|
const index = getIndex();
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = countModifier(counts[index]);
|
||||||
|
const barHeight = (count / yDomain[1]) * (y + height);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<Rectangle
|
||||||
|
x={ x }
|
||||||
|
y={ 0 }
|
||||||
|
width={ width }
|
||||||
|
height={ height + y }
|
||||||
|
fill='transparent'
|
||||||
|
onClick={ this.onClick }
|
||||||
|
/>
|
||||||
|
<Rectangle
|
||||||
|
x={ x }
|
||||||
|
y={ y + (height - barHeight) }
|
||||||
|
width={ width }
|
||||||
|
height={ barHeight }
|
||||||
|
fill={ COLORS.hover }
|
||||||
|
onClick={ this.onClick }
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick = () => {
|
||||||
|
const { onClick, getIndex } = this.props;
|
||||||
|
const index = getIndex();
|
||||||
|
onClick({ index });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomBar extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
selected: PropTypes.number,
|
||||||
|
x: PropTypes.number,
|
||||||
|
y: PropTypes.number,
|
||||||
|
width: PropTypes.number,
|
||||||
|
height: PropTypes.number,
|
||||||
|
index: PropTypes.number,
|
||||||
|
onClick: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { x, y, selected, index, width, height, onClick } = this.props;
|
||||||
|
|
||||||
|
const fill = selected === index
|
||||||
|
? COLORS.selected
|
||||||
|
: COLORS.default;
|
||||||
|
|
||||||
|
const borderWidth = 0.5;
|
||||||
|
const borderColor = 'rgba(255, 255, 255, 0.5)';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<Rectangle
|
||||||
|
x={ x - borderWidth }
|
||||||
|
y={ y }
|
||||||
|
width={ borderWidth }
|
||||||
|
height={ height }
|
||||||
|
fill={ borderColor }
|
||||||
|
/>
|
||||||
|
<Rectangle
|
||||||
|
x={ x + width }
|
||||||
|
y={ y }
|
||||||
|
width={ borderWidth }
|
||||||
|
height={ height }
|
||||||
|
fill={ borderColor }
|
||||||
|
/>
|
||||||
|
<Rectangle
|
||||||
|
x={ x - borderWidth }
|
||||||
|
y={ y - borderWidth }
|
||||||
|
width={ width + borderWidth * 2 }
|
||||||
|
height={ borderWidth }
|
||||||
|
fill={ borderColor }
|
||||||
|
/>
|
||||||
|
<Rectangle
|
||||||
|
x={ x }
|
||||||
|
y={ y }
|
||||||
|
width={ width }
|
||||||
|
height={ height }
|
||||||
|
fill={ fill }
|
||||||
|
onClick={ onClick }
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomizedShape extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
showValue: PropTypes.number.isRequired,
|
||||||
|
cx: PropTypes.number,
|
||||||
|
cy: PropTypes.number,
|
||||||
|
payload: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { cx, cy, showValue, payload } = this.props;
|
||||||
|
|
||||||
|
if (showValue !== payload.y) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<Dot
|
||||||
|
style={ { fill: 'white' } }
|
||||||
|
cx={ cx }
|
||||||
|
cy={ cy }
|
||||||
|
r={ 5 }
|
||||||
|
/>
|
||||||
|
<Dot
|
||||||
|
style={ { fill: 'rgb(255, 99, 132)' } }
|
||||||
|
cx={ cx }
|
||||||
|
cy={ cy }
|
||||||
|
r={ 3 }
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomTooltip extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
gasPriceHistogram: PropTypes.shape({
|
||||||
|
bucketBounds: PropTypes.array.isRequired,
|
||||||
|
counts: PropTypes.array.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
type: PropTypes.string,
|
||||||
|
payload: PropTypes.array,
|
||||||
|
label: PropTypes.number,
|
||||||
|
active: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { active, label, gasPriceHistogram } = this.props;
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = label;
|
||||||
|
|
||||||
|
const count = gasPriceHistogram.counts[index];
|
||||||
|
const minGasPrice = gasPriceHistogram.bucketBounds[index];
|
||||||
|
const maxGasPrice = gasPriceHistogram.bucketBounds[index + 1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className='label'>
|
||||||
|
{ count.toNumber() } transactions
|
||||||
|
with gas price set from
|
||||||
|
<span> { minGasPrice.toFormat(0) } </span>
|
||||||
|
to
|
||||||
|
<span> { maxGasPrice.toFormat(0) } </span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class GasPriceSelector extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
gasPriceHistogram: PropTypes.shape({
|
||||||
|
bucketBounds: PropTypes.array.isRequired,
|
||||||
|
counts: PropTypes.array.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
gasPrice: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.object
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
gasPrice: null,
|
||||||
|
sliderValue: 0.5,
|
||||||
|
selectedIndex: 0,
|
||||||
|
|
||||||
|
chartData: {
|
||||||
|
values: [],
|
||||||
|
xDomain: [],
|
||||||
|
yDomain: [],
|
||||||
|
N: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
this.computeCharts();
|
||||||
|
this.setGasPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
if (nextProps.gasPrice !== this.props.gasPrice) {
|
||||||
|
this.setGasPrice(nextProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUpdate (nextProps, nextState) {
|
||||||
|
if (Math.floor(nextState.sliderValue) !== Math.floor(this.state.sliderValue)) {
|
||||||
|
this.updateSelectedBarChart(nextState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{ this.renderChart() }
|
||||||
|
{ this.renderSlider() }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSlider () {
|
||||||
|
const { sliderValue } = this.state;
|
||||||
|
|
||||||
|
return (<div className={ styles.columns }>
|
||||||
|
<Slider
|
||||||
|
min={ 0 }
|
||||||
|
max={ 1 }
|
||||||
|
value={ sliderValue }
|
||||||
|
onChange={ this.onEditGasPriceSlider }
|
||||||
|
style={ {
|
||||||
|
flex: 1,
|
||||||
|
padding: '0 0.3em'
|
||||||
|
} }
|
||||||
|
sliderStyle={ {
|
||||||
|
marginBottom: 12
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChart () {
|
||||||
|
const { gasPriceHistogram } = this.props;
|
||||||
|
const { chartData, sliderValue, selectedIndex } = this.state;
|
||||||
|
|
||||||
|
if (chartData.values.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const height = 300;
|
||||||
|
const countIndex = Math.max(0, Math.min(selectedIndex, gasPriceHistogram.counts.length - 1));
|
||||||
|
const selectedCount = countModifier(gasPriceHistogram.counts[countIndex]);
|
||||||
|
|
||||||
|
return (<div className={ styles.columns }>
|
||||||
|
<div style={ { flex: 1, height } }>
|
||||||
|
<div className={ styles.chart }>
|
||||||
|
<ResponsiveContainer
|
||||||
|
height={ height }
|
||||||
|
>
|
||||||
|
<ScatterChart
|
||||||
|
margin={ { top: 0, right: 0, left: 0, bottom: 0 } }
|
||||||
|
>
|
||||||
|
<Scatter
|
||||||
|
data={ [
|
||||||
|
{ x: sliderValue, y: 0 },
|
||||||
|
{ x: sliderValue, y: selectedCount },
|
||||||
|
{ x: sliderValue, y: chartData.yDomain[1] }
|
||||||
|
] }
|
||||||
|
shape={ <CustomizedShape showValue={ selectedCount } /> }
|
||||||
|
line
|
||||||
|
isAnimationActive={ false }
|
||||||
|
/>
|
||||||
|
|
||||||
|
<XAxis
|
||||||
|
hide
|
||||||
|
height={ 0 }
|
||||||
|
dataKey='x'
|
||||||
|
domain={ [0, 1] }
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
hide
|
||||||
|
width={ 0 }
|
||||||
|
dataKey='y'
|
||||||
|
domain={ chartData.yDomain }
|
||||||
|
/>
|
||||||
|
</ScatterChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={ styles.chart }>
|
||||||
|
<ResponsiveContainer
|
||||||
|
height={ height }
|
||||||
|
>
|
||||||
|
<BarChart
|
||||||
|
data={ chartData.values }
|
||||||
|
margin={ { top: 0, right: 0, left: 0, bottom: 0 } }
|
||||||
|
barCategoryGap={ 1 }
|
||||||
|
ref='barChart'
|
||||||
|
>
|
||||||
|
<Bar
|
||||||
|
dataKey='value'
|
||||||
|
stroke={ COLORS.line }
|
||||||
|
onClick={ this.onClickGasPrice }
|
||||||
|
shape={ <CustomBar selected={ selectedIndex } onClick={ this.onClickGasPrice } /> }
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
wrapperStyle={ {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||||
|
padding: '0 0.5em',
|
||||||
|
fontSize: '0.9em'
|
||||||
|
} }
|
||||||
|
cursor={ this.renderCustomCursor() }
|
||||||
|
content={ <CustomTooltip gasPriceHistogram={ gasPriceHistogram } /> }
|
||||||
|
/>
|
||||||
|
|
||||||
|
<XAxis
|
||||||
|
hide
|
||||||
|
dataKey='index'
|
||||||
|
type='category'
|
||||||
|
domain={ chartData.xDomain }
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
hide
|
||||||
|
type='number'
|
||||||
|
domain={ chartData.yDomain }
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCustomCursor = () => {
|
||||||
|
const { gasPriceHistogram } = this.props;
|
||||||
|
const { chartData } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomCursor
|
||||||
|
getIndex={ this.getBarHoverIndex }
|
||||||
|
onClick={ this.onClickGasPrice }
|
||||||
|
counts={ gasPriceHistogram.counts }
|
||||||
|
yDomain={ chartData.yDomain }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBarHoverIndex = () => {
|
||||||
|
const { barChart } = this.refs;
|
||||||
|
|
||||||
|
if (!barChart || !barChart.state) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return barChart.state.activeTooltipIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeChartsData () {
|
||||||
|
const { gasPriceChartData } = this.state;
|
||||||
|
const { gasPriceHistogram } = this.props;
|
||||||
|
|
||||||
|
const values = gasPriceChartData
|
||||||
|
.map((value, index) => ({ value, index }));
|
||||||
|
|
||||||
|
const N = values.length - 1;
|
||||||
|
const maxGasCounts = countModifier(
|
||||||
|
gasPriceHistogram
|
||||||
|
.counts
|
||||||
|
.reduce((max, count) => count.greaterThan(max) ? count : max, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
const xDomain = [0, N];
|
||||||
|
const yDomain = [0, maxGasCounts * 1.1];
|
||||||
|
|
||||||
|
const chartData = {
|
||||||
|
values, N,
|
||||||
|
xDomain, yDomain
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({ chartData }, () => {
|
||||||
|
this.updateSelectedBarChart();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
computeCharts (props = this.props) {
|
||||||
|
const { gasPriceHistogram } = props;
|
||||||
|
|
||||||
|
const gasPriceChartData = gasPriceHistogram
|
||||||
|
.counts
|
||||||
|
.map(count => countModifier(count));
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
{ gasPriceChartData },
|
||||||
|
() => this.computeChartsData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectedBarChart (state = this.state) {
|
||||||
|
}
|
||||||
|
|
||||||
|
setGasPrice (props = this.props) {
|
||||||
|
const { gasPrice, gasPriceHistogram } = props;
|
||||||
|
|
||||||
|
// If no gas price yet...
|
||||||
|
if (!gasPrice) {
|
||||||
|
return this.setSliderValue(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bnGasPrice = (typeof gasPrice === 'string')
|
||||||
|
? new BigNumber(gasPrice)
|
||||||
|
: gasPrice;
|
||||||
|
|
||||||
|
// If gas price hasn't changed
|
||||||
|
if (this.state.gasPrice && bnGasPrice.equals(this.state.gasPrice)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gasPrices = gasPriceHistogram.bucketBounds;
|
||||||
|
const startIndex = gasPrices
|
||||||
|
.findIndex(price => price.greaterThan(bnGasPrice)) - 1;
|
||||||
|
|
||||||
|
// gasPrice Lower than the max in histogram
|
||||||
|
if (startIndex === -1) {
|
||||||
|
return this.setSliderValue(0, bnGasPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// gasPrice Greater than the max in histogram
|
||||||
|
if (startIndex === -2) {
|
||||||
|
return this.setSliderValue(1, bnGasPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
const priceA = gasPrices[startIndex];
|
||||||
|
const priceB = gasPrices[startIndex + 1];
|
||||||
|
|
||||||
|
const sliderValueDec = bnGasPrice
|
||||||
|
.minus(priceA)
|
||||||
|
.dividedBy(priceB.minus(priceA))
|
||||||
|
.toNumber();
|
||||||
|
|
||||||
|
const sliderValue = (startIndex + sliderValueDec) / (gasPrices.length - 1);
|
||||||
|
this.setSliderValue(sliderValue, bnGasPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSliderValue (value, gasPrice = this.state.gasPrice) {
|
||||||
|
const { gasPriceHistogram } = this.props;
|
||||||
|
|
||||||
|
const N = gasPriceHistogram.bucketBounds.length - 1;
|
||||||
|
|
||||||
|
const sliderValue = Math.max(0, Math.min(value, 1));
|
||||||
|
const selectedIndex = Math.floor(sliderValue * N);
|
||||||
|
|
||||||
|
this.setState({ sliderValue, gasPrice, selectedIndex });
|
||||||
|
}
|
||||||
|
|
||||||
|
onBarChartMouseUp = (event) => {
|
||||||
|
console.log(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickGasPrice = (bar) => {
|
||||||
|
const { index } = bar;
|
||||||
|
|
||||||
|
const ratio = (index + 0.5) / (this.state.chartData.N + 1);
|
||||||
|
|
||||||
|
this.onEditGasPriceSlider(null, ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditGasPriceSlider = (event, sliderValue) => {
|
||||||
|
const { gasPriceHistogram } = this.props;
|
||||||
|
|
||||||
|
const gasPrices = gasPriceHistogram.bucketBounds;
|
||||||
|
const N = gasPrices.length - 1;
|
||||||
|
const gasPriceAIdx = Math.floor(sliderValue * N);
|
||||||
|
const gasPriceBIdx = gasPriceAIdx + 1;
|
||||||
|
|
||||||
|
if (gasPriceBIdx === N + 1) {
|
||||||
|
const gasPrice = gasPrices[gasPriceAIdx];
|
||||||
|
this.props.onChange(event, gasPrice);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gasPriceA = gasPrices[gasPriceAIdx];
|
||||||
|
const gasPriceB = gasPrices[gasPriceBIdx];
|
||||||
|
|
||||||
|
const mult = Math.round((sliderValue % 1) * 100) / 100;
|
||||||
|
const gasPrice = gasPriceA.plus(gasPriceB.minus(gasPriceA).times(mult));
|
||||||
|
|
||||||
|
this.setSliderValue(sliderValue, gasPrice);
|
||||||
|
this.props.onChange(event, gasPrice);
|
||||||
|
}
|
||||||
|
}
|
17
js/src/modals/Transfer/GasPriceSelector/index.js
Normal file
17
js/src/modals/Transfer/GasPriceSelector/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 './gasPriceSelector';
|
@ -25,6 +25,13 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.columns>div {
|
.columns>div {
|
||||||
flex: 0 1 50%;
|
flex: 0 1 50%;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
@ -131,3 +138,16 @@
|
|||||||
.inputoverride {
|
.inputoverride {
|
||||||
padding-top: 24px !important;
|
padding-top: 24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contentTitle {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gasPriceDesc {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
@ -62,6 +62,7 @@ export default class Transfer extends Component {
|
|||||||
gasEst: '0',
|
gasEst: '0',
|
||||||
gasError: null,
|
gasError: null,
|
||||||
gasPrice: DEFAULT_GASPRICE,
|
gasPrice: DEFAULT_GASPRICE,
|
||||||
|
gasPriceHistogram: {},
|
||||||
gasPriceError: null,
|
gasPriceError: null,
|
||||||
recipient: '',
|
recipient: '',
|
||||||
recipientError: ERRORS.requireRecipient,
|
recipientError: ERRORS.requireRecipient,
|
||||||
@ -89,7 +90,9 @@ export default class Transfer extends Component {
|
|||||||
current={ stage }
|
current={ stage }
|
||||||
steps={ extras ? STAGES_EXTRA : STAGES_BASIC }
|
steps={ extras ? STAGES_EXTRA : STAGES_BASIC }
|
||||||
waiting={ extras ? [2] : [1] }
|
waiting={ extras ? [2] : [1] }
|
||||||
visible>
|
visible
|
||||||
|
scroll
|
||||||
|
>
|
||||||
{ this.renderPage() }
|
{ this.renderPage() }
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@ -169,6 +172,10 @@ export default class Transfer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderExtrasPage () {
|
renderExtrasPage () {
|
||||||
|
if (!this.state.gasPriceHistogram) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Extras
|
<Extras
|
||||||
isEth={ this.state.isEth }
|
isEth={ this.state.isEth }
|
||||||
@ -180,6 +187,7 @@ export default class Transfer extends Component {
|
|||||||
gasPrice={ this.state.gasPrice }
|
gasPrice={ this.state.gasPrice }
|
||||||
gasPriceDefault={ this.state.gasPriceDefault }
|
gasPriceDefault={ this.state.gasPriceDefault }
|
||||||
gasPriceError={ this.state.gasPriceError }
|
gasPriceError={ this.state.gasPriceError }
|
||||||
|
gasPriceHistogram={ this.state.gasPriceHistogram }
|
||||||
total={ this.state.total }
|
total={ this.state.total }
|
||||||
totalError={ this.state.totalError }
|
totalError={ this.state.totalError }
|
||||||
onChange={ this.onUpdateDetails } />
|
onChange={ this.onUpdateDetails } />
|
||||||
@ -581,12 +589,16 @@ export default class Transfer extends Component {
|
|||||||
getDefaults = () => {
|
getDefaults = () => {
|
||||||
const { api } = this.context;
|
const { api } = this.context;
|
||||||
|
|
||||||
api.eth
|
Promise
|
||||||
.gasPrice()
|
.all([
|
||||||
.then((gasPrice) => {
|
api.ethcore.gasPriceHistogram(),
|
||||||
|
api.eth.gasPrice()
|
||||||
|
])
|
||||||
|
.then(([gasPriceHistogram, gasPrice]) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
gasPrice: gasPrice.toString(),
|
gasPrice: gasPrice.toString(),
|
||||||
gasPriceDefault: gasPrice.toFormat()
|
gasPriceDefault: gasPrice.toFormat(),
|
||||||
|
gasPriceHistogram
|
||||||
}, this.recalculate);
|
}, this.recalculate);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -81,11 +81,18 @@ class Account extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderActionbar () {
|
renderActionbar () {
|
||||||
|
const { address } = this.props.params;
|
||||||
|
const { balances } = this.props;
|
||||||
|
const balance = balances[address];
|
||||||
|
|
||||||
|
const showTransferButton = !!(balance && balance.tokens);
|
||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
<Button
|
<Button
|
||||||
key='transferFunds'
|
key='transferFunds'
|
||||||
icon={ <ContentSend /> }
|
icon={ <ContentSend /> }
|
||||||
label='transfer'
|
label='transfer'
|
||||||
|
disabled={ !showTransferButton }
|
||||||
onClick={ this.onTransferClick } />,
|
onClick={ this.onTransferClick } />,
|
||||||
<Button
|
<Button
|
||||||
key='shapeshift'
|
key='shapeshift'
|
||||||
|
Loading…
Reference in New Issue
Block a user