Newer
Older
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
import BigNumber from 'bignumber.js';
import debounce from 'debounce-promise';
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 { OnChange } from 'react-final-form-listeners';
import { withProps } from 'recompose';
import { estimateGas } from '../../utils/estimateGas';
import RequireHealth from '../../RequireHealthOverlay';
import TokenBalance from '../../Tokens/TokensList/TokenBalance';
import withAccount from '../../utils/withAccount.js';
import withBalance, { withEthBalance } from '../../utils/withBalance';
import withTokens from '../../utils/withTokens';
const MAX_GAS_PRICE = 40; // In Gwei
const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei
Luke Schoen
committed
const ZERO = new BigNumber('0');
@withProps(({ match: { params: { tokenAddress } }, tokens }) => ({
token: tokens[tokenAddress]
@withBalance // Balance of current token (can be ETH)
@withEthBalance // ETH balance
@observer
class Send extends Component {
Luke Schoen
committed
isCancelled = false;
state = {
estimatedTxFee: ZERO,
showDetails: false
Luke Schoen
committed
};
componentWillUnmount () {
// Avoids encountering error `Can't call setState (or forceUpdate)
// on an unmounted component` when navigate from 'Send Ether' to 'Send THIBCoin'
this.isCancelled = true;
}
const { accountAddress, history, sendStore, token } = this.props;
history.push(`/send/${token.address}/from/${accountAddress}/signer`);
toggleDetails = () => {
const { showDetails } = this.state;
Luke Schoen
committed
!this.isCancelled && this.setState({ showDetails: !showDetails });
};
decorator = createDecorator({
field: /to|amount/, // when the value of these fields change...
updates: {
// ...set field "gas"
Luke Schoen
committed
gas: async (value, allValues) => {
const { parityStore, token } = this.props;
Luke Schoen
committed
(await !this.isCancelled) && this.setState({ estimatedTxFee: ZERO });
Luke Schoen
committed
Luke Schoen
committed
const newEstimatedTxFee = await estimateGas(
allValues,
token,
parityStore.api
);
return newEstimatedTxFee;
} else {
return null;
}
}
}
});
calculateMax = (gas, gasPrice) => {
const { token, balance } = this.props;
const gasBn = gas ? new BigNumber(gas) : new BigNumber(21000);
const gasPriceBn = new BigNumber(gasPrice);
if (token.address === 'ETH') {
output = fromWei(
toWei(balance).minus(gasBn.multipliedBy(toWei(gasPriceBn, 'shannon')))
output = output.isNegative() ? new BigNumber(0) : output;
} else {
output = balance;
}
return output;
};
recalculateMax = (args, state, { changeValue }) => {
changeValue(state, 'amount', value => {
return this.calculateMax(
state.formState.values.gas,
state.formState.values.gasPrice
);
});
};
toggleMax = () => {
this.setState({ maxSelected: !this.state.maxSelected });
const { estimatedTxFee, showDetails } = this.state;
const renderFee = () => {
return `Fee: ${estimatedTxFee
.div(10 ** 18)
Luke Schoen
committed
.toFixed(9)
.toString()} ETH (estimate * gas price)`;
};
const renderCalculation = values => {
Luke Schoen
committed
const gasPriceBn = new BigNumber(values.gasPrice.toString());
const gasLimitBn = estimatedTxFee
.div(gasPriceBn)
.div(10 ** 9)
Luke Schoen
committed
.toString();
return `Estimate amount of gas: ${gasLimitBn}`;
};
const renderTotalAmount = values => {
return `Total Amount: ${estimatedTxFee
.plus(token.address === 'ETH' ? toWei(values.amount.toString()) : 0)
.div(10 ** 18)
.toString()} ETH`;
};
const renderDetails = values => {
return `${renderCalculation(values)}\n${renderFee()}\n${renderTotalAmount(
values
)}`;
};
Luke Schoen
committed
Luke Schoen
committed
const showHideLabel = () => {
return (
<span className='details'>
<a onClick={this.toggleDetails}>↑ Hide</a>
Luke Schoen
committed
</span>
);
};
const showDetailsLabel = () => {
return (
<span className='details'>
<a onClick={this.toggleDetails}>↓ Details</a>
Luke Schoen
committed
</span>
);
};
<Link to={`/tokens/${accountAddress}`} className='icon -back'>
title={token && <h1>Send {token.name}</h1>}
<div className='window_content'>
<div className='box -padded'>
<TokenBalance
decimals={6}
drawers={[
<Form
key='txForm'
initialValues={{ from: accountAddress, gasPrice: 4, ...tx }}
onSubmit={this.handleSubmit}
validate={this.validateForm}
decorators={[this.decorator]}
mutators={{ recalculateMax: this.recalculateMax }}
render={({
handleSubmit,
valid,
validating,
values,
form: { mutators }
}) => (
<form className='send-form' onSubmit={handleSubmit}>
<fieldset className='form_fields'>
<Field
as='textarea'
className='-sm'
label='To'
name='to'
placeholder='0x...'
required
render={FetherForm.Field}
/>
<Field
className='form_field_amount'
formNoValidate
label='Amount'
name='amount'
placeholder='0.00'
render={FetherForm.Field}
required
<button
type='button'
className={
this.state.maxSelected
? 'button -tiny active max'
: 'button -tiny max'
<Field
centerText={`${values.gasPrice} GWEI`}
className='-range'
label='Transaction Speed'
max={MAX_GAS_PRICE}
min={MIN_GAS_PRICE}
name='gasPrice'
render={FetherForm.Slider}
required
step={0.5}
type='range' // In Gwei
/>
Luke Schoen
committed
{estimatedTxFee &&
!estimatedTxFee.isZero() && (
<div>
Luke Schoen
committed
{valid && !isNaN(values.amount) ? (
<div>
Luke Schoen
committed
<div
className={`form_details_buttons ${
showDetails ? 'hide' : 'show'
}`}
>
{showDetails
? showHideLabel()
: showDetailsLabel()}
</div>
<div
className={`form_details_text ${
showDetails ? 'hide' : 'show'
}`}
hidden={!showDetails}
>
<Field
as='textarea'
className='-sm-details'
disabled
label='Transaction Details (Estimate)'
name='txFeeEstimate'
render={FetherForm.Field}
placeholder={renderDetails(values)}
/>
</div>
</div>
Luke Schoen
committed
) : null}
Luke Schoen
committed
</div>
)}
<OnChange name='gasPrice'>
{(value, previous) => {
if (this.state.maxSelected) {
<span>
<h3>WARNING:</h3>
<p>
The sender and receiver addresses are the same.
</p>
</span>
</fieldset>
<nav className='form-nav'>
<button
disabled={!valid || validating}
className='button'
>
{validating ? 'Checking...' : 'Send'}
</button>
</nav>
</form>
)}
/>
]}
onClick={null} // To disable cursor:pointer on card // TODO Can this be done better?
token={token}
/>
</div>
Luke Schoen
committed
if (!values) {
return;
}
if (!values.amount) {
}
const amountBn = new BigNumber(values.amount.toString());
if (amountBn.isNaN()) {
return { amount: 'Please enter a valid amount' };
} else if (amountBn.isZero()) {
if (this.state.maxSelected) {
return { amount: 'ETH balance too low to pay for gas.' };
}
return { amount: 'Please enter a non-zero amount' };
} else if (amountBn.isNegative()) {
Luke Schoen
committed
return { amount: 'Please enter a positive amount' };
} else if (token.address === 'ETH' && toWei(values.amount).lt(1)) {
Luke Schoen
committed
return { amount: 'Please enter at least 1 Wei' };
} else if (amountBn.dp() > token.decimals) {
Luke Schoen
committed
return {
amount: `Please enter a ${token.name} value of less than ${
Luke Schoen
committed
token.decimals
} decimal places`
};
} else if (balance && balance.lt(amountBn)) {
return { amount: `You don't have enough ${token.symbol} balance` };
} else if (!values.to || !isAddress(values.to)) {
return { to: 'Please enter a valid Ethereum address' };
Luke Schoen
committed
} else if (values.to === '0x0000000000000000000000000000000000000000') {
return {
to: `You are not permitted to send ${
token.name
} to the zero account (0x0)`
};
/**
* Estimate gas amount, and validate that the user has enough balance to make
* the tx.
*/
Luke Schoen
committed
validateForm = debounce(async values => {
Luke Schoen
committed
if (!values) {
return;
}
// If the gas hasn't been calculated yet, then we don't show any errors,
// just wait a bit more
if (!values.gas) {
return;
if (!ethBalance || isNaN(values.gas)) {
throw new Error('No "ethBalance" or "gas" value.');
Luke Schoen
committed
if (!values.gas) {
return;
}
const estimatedTxFee = values.gas.multipliedBy(
toWei(values.gasPrice, 'shannon')
);
Luke Schoen
committed
// Verify that `gas + (eth amount if sending eth) <= ethBalance`
Luke Schoen
committed
estimatedTxFee
.plus(token.address === 'ETH' ? toWei(values.amount) : 0)
Luke Schoen
committed
!this.isCancelled &&
this.setState({ estimatedTxFee: new BigNumber('0') });
? { amount: 'ETH balance too low to pay for gas' }
: { amount: "You don't have enough ETH balance" };
Luke Schoen
committed
} else {
Luke Schoen
committed
// `await` prevents error `Uncaught TypeError: Cannot read property 'resolve' of null at flush`
(await !this.isCancelled) && this.setState({ estimatedTxFee });
amount: 'Failed estimating balance, please try again'