TxForm.js 7.16 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

import React, { Component } from 'react';
import debounce from 'lodash/debounce';
import { FormField, 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';
Amaury Martiny's avatar
Amaury Martiny committed
import { Link } from 'react-router-dom';
import ReactTooltip from 'react-tooltip';
import TokenBalance from '../../Tokens/TokensList/TokenBalance';
import withBalance from '../../utils/withBalance';
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('sendStore', 'tokensStore')
@withBalance(
  ({ sendStore: { tokenAddress }, tokensStore }) =>
    tokensStore.tokens[tokenAddress]
)
@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;

Amaury Martiny's avatar
Amaury Martiny committed
    // 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);
Amaury Martiny's avatar
Amaury Martiny committed
  handleChangeGasPrice = ({ target: { value } }) =>
    this.setState({ gasPrice: value }, this.handleEstimateGasPrice);
Amaury Martiny's avatar
Amaury Martiny committed
  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);
Amaury Martiny's avatar
Amaury Martiny committed
  };
  handleMax = () =>
    this.setState(
      { amount: this.state.maxAmount },
      this.handleEstimateGasPrice
    );
Amaury Martiny's avatar
Amaury Martiny committed
  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;
  };

  render () {
    const {
Amaury Martiny's avatar
Amaury Martiny committed
      sendStore: { tokenAddress },
      tokensStore
    } = this.props;
    const { amount, estimating, gasPrice, maxAmount, to } = this.state;
Amaury Martiny's avatar
Amaury Martiny committed
    const token = tokensStore.tokens[tokenAddress];
    const error = this.hasError();
Amaury Martiny's avatar
Amaury Martiny committed
      <div>
        <Header
          left={
Amaury Martiny's avatar
Amaury Martiny committed
            <Link to='/tokens' className='icon -close'>
              Close
            </Link>
          }
          title={<h1>Send {token.name}</h1>}
        />

Amaury Martiny's avatar
Amaury Martiny committed
        <div className='window_content'>
          <div className='box -padded'>
            <TokenBalance
              decimals={6}
              drawers={[
                <form
                  className='send-form'
                  key='txForm'
                  onSubmit={this.handleSubmit}
                >
Amaury Martiny's avatar
Amaury Martiny committed
                  <fieldset className='form_fields'>
                    <FormField
                      input={
                        <div>
                          <input
                            className='form_field_amount'
                            formNoValidate
                            max={maxAmount}
                            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
                          className='-sm'
                          onChange={this.handleChangeTo}
                          placeholder='0x...'
                          required
                          type='text'
                          value={to}
                        />
                      }
                      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'
                    />
Amaury Martiny's avatar
Amaury Martiny committed
                  </fieldset>
                  <nav className='form-nav'>
                    <span data-tip={error || ''}>
                      <button disabled={error || estimating} className='button'>
                        {estimating ? 'Checking...' : 'Send'}
                      </button>
                    </span>
Amaury Martiny's avatar
Amaury Martiny committed
                  </nav>
                </form>
              ]}
              onClick={null}
              token={token}
            />
Amaury Martiny's avatar
Amaury Martiny committed
          </div>
        <ReactTooltip
          effect='solid'
          event='mouseover'
          eventOff='mouseout'
          place='top'
        />
      </div>
    );
  }
}

export default Send;