sendStore.js 5.15 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 abi from '@parity/shared/lib/contracts/abi/eip20';
import { action, computed, observable } from 'mobx';
import { BigNumber } from 'bignumber.js';
Amaury Martiny's avatar
Amaury Martiny committed
import { blockNumber$, makeContract$, post$ } from '@parity/light.js';
import memoize from 'lodash/memoize';
Amaury Martiny's avatar
Amaury Martiny committed
import noop from 'lodash/noop';
Amaury Martiny's avatar
Amaury Martiny committed
import { toWei } from '@parity/api/lib/util/wei';
Amaury Martiny's avatar
Amaury Martiny committed
import Debug from '../utils/debug';
import parityStore from './parityStore';
import tokensStore from './tokensStore';

Amaury Martiny's avatar
Amaury Martiny committed
const debug = Debug('sendStore');
const GAS_MULT_FACTOR = 1.25; // Since estimateGas is not always accurate, we add a 33% factor for buffer.
const DEFAULT_GAS = new BigNumber(21000 * GAS_MULT_FACTOR); // Default gas amount
Amaury Martiny's avatar
Amaury Martiny committed

Amaury Martiny's avatar
Amaury Martiny committed
export class SendStore {
Amaury Martiny's avatar
Amaury Martiny committed
  @observable blockNumber; // Current block number, used to calculate tx confirmations.
Amaury Martiny's avatar
Amaury Martiny committed
  @observable estimated = DEFAULT_GAS; // Estimated gas amount for this transaction.
Amaury Martiny's avatar
Amaury Martiny committed
  @observable tokenAddress; // 'ETH', or the token contract address
  tx = {}; // The actual tx we are sending. No need to be observable.
Amaury Martiny's avatar
Amaury Martiny committed
  @observable txStatus; // Status of the tx, see wiki for details.
Amaury Martiny's avatar
Amaury Martiny committed
  acceptRequest = (requestId, password) => {
    // Since we accepted this request, we also start to listen to blockNumber,
    // to calculate the number of confirmations
Amaury Martiny's avatar
Amaury Martiny committed
    this.subscription = blockNumber$().subscribe(this.setBlockNumber);
Amaury Martiny's avatar
Amaury Martiny committed
    return parityStore.api.signer.confirmRequest(requestId, null, password);
Amaury Martiny's avatar
Amaury Martiny committed
  /**
   * Back to defaults.
   */
  clear = () => {
Amaury Martiny's avatar
Amaury Martiny committed
    this.tx = {};
Amaury Martiny's avatar
Amaury Martiny committed
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  };

Amaury Martiny's avatar
Amaury Martiny committed
  /**
   * Get the number of confirmations our transaction has.
   */
  @computed
  get confirmations () {
Amaury Martiny's avatar
Amaury Martiny committed
    if (!this.txStatus || !this.txStatus.confirmed) {
      return -1;
    }
    return this.blockNumber - +this.txStatus.confirmed.blockNumber;
  }

Amaury Martiny's avatar
Amaury Martiny committed
  /**
Amaury Martiny's avatar
Amaury Martiny committed
   * If it's a token, then return the makeContract$ object.
   */
  @computed
  get contract () {
    if (this.tokenAddress === 'ETH') {
      return null;
    }
    return makeContract$(this.tokenAddress, abi);
  }

  /**
   * Estimate the amount of gas for our transaction.
Amaury Martiny's avatar
Amaury Martiny committed
   */
Amaury Martiny's avatar
Amaury Martiny committed
  estimateGas = () => {
Amaury Martiny's avatar
Amaury Martiny committed
    if (!this.tx || !Object.keys(this.tx).length) {
      return Promise.reject(new Error('Tx not set in sendStore.'));
Amaury Martiny's avatar
Amaury Martiny committed
    if (this.tokenAddress === 'ETH') {
      return this.estimateGasForEth(this.txForEth);
Amaury Martiny's avatar
Amaury Martiny committed
    } else {
      return this.estimateGasForErc20(this.txForErc20);
Amaury Martiny's avatar
Amaury Martiny committed
    }
  };

  /**
   * Estimate gas to transfer in ERC20 contract. Expensive function, so we
   * memoize it.
   */
  estimateGasForErc20 = memoize(
    txForErc20 =>
      this.contract.contractObject.instance.transfer
        .estimateGas(txForErc20.options, txForErc20.args)
        .then(this.setEstimated)
        .catch(noop),
    JSON.stringify
  );
  /**
   * Estimate gas to transfer to an ETH address. Expensive function, so we
   * memoize it.
   */
  estimateGasForEth = memoize(
    txForEth =>
      parityStore.api.eth
        .estimateGas(txForEth)
        .then(this.setEstimated)
        .catch(noop),
    JSON.stringify
  );
Amaury Martiny's avatar
Amaury Martiny committed
  /**
   * Create a transaction.
   */
Amaury Martiny's avatar
Amaury Martiny committed
  send = password => {
Amaury Martiny's avatar
Amaury Martiny committed
    const send$ =
      this.tokenAddress === 'ETH'
        ? post$(this.txForEth)
        : this.contract.transfer$(
          ...this.txForErc20.args,
          this.txForErc20.options
        );

Amaury Martiny's avatar
Amaury Martiny committed
    debug(
      'Sending tx.',
      this.tokenAddress === 'ETH' ? this.txForEth : this.txForErc20
    );

Amaury Martiny's avatar
Amaury Martiny committed
    return new Promise((resolve, reject) => {
      send$.subscribe(txStatus => {
        // When we arrive to the `requested` stage, we accept the request
        if (txStatus.requested) {
          this.acceptRequest(txStatus.requested, password)
            .then(resolve)
            .catch(reject);
        }
        this.setTxStatus(txStatus);
Amaury Martiny's avatar
Amaury Martiny committed
        debug('Tx status updated.', txStatus);
Amaury Martiny's avatar
Amaury Martiny committed
      });
Amaury Martiny's avatar
Amaury Martiny committed
  /**
   * This.tx is a user-friendly tx object. We convert it now as it can be
   * passed to makeContract$(...).
   */
  @computed
  get txForErc20 () {
    return {
        new BigNumber(this.tx.amount).mul(
Amaury Martiny's avatar
Amaury Martiny committed
          new BigNumber(10).pow(tokensStore.tokens[this.tokenAddress].decimals)
Amaury Martiny's avatar
Amaury Martiny committed
      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())
    };
  }
  setBlockNumber = blockNumber => {
    this.blockNumber = blockNumber;
  setEstimated = estimated => {
    this.estimated = estimated.mul(GAS_MULT_FACTOR);
    debug('Estimated gas,', +estimated, ', with buffer,', +this.estimated);
  setTokenAddress = tokenAddress => {
    this.tokenAddress = tokenAddress;
Amaury Martiny's avatar
Amaury Martiny committed
  @action
  setTx = tx => {
    this.tx = tx;
Amaury Martiny's avatar
Amaury Martiny committed
  };

  @action
  setTxStatus = txStatus => {
    this.txStatus = txStatus;
  };
}

export default new SendStore();