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
@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
state = {
showDetails: false
Luke Schoen
committed
};
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
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;
};
estimatedTxFee = values =>
values.gas.multipliedBy(toWei(values.gasPrice, 'shannon'));
handleSubmit = values => {
const { accountAddress, history, sendStore, token } = this.props;
sendStore.setTx(values);
history.push(`/send/${token.address}/from/${accountAddress}/signer`);
};
isEstimatedTxFee = values => {
if (!values.gas || !values.gasPrice) {
return false;
}
return this.estimatedTxFee(values) && !this.estimatedTxFee(values).isZero();
};
recalculateMax = (args, state, { changeValue }) => {
changeValue(state, 'amount', value => {
return this.calculateMax(
state.formState.values.gas,
state.formState.values.gasPrice
);
});
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
renderCalculation = values => {
const gasPriceBn = new BigNumber(values.gasPrice.toString());
const gasLimitBn = this.estimatedTxFee(values)
.div(gasPriceBn)
.div(10 ** 9)
.toFixed(0)
.toString();
return `Estimate amount of gas: ${gasLimitBn}`;
};
renderDetails = values => {
return `${this.renderCalculation(values)}\n${this.renderFee(
values
)}\n${this.renderTotalAmount(values)}`;
};
renderFee = values => {
return `Fee: ${this.estimatedTxFee(values)
.div(10 ** 18)
.toFixed(9)
.toString()} ETH (estimate * gas price)`;
};
renderTotalAmount = values => {
const { token } = this.props;
return `Total Amount: ${this.estimatedTxFee(values)
.plus(token.address === 'ETH' ? toWei(values.amount.toString()) : 0)
.div(10 ** 18)
.toFixed(10)
.toString()} ETH`;
};
showDetailsLabel = () => {
return (
<span className='details'>
<a onClick={this.toggleDetails}>↓ Details</a>
</span>
);
};
showHideLabel = () => {
return (
<span className='details'>
<a onClick={this.toggleDetails}>↑ Hide</a>
</span>
);
};
toggleDetails = () => {
const { showDetails } = this.state;
this.setState({ showDetails: !showDetails });
};
toggleMax = () => {
this.setState({ maxSelected: !this.state.maxSelected });
const { showDetails } = this.state;
Luke Schoen
committed
<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
/>
{this.isEstimatedTxFee(values) && (
Luke Schoen
committed
<div>
Luke Schoen
committed
{valid && !isNaN(values.amount) ? (
<div>
Luke Schoen
committed
<div
className={`form_details_buttons ${
showDetails ? 'hide' : 'show'
}`}
>
{showDetails
? this.showHideLabel()
: this.showDetailsLabel()}
Luke Schoen
committed
</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={this.renderDetails(values)}
Luke Schoen
committed
/>
</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.
*/
validateForm = debounce(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 (!this.isEstimatedTxFee(values)) {
if (!ethBalance || isNaN(values.gas)) {
throw new Error('No "ethBalance" or "gas" value.');
// Verify that `gas + (eth amount if sending eth) <= ethBalance`
this.estimatedTxFee(values)
.plus(token.address === 'ETH' ? toWei(values.amount) : 0)
? { amount: 'ETH balance too low to pay for gas' }
: { amount: "You don't have enough ETH balance" };
Luke Schoen
committed
} else {
amount: 'Failed estimating balance, please try again'