TxForm.js 7.47 KiB
Newer Older
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
Amaury Martiny's avatar
Amaury Martiny committed
// SPDX-License-Identifier: BSD-3-Clause
Amaury Martiny's avatar
Amaury Martiny committed
import React, { Component } from 'react';
import debounce from 'debounce-promise';
import { Field, Form } from 'react-final-form';
import { Form as FetherForm, Header } from 'fether-ui';
import { inject, observer } from 'mobx-react';
import { isAddress } from '@parity/api/lib/util/address';
import { Link } from 'react-router-dom';
import { toWei } from '@parity/api/lib/util/wei';
import { withProps } from 'recompose';

import { estimateGas } from '../../utils/estimateGas';
import RequireHealth from '../../RequireHealthOverlay';
Amaury Martiny's avatar
Amaury Martiny committed
import TokenBalance from '../../Tokens/TokensList/TokenBalance';
import withAccount from '../../utils/withAccount.js';
import withBalance, { withEthBalance } from '../../utils/withBalance';
Amaury Martiny's avatar
Amaury Martiny committed
import withTokens from '../../utils/withTokens';
Thibaut Sardan's avatar
Thibaut Sardan committed
import createDecorator from 'final-form-calculate';
Amaury Martiny's avatar
Amaury Martiny committed
const MAX_GAS_PRICE = 40; // In Gwei
const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei

Amaury Martiny's avatar
Amaury Martiny committed
@inject('parityStore', 'sendStore')
Amaury Martiny's avatar
Amaury Martiny committed
@withTokens
Axel Chalon's avatar
Axel Chalon committed
@withProps(({ match: { params: { tokenAddress } }, tokens }) => ({
  token: tokens[tokenAddress]
@withAccount
@withBalance // Balance of current token (can be ETH)
@withEthBalance // ETH balance
@observer
class Send extends Component {
  handleSubmit = values => {
Axel Chalon's avatar
Axel Chalon committed
    const { accountAddress, history, sendStore, token } = this.props;
    sendStore.setTx(values);
    history.push(`/send/${token.address}/from/${accountAddress}/signer`);
Amaury Martiny's avatar
Amaury Martiny committed
  render () {
      accountAddress,
      sendStore: { tx },
      token
    } = this.props;

Thibaut Sardan's avatar
Thibaut Sardan committed
    const decorator = createDecorator(
      // Calculations:
      {
Thibaut Sardan's avatar
Thibaut Sardan committed
        field: /to|amount/, // when the value of these fields change...
Thibaut Sardan's avatar
Thibaut Sardan committed
        updates: {
          // ...set field "gas"
Thibaut Sardan's avatar
Thibaut Sardan committed
          gas: (value, allValues) => {
Thibaut Sardan's avatar
Thibaut Sardan committed
            const { parityStore, token } = this.props;
            if (this.preValidate(allValues, false)) {
              // this means amount has errors
Thibaut Sardan's avatar
Thibaut Sardan committed
              return estimateGas(allValues, token, parityStore.api);
Thibaut Sardan's avatar
Thibaut Sardan committed
            } else {
              return null;
            }
Amaury Martiny's avatar
Amaury Martiny committed
      <div>
        <Header
          left={
Thibaut Sardan's avatar
Thibaut Sardan committed
            <Link to={`/tokens/${accountAddress}`} className='icon -back'>
Amaury Martiny's avatar
Amaury Martiny committed
              Close
            </Link>
          title={token && <h1>Send {token.name}</h1>}
        <RequireHealth require='sync'>
          <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}
Thibaut Sardan's avatar
Thibaut Sardan committed
                    decorators={[decorator]}
                    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='0.00'
                            render={FetherForm.Field}
                            required
                            type='number' // In ETH or coin
                          />
                          <Field
                            as='textarea'
                            className='-sm'
                            label='To'
                            name='to'
                            placeholder='0x...'
                            required
                            render={FetherForm.Field}
                          />
                          <Field
                            centerText={`${values.gasPrice} GWEI`}
                            className='-range'
                            label='Transaction Fee'
                            leftText='Slow'
                            max={MAX_GAS_PRICE}
                            min={MIN_GAS_PRICE}
                            name='gasPrice'
                            render={FetherForm.Slider}
                            required
                            rightText='Fast'
                            step={0.5}
                            type='range' // In Gwei
                          />
Thibaut Sardan's avatar
Thibaut Sardan committed

Thibaut Sardan's avatar
Thibaut Sardan committed
                          <div className='hidden'>
Thibaut Sardan's avatar
Thibaut Sardan committed
                            <Field name='gas' render={FetherForm.Field} />
Thibaut Sardan's avatar
Thibaut Sardan committed
                          </div>
Thibaut Sardan's avatar
Thibaut Sardan committed

YJ's avatar
YJ committed
                          {values.to === values.from && (
YJ's avatar
YJ committed
                            <span>
                              <h3>WARNING:</h3>
                              <p>
                                The sender and receiver addresses are the same.
                              </p>
                            </span>
YJ's avatar
YJ committed
                          )}
                        </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>
Amaury Martiny's avatar
Amaury Martiny committed
          </div>
        </RequireHealth>
Thibaut Sardan's avatar
Thibaut Sardan committed
  preValidate = (values, withError) => {
    const { balance, token } = this.props;
Thibaut Sardan's avatar
Thibaut Sardan committed
    const amount = +values.amount;

    if (!amount || isNaN(amount)) {
      return withError ? { amount: 'Please enter a valid amount' } : false;
    } else if (amount < 0) {
      return withError ? { amount: 'Please enter a positive amount ' } : false;
    } else if (balance && balance.lt(amount)) {
      return withError
        ? { amount: `You don't have enough ${token.symbol} balance` }
        : false;
    }
Thibaut Sardan's avatar
Thibaut Sardan committed
    return true;
  /**
   * Estimate gas amount, and validate that the user has enough balance to make
   * the tx.
   */
  validateAmount = debounce(async values => {
Thibaut Sardan's avatar
Thibaut Sardan committed
      const { ethBalance, token } = this.props;
Thibaut Sardan's avatar
Thibaut Sardan committed
      const preValidation = this.preValidate(values, true);
      // preValidate return an error if a field isn't valid
      if (preValidation.amount) {
        return preValidation;
Thibaut Sardan's avatar
Thibaut Sardan committed
      if (!ethBalance || isNaN(values.gas)) {
        throw new Error('No "ethBalance" or "gas" value.');
Amaury Martiny's avatar
Amaury Martiny committed
      // Verify that `gas + (eth amount if sending eth) <= ethBalance`
Thibaut Sardan's avatar
Thibaut Sardan committed
        values.gas
Amaury Martiny's avatar
Amaury Martiny committed
          .mul(toWei(values.gasPrice, 'shannon'))
          .plus(token.address === 'ETH' ? toWei(values.amount) : 0)
Amaury Martiny's avatar
Amaury Martiny committed
          .gt(toWei(ethBalance))
Amaury Martiny's avatar
Amaury Martiny committed
        return { amount: "You don't have enough ETH balance" };
Thibaut Sardan's avatar
Thibaut Sardan committed
      console.log(err);
Amaury Martiny's avatar
Amaury Martiny committed
        amount: 'Failed estimating balance, please try again'
      };
    }
  }, 1000);

  validateForm = values => {
    const errors = {};
YJ's avatar
YJ committed

    if (!isAddress(values.to)) {
Amaury Martiny's avatar
Amaury Martiny committed
      errors.to = 'Please enter a valid Ethereum address';
    return Object.keys(errors).length ? errors : this.validateAmount(values);
}

export default Send;