AccountImportOptions.js 8.12 KiB
Newer Older
// Copyright 2015-2019 Parity Technologies (UK) Ltd.
YJ's avatar
YJ committed
// This file is part of Parity.
//
// SPDX-License-Identifier: BSD-3-Clause

YJ's avatar
YJ committed
import React, { Component } from 'react';
import { chainId$, chainName$ } from '@parity/light.js';
import light from '@parity/light.js-react';
YJ's avatar
YJ committed
import { inject, observer } from 'mobx-react';
import { addressShort, Card, Form as FetherForm } from 'fether-ui';
YJ's avatar
YJ committed

import Scanner from '../../../Scanner';
Axel Chalon's avatar
Axel Chalon committed
import withAccountsInfo from '../../../utils/withAccountsInfo';
import withHealth from '../../../utils/withHealth';
import i18n, { packageNS } from '../../../i18n';
Axel Chalon's avatar
Axel Chalon committed
@withAccountsInfo
YJ's avatar
YJ committed
@inject('createAccountStore')
@light({
  chainId: () => chainId$(),
  /**
   * It is not necessary to check the health status here before
   * calling chainId RPC using light.js like we do in Health.js since
   * the AccountImportOptions.js page may only be accessed through
   * navigation inside the API, after the API is set.
   *
   * Reference: https://github.com/paritytech/fether/pull/483#discussion_r271303462
   */
  chainName: () => chainName$()
})
YJ's avatar
YJ committed
@observer
class AccountImportOptions extends Component {
  state = {
YJ's avatar
YJ committed
    isLoading: false,
    phrase: '',
    importingFromSigner: false
YJ's avatar
YJ committed
  };

  handleNextStep = async () => {
    const {
      history,
YJ's avatar
YJ committed
    } = this.props;
    const currentStep = pathname.slice(-1);
    history.push(`/accounts/new/${+currentStep + 1}`);
  };

  handlePhraseChange = ({ target: { value: phrase } }) => {
    this.setState({ phrase });
YJ's avatar
YJ committed
  };

