import { colors, constants as sharedConstants } from '@0xproject/react-shared'; import { BigNumber, logUtils } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as _ from 'lodash'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table'; import TextField from 'material-ui/TextField'; import * as React from 'react'; import ReactTooltip = require('react-tooltip'); import { Blockchain } from 'ts/blockchain'; import { NetworkDropDown } from 'ts/components/dropdowns/network_drop_down'; import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button'; import { Dispatcher } from 'ts/redux/dispatcher'; import { ProviderType } from 'ts/types'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; import { utils } from 'ts/utils/utils'; const VALID_ETHEREUM_DERIVATION_PATH_PREFIX = `44'/60'`; enum LedgerSteps { CONNECT, SELECT_ADDRESS, } interface LedgerConfigDialogProps { isOpen: boolean; toggleDialogFn: (isOpen: boolean) => void; dispatcher: Dispatcher; blockchain: Blockchain; networkId: number; providerType: ProviderType; } interface LedgerConfigDialogState { connectionErrMsg: string; stepIndex: LedgerSteps; userAddresses: string[]; addressBalances: BigNumber[]; derivationPath: string; derivationErrMsg: string; preferredNetworkId: number; } export class LedgerConfigDialog extends React.Component { constructor(props: LedgerConfigDialogProps) { super(props); const derivationPathIfExists = props.blockchain.getLedgerDerivationPathIfExists(); this.state = { connectionErrMsg: '', stepIndex: LedgerSteps.CONNECT, userAddresses: [], addressBalances: [], derivationPath: _.isUndefined(derivationPathIfExists) ? configs.DEFAULT_DERIVATION_PATH : derivationPathIfExists, derivationErrMsg: '', preferredNetworkId: props.networkId, }; } public render(): React.ReactNode { const dialogActions = [ , ]; const dialogTitle = this.state.stepIndex === LedgerSteps.CONNECT ? 'Connect to your Ledger' : 'Select desired address'; return (
{this.state.stepIndex === LedgerSteps.CONNECT && this._renderConnectStep()} {this.state.stepIndex === LedgerSteps.SELECT_ADDRESS && this._renderSelectAddressStep()}
); } private _renderConnectStep(): React.ReactNode { const networkIds = _.values(sharedConstants.NETWORK_ID_BY_NAME); return (
Follow these instructions before proceeding:
  1. Connect your Ledger Nano S & Open the Ethereum application
  2. Verify that "Browser Support" AND "Contract Data" are enabled in Settings
  3. If no Browser Support is found in settings, verify that you have{' '} Firmware >1.2
  4. Choose your desired network:
{!_.isEmpty(this.state.connectionErrMsg) && (
{this.state.connectionErrMsg}
)}
); } private _renderSelectAddressStep(): React.ReactNode { return (
Address Balance {this._renderAddressTableRows()}
); } private _renderAddressTableRows(): React.ReactNode { const rows = _.map(this.state.userAddresses, (userAddress: string, i: number) => { const balanceInWei = this.state.addressBalances[i]; const addressTooltipId = `address-${userAddress}`; const balanceTooltipId = `balance-${userAddress}`; const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; // We specifically prefix kovan ETH. // TODO: We should probably add prefixes for all networks const isKovanNetwork = networkName === 'Kovan'; const balanceInEth = Web3Wrapper.toUnitAmount(balanceInWei, constants.DECIMAL_PLACES_ETH); const balanceString = `${balanceInEth.toString()} ${isKovanNetwork ? 'Kovan ' : ''}ETH`; return (
{userAddress}
{userAddress}
{balanceString}
{balanceString}
); }); return rows; } private _onClose(): void { this.setState({ connectionErrMsg: '', stepIndex: LedgerSteps.CONNECT, }); const isOpen = false; this.props.toggleDialogFn(isOpen); } private _onAddressSelected(selectedRowIndexes: number[]): void { const selectedRowIndex = selectedRowIndexes[0]; const selectedAddress = this.state.userAddresses[selectedRowIndex]; const selectAddressBalance = this.state.addressBalances[selectedRowIndex]; this.props.dispatcher.updateUserAddress(selectedAddress); this.props.blockchain.updateWeb3WrapperPrevUserAddress(selectedAddress); // tslint:disable-next-line:no-floating-promises this.props.blockchain.fetchTokenInformationAsync(); this.props.dispatcher.updateUserWeiBalance(selectAddressBalance); this.setState({ stepIndex: LedgerSteps.CONNECT, }); const isOpen = false; this.props.toggleDialogFn(isOpen); } private async _onFetchAddressesForDerivationPathAsync(): Promise { const currentlySetPath = this.props.blockchain.getLedgerDerivationPathIfExists(); let didSucceed; if (currentlySetPath === this.state.derivationPath) { didSucceed = true; return didSucceed; } this.props.blockchain.updateLedgerDerivationPathIfExists(this.state.derivationPath); didSucceed = await this._fetchAddressesAndBalancesAsync(); if (!didSucceed) { this.setState({ derivationErrMsg: 'Failed to connect to Ledger.', }); } return didSucceed; } private async _fetchAddressesAndBalancesAsync(): Promise { let userAddresses: string[]; const addressBalances: BigNumber[] = []; try { userAddresses = await this._getUserAddressesAsync(); for (const address of userAddresses) { const balanceInWei = await this.props.blockchain.getBalanceInWeiAsync(address); addressBalances.push(balanceInWei); } } catch (err) { logUtils.log(`Ledger error: ${JSON.stringify(err)}`); this.setState({ connectionErrMsg: 'Failed to connect. Follow the instructions and try again.', }); return false; } this.setState({ userAddresses, addressBalances, }); return true; } private _onDerivationPathChanged(e: any, derivationPath: string): void { let derivationErrMsg = ''; if (!_.startsWith(derivationPath, VALID_ETHEREUM_DERIVATION_PATH_PREFIX)) { derivationErrMsg = 'Must be valid Ethereum path.'; } this.setState({ derivationPath, derivationErrMsg, }); } private async _onConnectLedgerClickAsync(): Promise { const isU2FSupported = await utils.isU2FSupportedAsync(); if (!isU2FSupported) { logUtils.log(`U2F not supported in this browser`); this.setState({ connectionErrMsg: 'U2F not supported by this browser. Try using Chrome.', }); return false; } if ( this.props.providerType !== ProviderType.Ledger || (this.props.providerType === ProviderType.Ledger && this.props.networkId !== this.state.preferredNetworkId) ) { await this.props.blockchain.updateProviderToLedgerAsync(this.state.preferredNetworkId); } const didSucceed = await this._fetchAddressesAndBalancesAsync(); if (didSucceed) { this.setState({ stepIndex: LedgerSteps.SELECT_ADDRESS, }); } return didSucceed; } private async _getUserAddressesAsync(): Promise { let userAddresses: string[]; userAddresses = await this.props.blockchain.getUserAccountsAsync(); if (_.isEmpty(userAddresses)) { throw new Error('No addresses retrieved.'); } return userAddresses; } private _onSelectedNetworkUpdated(e: any, index: number, networkId: number): void { this.setState({ preferredNetworkId: networkId, }); } }