-
-
-
+
+
+
+ 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.
+
+
);
}
@@ -99,8 +133,8 @@ export default class Extras extends Component {
this.props.onChange('gas', event.target.value);
}
- onEditGasPrice = (event) => {
- this.props.onChange('gasPrice', event.target.value);
+ onEditGasPrice = (event, value) => {
+ this.props.onChange('gasPrice', value);
}
onEditData = (event) => {
diff --git a/js/src/modals/Transfer/GasPriceSelector/gasPriceSelector.css b/js/src/modals/Transfer/GasPriceSelector/gasPriceSelector.css
new file mode 100644
index 000000000..445174c59
--- /dev/null
+++ b/js/src/modals/Transfer/GasPriceSelector/gasPriceSelector.css
@@ -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
.
+*/
+
diff --git a/js/src/modals/Transfer/GasPriceSelector/gasPriceSelector.js b/js/src/modals/Transfer/GasPriceSelector/gasPriceSelector.js
new file mode 100644
index 000000000..0dcee8f9d
--- /dev/null
+++ b/js/src/modals/Transfer/GasPriceSelector/gasPriceSelector.js
@@ -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
.
+
+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 (
+
+
+
+
+ );
+ }
+
+ 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 (
+
+
+
+
+
+
+ );
+ }
+}
+
+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 (
+
+
+
+
+ );
+ }
+}
+
+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 (
+
+
+ { count.toNumber() } transactions
+ with gas price set from
+ { minGasPrice.toFormat(0) }
+ to
+ { maxGasPrice.toFormat(0) }
+
+
+ );
+ }
+}
+
+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 (
+
+ { this.renderChart() }
+ { this.renderSlider() }
+
+ );
+ }
+
+ renderSlider () {
+ const { sliderValue } = this.state;
+
+ return (
+
+
);
+ }
+
+ 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 (
+
+
+
+
+ }
+ line
+ isAnimationActive={ false }
+ />
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+ }
+ />
+
+
+
+
+
+
+
+
);
+ }
+
+ renderCustomCursor = () => {
+ const { gasPriceHistogram } = this.props;
+ const { chartData } = this.state;
+
+ return (
+
+ );
+ }
+
+ 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);
+ }
+}
diff --git a/js/src/modals/Transfer/GasPriceSelector/index.js b/js/src/modals/Transfer/GasPriceSelector/index.js
new file mode 100644
index 000000000..958075867
--- /dev/null
+++ b/js/src/modals/Transfer/GasPriceSelector/index.js
@@ -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
.
+
+export default from './gasPriceSelector';
diff --git a/js/src/modals/Transfer/transfer.css b/js/src/modals/Transfer/transfer.css
index 720cd694b..89b58666e 100644
--- a/js/src/modals/Transfer/transfer.css
+++ b/js/src/modals/Transfer/transfer.css
@@ -25,6 +25,13 @@
position: relative;
}
+.row {
+ display: flex;
+ flex-wrap: wrap;
+ position: relative;
+ flex-direction: column;
+}
+
.columns>div {
flex: 0 1 50%;
width: 50%;
@@ -131,3 +138,16 @@
.inputoverride {
padding-top: 24px !important;
}
+
+.contentTitle {
+ font-size: 1.2rem;
+}
+
+.chart {
+ position: absolute;
+ width: 100%;
+}
+
+.gasPriceDesc {
+ font-size: 0.9em;
+}
diff --git a/js/src/modals/Transfer/transfer.js b/js/src/modals/Transfer/transfer.js
index 0ffad525c..192b2f460 100644
--- a/js/src/modals/Transfer/transfer.js
+++ b/js/src/modals/Transfer/transfer.js
@@ -62,6 +62,7 @@ export default class Transfer extends Component {
gasEst: '0',
gasError: null,
gasPrice: DEFAULT_GASPRICE,
+ gasPriceHistogram: {},
gasPriceError: null,
recipient: '',
recipientError: ERRORS.requireRecipient,
@@ -89,7 +90,9 @@ export default class Transfer extends Component {
current={ stage }
steps={ extras ? STAGES_EXTRA : STAGES_BASIC }
waiting={ extras ? [2] : [1] }
- visible>
+ visible
+ scroll
+ >
{ this.renderPage() }
);
@@ -169,6 +172,10 @@ export default class Transfer extends Component {
}
renderExtrasPage () {
+ if (!this.state.gasPriceHistogram) {
+ return null;
+ }
+
return (
@@ -581,12 +589,16 @@ export default class Transfer extends Component {
getDefaults = () => {
const { api } = this.context;
- api.eth
- .gasPrice()
- .then((gasPrice) => {
+ Promise
+ .all([
+ api.ethcore.gasPriceHistogram(),
+ api.eth.gasPrice()
+ ])
+ .then(([gasPriceHistogram, gasPrice]) => {
this.setState({
gasPrice: gasPrice.toString(),
- gasPriceDefault: gasPrice.toFormat()
+ gasPriceDefault: gasPrice.toFormat(),
+ gasPriceHistogram
}, this.recalculate);
})
.catch((error) => {
diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js
index ecdf1cfd8..902e3e7c1 100644
--- a/js/src/views/Account/account.js
+++ b/js/src/views/Account/account.js
@@ -81,11 +81,18 @@ class Account extends Component {
}
renderActionbar () {
+ const { address } = this.props.params;
+ const { balances } = this.props;
+ const balance = balances[address];
+
+ const showTransferButton = !!(balance && balance.tokens);
+
const buttons = [
}
label='transfer'
+ disabled={ !showTransferButton }
onClick={ this.onTransferClick } />,