  handleSubmitPhrase = async () => {
    const phrase = this.state.phrase.trim();
YJ's avatar
YJ committed
    const {
      createAccountStore: { setPhrase }
YJ's avatar
YJ committed
    } = this.props;

    this.setState({ isLoading: true, phrase });
YJ's avatar
YJ committed
    try {
      if (this.accountAlreadyExists(createAccountStore.address)) {
        error: i18n.t(
          `${packageNS}:account.import.phrase.error_msg_submit_phrase`
        )
  handleChangeFile = async jsonString => {
    const {
      createAccountStore: { setJsonString }
    } = this.props;
YJ's avatar
YJ committed

    this.setState({
      error: '',
      isLoading: true
    });
    try {
      await setJsonString(jsonString);
      if (this.accountAlreadyExists(createAccountStore.address)) {
YJ's avatar
YJ committed
    } catch (error) {
      this.setState({
        error: i18n.t(`${packageNS}:account.import.error_msg_change_json_file`)
YJ's avatar
YJ committed
    }
  };

  /**
   * The `chainId$` and `chainName$` from light.js corresponds to `chainID` in the
   * Genesis configs contained in: paritytech/parity-ethereum/ethcore/res/ethereum
   * and was introduced in EIP-155 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
   * to prevent replay attacks between `foundation` and `classic` chains, which both have
   * `networkID` of `1`.
   */
  handleSignerImported = async ({ address, chainId: signerChainId }) => {
      createAccountStore: { importFromSigner }
    } = this.props;

      this.setState({
        error: i18n.t(
          `${packageNS}:account.import.signer.error_msg_signer_imported`
        )
      });
    if (!currentChainIdBN.eq(signerChainId)) {
      console.error(
        `Parity Signer account chainId ${signerChainId} must match current chainId ${currentChainIdBN.valueOf()} (${chainName}).`
      );

      this.setState({
        error: i18n.t(
          `${packageNS}:account.import.signer.error_msg_signer_imported_network_mismatch`,
          { chain_name: chainName }
        )
      });

      return;
    }
    if (this.accountAlreadyExists(address, signerChainId)) {
Axel Chalon's avatar
Axel Chalon committed
      return;
    }

    await importFromSigner({ address, signerChainId });

    this.handleNextStep();
  };

  handleSignerImport = () => {
    this.setState({
      importingFromSigner: true
    });
Axel Chalon's avatar
Axel Chalon committed

  accountAlreadyExists = (addressForImport, signerChainId) => {
Axel Chalon's avatar
Axel Chalon committed
    const { accountsInfo } = this.props;
    const isExistingAddress = Object.keys(accountsInfo).some(
Axel Chalon's avatar
Axel Chalon committed
      key =>
        key.toLowerCase() === addressForImport.toLowerCase() &&
        (!accountsInfo[key].chainId ||
          !signerChainId ||
          accountsInfo[key].chainId === signerChainId)
        error: i18n.t(
          `${packageNS}:account.import.error_msg_existing_address`,
          {
            address: addressShort(addressForImport)
          }
        )
YJ's avatar
YJ committed
  render () {
    const {
      history,
      location: { pathname }
    } = this.props;
    const { error, importingFromSigner, phrase } = this.state;
YJ's avatar
YJ committed
    const currentStep = pathname.slice(-1);

YJ's avatar
YJ committed
    const jsonCard = (
      <Card>
        <div key='createAccount'>
          <div className='text -centered'>
            <p>
              {i18n.t(
                `${packageNS}:account.import.json.label_msg_recover_json`
              )}
            </p>

            <FetherForm.InputFile
              i18n={i18n}
              label={i18n.t(
                `${packageNS}:account.import.json.label_recover_json`
              )}
              onChangeFile={this.handleChangeFile}
              required
            />
          </div>
YJ's avatar
YJ committed
        </div>
      </Card>
    );

    const signerCard = (
      <Card>
        <div key='createAccount'>
          <div className='text -centered'>
            <p>
              {i18n.t(
                `${packageNS}:account.import.signer.label_msg_recover_signer`
              )}
            </p>

            {importingFromSigner ? (
              <Scanner
                onScan={this.handleSignerImported}
                label={i18n.t(
                  `${packageNS}:account.import.signer.label_msg_recover_signer_scan`
                )}
              />
            ) : (
              <button
                className='button -footer'
                onClick={this.handleSignerImport}
              >
                {i18n.t(
                  `${packageNS}:account.import.signer.label_button_recover_signer_scan`
                )}
              </button>
            )}
          </div>
        </div>
      </Card>
YJ's avatar
YJ committed
    );
YJ's avatar
YJ committed

YJ's avatar
YJ committed
    const phraseCard = (
      <Card>
        <div key='importBackup'>
          <div className='text -centered'>
            <p>
              {i18n.t(
                `${packageNS}:account.import.phrase.label_msg_recover_phrase`
              )}
            </p>
            <FetherForm.Field
              as='textarea'
              label={i18n.t(
                `${packageNS}:account.import.phrase.label_recover_phrase`
              )}
              onChange={this.handlePhraseChange}
              required
              phrase={phrase}
            />

            {this.renderButton()}
          </div>
YJ's avatar
YJ committed
        </div>
YJ's avatar
YJ committed
    );
YJ's avatar
YJ committed

    const spacer = <div style={{ height: '0.5rem' }} />;

YJ's avatar
YJ committed
    return (
      <div className='center-md'>
        {!importingFromSigner && jsonCard}
        {spacer}
        {signerCard}
        {spacer}
        {!importingFromSigner && phraseCard}
        <p className='error-import-account'>{error}</p>
        {currentStep > 1 && (
          <nav className='form-nav -space-around'>
            <button className='button -back' onClick={history.goBack}>
              {i18n.t(`${packageNS}:navigation.back`)}
            </button>
          </nav>
        )}
      </div>
YJ's avatar
YJ committed
    );
  }

  renderButton = () => {
    const { isLoading, json, phrase } = this.state;

YJ's avatar
YJ committed
    // If we are importing an existing account, the button goes to the next step
    return (
      <button
        className='button'
        disabled={(!json && !phrase) || isLoading}
        onClick={this.handleSubmitPhrase}
YJ's avatar
YJ committed
      >
        {i18n.t(`${packageNS}:navigation.next`)}
YJ's avatar
YJ committed
      </button>
    );
  };
}

export default AccountImportOptions;