Commit 91bc364e authored by Amaury Martiny's avatar Amaury Martiny

Take out estimateGas logic from sendStore

parent a85a8f25
......@@ -38,8 +38,10 @@
"@parity/light.js": "https://github.com/parity-js/light.js#9646ce15d9dd9c4cf11776ddd613d5bd86016f94",
"@parity/shared": "^3.0.2",
"bignumber.js": "^4.1.0",
"debounce-promise": "^3.1.0",
"debug": "^3.1.0",
"fether-ui": "^0.1.0",
"final-form": "^4.8.3",
"is-electron": "^2.1.0",
"light-hoc": "^0.1.0",
"lodash": "^4.17.10",
......@@ -48,10 +50,12 @@
"react": "^16.3.2",
"react-blockies": "^1.3.0",
"react-dom": "^16.3.2",
"react-final-form": "^3.6.4",
"react-markdown": "^3.3.4",
"react-router-dom": "^4.2.2",
"react-scripts": "1.1.4",
"react-tooltip": "^3.6.1",
"recompose": "^0.27.1",
"rxjs": "^6.2.0"
},
"devDependencies": {
......
......@@ -4,32 +4,20 @@
// SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react';
import { inject, observer } from 'mobx-react';
import { Route, Redirect, Switch } from 'react-router-dom';
import Sent from './Sent';
import Signer from './Signer';
import TxForm from './TxForm';
@inject('sendStore')
@observer
class Send extends Component {
render () {
const {
sendStore: { tokenAddress }
} = this.props;
// We only show then Send components if we have already selected a token to
// send.
if (!tokenAddress) {
return <Redirect to='/' />;
}
return (
<Switch>
<Route exact path='/send' component={TxForm} />
<Route path='/send/signer' component={Signer} />
<Route path='/send/sent' component={Sent} />
<Route exact path='/send/:tokenAddress' component={TxForm} />
<Route path='/send/:tokenAddress/signer' component={Signer} />
<Route path='/send/:tokenAddress/sent' component={Sent} />
<Redirect to='/' />
</Switch>
);
}
......
......@@ -9,10 +9,14 @@ import { FormField, Header } from 'fether-ui';
import { inject, observer } from 'mobx-react';
import { Link } from 'react-router-dom';
import ReactTooltip from 'react-tooltip';
import { withProps } from 'recompose';
import TokenBalance from '../../Tokens/TokensList/TokenBalance';
@inject('sendStore', 'tokensStore')
@withProps(({ match: { params: { tokenAddress } }, tokensStore }) => ({
token: tokensStore.tokens[tokenAddress]
}))
@observer
class Signer extends Component {
state = {
......@@ -58,11 +62,10 @@ class Signer extends Component {
render () {
const {
sendStore: { tokenAddress, tx },
tokensStore
sendStore: { tx },
token
} = this.props;
const { error, isSending, password } = this.state;
const token = tokensStore.tokens[tokenAddress];
return (
<div>
......@@ -132,12 +135,6 @@ class Signer extends Component {
/>
</div>
</div>
<ReactTooltip
effect='solid'
event='mouseover'
eventOff='keydown mouseout'
place='top'
/>
</div>
);
}
......
......@@ -4,13 +4,15 @@
// SPDX-License-Identifier: BSD-3-Clause
import React, { Component } from 'react';
import debounce from 'lodash/debounce';
import { FormField, Header } from 'fether-ui';
import debounce from 'debounce-promise';
import { estimateGas } from '../../utils/estimateGas';
import { Field, Form } from 'react-final-form';
import { Form as FetherForm, Header } from 'fether-ui';
import { fromWei, toWei } from '@parity/api/lib/util/wei';
import { inject, observer } from 'mobx-react';
import { isAddress } from '@parity/api/lib/util/address';
import { Link } from 'react-router-dom';
import ReactTooltip from 'react-tooltip';
import { withProps } from 'recompose';
import TokenBalance from '../../Tokens/TokensList/TokenBalance';
import withBalance from '../../utils/withBalance';
......@@ -18,120 +20,25 @@ import withBalance from '../../utils/withBalance';
const MAX_GAS_PRICE = 40; // In Gwei
const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei
@inject('sendStore', 'tokensStore')
@withBalance(
({ sendStore: { tokenAddress }, tokensStore }) =>
tokensStore.tokens[tokenAddress]
)
@inject('parityStore', 'sendStore', 'tokensStore')
@withProps(({ match: { params: { tokenAddress } }, tokensStore }) => ({
token: tokensStore.tokens[tokenAddress]
}))
@withBalance
@observer
class Send extends Component {
state = {
amount: '', // In Ether or in token
gasPrice: 4, // in Gwei
to: '',
estimating: false, // Currently estimating gasPrice
...this.props.sendStore.tx
};
static getDerivedStateFromProps (nextProps, prevState) {
const {
balance,
sendStore: { estimated }
} = nextProps;
// Calculate the maxAount
return {
maxAmount:
balance && estimated
? +fromWei(
toWei(balance).minus(
estimated.mul(toWei(prevState.gasPrice, 'shannon'))
)
)
: 0.01
};
}
componentDidMount () {
this.handleEstimateGasPrice();
}
estimateGas = debounce(
() =>
this.props.sendStore
.estimateGas()
.then(() => this.setState({ estimating: false }))
.catch(() => this.setState({ estimating: false })),
1000
);
handleChangeAmount = ({ target: { value } }) =>
this.setState({ amount: value }, this.handleEstimateGasPrice);
handleChangeGasPrice = ({ target: { value } }) =>
this.setState({ gasPrice: value }, this.handleEstimateGasPrice);
handleChangeTo = ({ target: { value } }) => {
this.setState({ to: value }, this.handleEstimateGasPrice);
};
handleEstimateGasPrice = () => {
if (this.hasError()) {
return;
}
const { amount, gasPrice, to } = this.state;
this.props.sendStore.setTx({ amount, gasPrice, to });
this.setState({ estimating: true }, this.estimateGas);
};
handleMax = () =>
this.setState(
{ amount: this.state.maxAmount },
this.handleEstimateGasPrice
);
handleSubmit = event => {
event.preventDefault();
const { history } = this.props;
history.push('/send/signer');
};
/**
* Get form errors.
*
* TODO Use a React form library to do this?
*/
hasError = () => {
const { amount, maxAmount, to } = this.state;
if (!amount || isNaN(amount)) {
return 'Please enter a valid amount';
}
if (amount < 0) {
return 'Please enter a positive amount ';
}
if (amount > maxAmount) {
return "You don't have enough balance";
}
if (!isAddress(to)) {
return 'Please enter a valid Ethereum address';
}
return null;
const { history, sendStore, token } = this.props;
sendStore.setTx(event);
history.push(`/send/${token.address}/signer`);
};
render () {
const {
sendStore: { tokenAddress },
tokensStore
sendStore: { tx },
token
} = this.props;
const { amount, estimating, gasPrice, maxAmount, to } = this.state;
const token = tokensStore.tokens[tokenAddress];
const error = this.hasError();
return (
<div>
......@@ -141,7 +48,7 @@ class Send extends Component {
Close
</Link>
}
title={<h1>Send {token.name}</h1>}
title={token && <h1>Send {token.name}</h1>}
/>
<div className='window_content'>
......@@ -149,104 +56,121 @@ class Send extends Component {
<TokenBalance
decimals={6}
drawers={[
<form
className='send-form'
<Form
key='txForm'
initialValues={{ gasPrice: 4, ...tx }}
onSubmit={this.handleSubmit}
>
<fieldset className='form_fields'>
<FormField
input={
<div>
<input
className='form_field_amount'
formNoValidate
max={maxAmount}
min={0}
onChange={this.handleChangeAmount}
placeholder='1.00'
required
step={+fromWei(1)}
type='number'
value={amount}
/>
<nav className='form-field_nav'>
<button
className='button -utility'
onClick={this.handleMax}
tabIndex={-1}
type='button'
>
Max
</button>
</nav>
</div>
}
label='Amount'
/>
<FormField
input={
<textarea
validate={this.validateForm}
render={({ handleSubmit, valid, validating, values }) => (
<form className='send-form' onSubmit={handleSubmit}>
<fieldset className='form_fields'>
<Field
className='form_field_amount'
formNoValidate
label='Amount'
name='amount'
placeholder='1.00'
render={FetherForm.Field}
required
type='number' // In ETH or coin
/>
<Field
as='textarea'
className='-sm'
onChange={this.handleChangeTo}
label='To'
name='to'
placeholder='0x...'
required
type='text'
value={to}
render={FetherForm.Field}
/>
<Field
centerText={`${values.gasPrice} GWEI`}
className='-range'
label='Gas'
leftText='Cheap'
max={MAX_GAS_PRICE}
min={MIN_GAS_PRICE}
name='gasPrice'
render={FetherForm.Slider}
required
rightText='Fast'
step={0.5}
type='range' // In Gwei
/>
}
label='To'
/>
<FormField
className='-range'
input={
<div>
<input
max={MAX_GAS_PRICE}
min={MIN_GAS_PRICE}
onChange={this.handleChangeGasPrice}
required
step={0.5}
type='range'
value={gasPrice}
/>
<nav className='range-nav'>
<span className='range-nav_label'>Cheap</span>
<span className='range-nav_value'>
{gasPrice} Gwei
</span>
<span className='range-nav_label'>Fast</span>
</nav>
</div>
}
label='Gas'
/>
</fieldset>
<nav className='form-nav'>
<span data-tip={error || ''}>
<button disabled={error || estimating} className='button'>
{estimating ? 'Checking...' : 'Send'}
</button>
</span>
</nav>
</form>
</fieldset>
<nav className='form-nav'>
<button
disabled={!valid || validating}
className='button'
>
{validating ? 'Checking...' : 'Send'}
</button>
</nav>
</form>
)}
/>
]}
onClick={null}
onClick={null} // To disable cursor:pointer on card // TODO Can this be done better?
token={token}
/>
</div>
</div>
<ReactTooltip
effect='solid'
event='mouseover'
eventOff='mouseout'
place='top'
/>
</div>
);
}
/**
* Estimate gas amount, and validate that the user has enough balance to make
* the tx.
*/
validateBalance = debounce(async values => {
try {
const estimated = await estimateGas(
values,
this.props.token,
this.props.parityStore.api
);
const { balance } = this.props;
if (!balance || isNaN(estimated)) {
throw new Error('No "balance" or "estimated" value.');
}
// Calculate the max amount the user can send
const maxAmount = +fromWei(
toWei(balance).minus(estimated.mul(toWei(values.gasPrice, 'shannon')))
);
if (values.amount > maxAmount) {
return { amount: "You don't have enough balance" };
}
} catch (err) {
return {
amount: 'Failed estimating balance, please try again'
};
}
}, 1000);
validateForm = values => {
if (!values.amount || isNaN(values.amount)) {
return { amount: 'Please enter a valid amount' };
}
if (values.amount < 0) {
return { amount: 'Please enter a positive amount ' };
}
if (this.props.balance && this.props.balance.lt(values.amount)) {
return { amount: "You don't have enough balance" };
}
if (!isAddress(values.to)) {
return { to: 'Please enter a valid Ethereum address' };
}
return this.validateBalance(values);
};
}
export default Send;
......@@ -11,12 +11,12 @@ import { withRouter } from 'react-router-dom';
import withBalance from '../../../utils/withBalance';
@withBalance()
@withBalance
@inject('sendStore')
@withRouter
class TokenBalance extends Component {
static propTypes = {
token: PropTypes.object.isRequired
token: PropTypes.object
};
handleClick = () => {
......@@ -25,8 +25,7 @@ class TokenBalance extends Component {
return;
}
sendStore.clear();
sendStore.setTokenAddress(token.address);
history.push('/send');
history.push(`/send/${token.address}`);
};
render () {
......
......@@ -9,11 +9,11 @@ import { BigNumber } from 'bignumber.js';
import { blockNumber$, makeContract$, post$ } from '@parity/light.js';
import memoize from 'lodash/memoize';
import noop from 'lodash/noop';
import { toWei } from '@parity/api/lib/util/wei';
import Debug from '../utils/debug';
import parityStore from './parityStore';
import tokensStore from './tokensStore';
import { txForErc20, txForEth } from '../utils/estimateGas';
const debug = Debug('sendStore');
const GAS_MULT_FACTOR = 1.25; // Since estimateGas is not always accurate, we add a 33% factor for buffer.
......@@ -75,9 +75,11 @@ export class SendStore {
}
if (this.tokenAddress === 'ETH') {
return this.estimateGasForEth(this.txForEth);
return this.estimateGasForEth(txForEth(this.tx));
} else {
return this.estimateGasForErc20(this.txForErc20);
return this.estimateGasForErc20(
txForErc20(this.tx, tokensStore.tokens[this.tokenAddress])
);
}
};
......@@ -113,10 +115,10 @@ export class SendStore {
send = password => {
const send$ =
this.tokenAddress === 'ETH'
? post$(this.txForEth)
? post$(txForEth(this.tx))
: this.contract.transfer$(
...this.txForErc20.args,
this.txForErc20.options
...txForErc20(this.tx).args,
txForErc20(this.tx).options
);
debug(
......@@ -138,38 +140,6 @@ export class SendStore {
});
};
/**
* This.tx is a user-friendly tx object. We convert it now as it can be
* passed to makeContract$(...).
*/
@computed
get txForErc20 () {
return {
args: [
this.tx.to,
new BigNumber(this.tx.amount).mul(
new BigNumber(10).pow(tokensStore.tokens[this.tokenAddress].decimals)
)
],
options: {
gasPrice: toWei(this.tx.gasPrice, 'shannon') // shannon == gwei
}
};
}
/**
* This.tx is a user-friendly tx object. We convert it now as it can be
* passed to post$(tx).
*/
@computed
get txForEth () {
return {
gasPrice: toWei(this.tx.gasPrice, 'shannon'), // shannon == gwei
to: this.tx.to,
value: toWei(this.tx.amount.toString())
};
}
@action
setBlockNumber = blockNumber => {
this.blockNumber = blockNumber;
......
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
// SPDX-License-Identifier: BSD-3-Clause
import BigNumber from 'bignumber.js';
import memoize from 'lodash/memoize';
import { toWei } from '@parity/api/lib/util/wei';
import Debug from './debug';
const debug = Debug('estimateGas');
const GAS_MULT_FACTOR = 1.25; // Since estimateGas is not always accurate, we add a 33% factor for buffer.
/**
* Estimate the amount of gas for our transaction.
*/
export const estimateGas = (tx, token, api) => {
if (!tx || !Object.keys(tx).length) {
return Promise.reject(new Error('Tx not set in sendStore.'));
}
if (token.address === 'ETH') {