sendStore.js 5.38 KiB
Newer Older
// Copyright 2015-2018 Parity Technologies (UK) Ltd.
// This file is part of Parity.
//
// SPDX-License-Identifier: MIT

Amaury Martiny's avatar
Amaury Martiny committed
import abi from '@parity/shared/lib/contracts/abi/eip20';
import { action, computed, observable } from 'mobx';
Amaury Martiny's avatar
Amaury Martiny committed
import { blockNumber$, makeContract$, post$ } from '@parity/light.js';
Amaury Martiny's avatar
Amaury Martiny committed
import { isAddress } from '@parity/api/lib/util/address';
import noop from 'lodash/noop';
Amaury Martiny's avatar
Amaury Martiny committed
import { toWei } from '@parity/api/lib/util/wei';

import parityStore from './parityStore';
import tokensStore from './tokensStore';

Amaury Martiny's avatar
Amaury Martiny committed
const DEFAULT_GAS = 21000; // Default gas amount to show

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
  @observable
  tx = {
    amount: 0.01, // In Ether or in token
    gasPrice: 4, // in Gwei
    to: '0x00Ae02834e91810B223E54ce3f9B7875258a1747'
  }; // 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.

  constructor () {
    this.api = parityStore.api;
  }

  acceptRequest = password => {
Amaury Martiny's avatar
Amaury Martiny committed
    // Avoid calling this method from a random place
    if (!this.requestId) {
      return Promise.reject(
        new Error('The requestId has not been generated yet.')
      );
    }

    // 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);
    return this.api.signer.confirmRequest(this.requestId, null, password);
  };

Amaury Martiny's avatar
Amaury Martiny committed
  /**
   * Back to defaults.
   */
  clear = () => {
    this.tx.amount = 0;
    this.tx.to = '';
    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 = () => {
    if (!this.isTxValid) {
      return Promise.resolve(DEFAULT_GAS);
    }

Amaury Martiny's avatar
Amaury Martiny committed
    if (this.tokenAddress === 'ETH') {
      return this.api.eth
        .estimateGas(this.txForEth)
        .then(this.setEstimated)
        .catch(noop);
    } else {
      return this.contract.contractObject.instance.transfer
        .estimateGas(this.txForErc20.options, this.txForErc20.args)
        .then(this.setEstimated)
        .catch(noop);
  @computed
  get isTxValid () {
Amaury Martiny's avatar
Amaury Martiny committed
    if (
      !this.tx || // There should be a tx
      !isAddress(this.tx.to) || // The address should be okay
      isNaN(this.tx.amount) ||
      isNaN(this.tx.gasPrice)
    ) {
Amaury Martiny's avatar
Amaury Martiny committed
  /**
   * Create a transaction.
   */
Amaury Martiny's avatar
Amaury Martiny committed
  send = () => {
    if (!this.isTxValid) {
Amaury Martiny's avatar
Amaury Martiny committed
      console.error('Transaction is invalid.', this.tx);
Amaury Martiny's avatar
Amaury Martiny committed
    const send$ =
      this.tokenAddress === 'ETH'
        ? post$(this.txForEth)
        : this.contract.transfer$(
          ...this.txForErc20.args,
          this.txForErc20.options
        );

    send$.subscribe(txStatus => {
      if (txStatus.requested) {
        this.requestId = txStatus.requested;
      }
      this.setTxStatus(txStatus);
    });
  };

Amaury Martiny's avatar
Amaury Martiny committed
  rejectRequest = () => {
Amaury Martiny's avatar
Amaury Martiny committed
    // Avoid calling this method from a random place
    if (!this.requestId) {
      return Promise.reject(
        new Error('The requestId has not been generated yet.')
      );
    }
    return this.api.signer.rejectRequest(this.requestId);
  };

Amaury Martiny's avatar
Amaury Martiny committed
  @computed
  get token () {
    return tokensStore.tokens[this.tokenAddress];
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 post$(tx).
   */
  @computed
  get txForEth () {
    return {
      gasPrice: toWei(this.tx.gasPrice, 'shannon'), // shannon == gwei
      to: this.tx.to,
      value: toWei(this.tx.amount.toString())
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 {
      args: [
        this.tx.to,
        (this.tx.amount * 10 ** this.token.decimals).toString()
      ],
Amaury Martiny's avatar
Amaury Martiny committed
      options: {
        gasPrice: toWei(this.tx.gasPrice, 'shannon') // shannon == gwei
      }
    };
  }

  @action
Amaury Martiny's avatar
Amaury Martiny committed
  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
  setTxAmount = amount => {
    this.tx.amount = amount;
  };

  @action
  setTxGasPrice = gasPrice => {
    this.tx.gasPrice = gasPrice;
  };

  @action
  setTxTo = to => {
    this.tx.to = to;
  };

  @action
  setBlockNumber = blockNumber => {
    this.blockNumber = blockNumber;
  };

Amaury Martiny's avatar
Amaury Martiny committed
  @action
  setEstimated = estimated => {
    // Since estimateGas is not always accurate, we add a 120% factor for buffer.
    const GAS_MULT_FACTOR = 1.2;

    this.estimated = +estimated * GAS_MULT_FACTOR; // Don't store as BigNumber
  };

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

export default new SendStore();