Files
protocol/packages/website/ts/pages/governance/connect_form.tsx
Xianny 7423028fea Replace lodash with built-ins where possible to reduce bundle size (#1766)
* add tslint rule to disallow lodash.isUndefined

* add tslint rule to disallow lodash.isNull

* apply fixes
2019-04-10 09:36:32 -07:00

556 lines
22 KiB
TypeScript

import { getContractAddressesForNetworkOrThrow } from '@0x/contract-addresses';
import { ContractWrappers } from '@0x/contract-wrappers';
import {
ledgerEthereumBrowserClientFactoryAsync,
LedgerSubprovider,
MetamaskSubprovider,
RedundantSubprovider,
RPCSubprovider,
SignerSubprovider,
Web3ProviderEngine,
} from '@0x/subproviders';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import '@reach/dialog/styles.css';
import { ZeroExProvider } from 'ethereum-types';
import * as _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
import { Button } from 'ts/components/button';
import { Icon } from 'ts/components/icon';
import { Heading, Paragraph } from 'ts/components/text';
import { AddressTable } from 'ts/pages/governance/address_table';
import { DerivationPathInput } from 'ts/pages/governance/derivation_path_input';
import { colors } from 'ts/style/colors';
import { InjectedProvider, Providers } from 'ts/types';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
import { utils } from 'ts/utils/utils';
const providerToName: { [provider: string]: string } = {
[Providers.Metamask]: constants.PROVIDER_NAME_METAMASK,
[Providers.Parity]: constants.PROVIDER_NAME_PARITY_SIGNER,
[Providers.Mist]: constants.PROVIDER_NAME_MIST,
[Providers.CoinbaseWallet]: constants.PROVIDER_NAME_COINBASE_WALLET,
[Providers.Cipher]: constants.PROVIDER_NAME_CIPHER,
};
export interface WalletConnectedProps {
providerName: string;
selectedAddress: string;
currentBalance: BigNumber;
contractWrappers?: ContractWrappers;
injectedProviderIfExists?: InjectedProvider;
providerEngine?: ZeroExProvider;
ledgerSubproviderIfExists?: LedgerSubprovider;
isLedger?: boolean;
web3Wrapper?: Web3Wrapper;
}
interface Props {
onDismiss?: () => void;
onWalletConnected?: (props: WalletConnectedProps) => void;
onVoted?: () => void;
onError?: (errorMessage: string) => void;
web3Wrapper?: Web3Wrapper;
currentBalance: BigNumber;
}
interface State {
providerName?: string;
connectionErrMsg: string;
isWalletConnected: boolean;
isLedgerConnected: boolean;
isSubmitting: boolean;
isSuccessful: boolean;
preferredNetworkId: number;
selectedUserAddressIndex: number;
errors: ErrorProps;
userAddresses: string[];
addressBalances: BigNumber[];
derivationPath: string;
derivationErrMsg: string;
}
interface ErrorProps {
[key: string]: string;
}
enum ConnectSteps {
Connect,
SelectAddress,
}
const ZERO = new BigNumber(0);
export class ConnectForm extends React.Component<Props, State> {
public static defaultProps = {
currentBalance: ZERO,
isWalletConnected: false,
errors: {},
};
// blockchain related
public networkId: number;
private _providerName: string;
private _userAddressIfExists: string;
private _contractWrappers: ContractWrappers;
private _injectedProviderIfExists?: InjectedProvider;
private _web3Wrapper?: Web3Wrapper;
private _providerEngine?: ZeroExProvider;
private _ledgerSubprovider: LedgerSubprovider;
public constructor(props: Props) {
super(props);
const derivationPathIfExists = this.getLedgerDerivationPathIfExists();
this.state = {
connectionErrMsg: '',
isWalletConnected: false,
isLedgerConnected: false,
isSubmitting: false,
isSuccessful: false,
providerName: null,
preferredNetworkId: constants.NETWORK_ID_MAINNET,
selectedUserAddressIndex: 0,
errors: {},
userAddresses: [],
addressBalances: [],
derivationPath:
derivationPathIfExists === undefined ? configs.DEFAULT_DERIVATION_PATH : derivationPathIfExists,
derivationErrMsg: '',
};
}
public render(): React.ReactNode {
const { errors } = this.state;
return (
<div style={{ textAlign: 'center' }}>
<Icon name="wallet" size={120} margin={[0, 0, 'default', 0]} />
{this._renderContent(errors)}
</div>
);
}
public _renderContent(errors: ErrorProps): React.ReactNode {
switch (this.state.isLedgerConnected) {
case true:
return this._renderChooseAddressContent(errors);
case false:
default:
return this._renderButtonsContent(errors);
}
}
public _renderButtonsContent(errors: ErrorProps): React.ReactNode {
return (
<div style={{ maxWidth: '470px', margin: '0 auto' }}>
<Heading color={colors.textDarkPrimary} size={34} asElement="h2">
Connect Your Wallet
</Heading>
<Paragraph isMuted={true} color={colors.textDarkPrimary}>
In order to vote on this issue you will need to connect a wallet with a balance of ZRX tokens.
</Paragraph>
<ButtonRow>
<ButtonHalf onClick={this._onConnectWalletClickAsync.bind(this)}>Connect Wallet</ButtonHalf>
<ButtonHalf onClick={this._onConnectLedgerClickAsync.bind(this)}>Connect Ledger</ButtonHalf>
</ButtonRow>
{errors.connectionError !== undefined && (
<Paragraph isMuted={true} color={colors.red}>
{errors.connectionError}
</Paragraph>
)}
</div>
);
}
public _renderChooseAddressContent(errors: ErrorProps): React.ReactNode {
const { userAddresses, addressBalances, derivationPath } = this.state;
return (
<>
<Heading color={colors.textDarkPrimary} size={34} asElement="h2">
Choose Address to Vote From
</Heading>
<AddressTable
userAddresses={userAddresses}
addressBalances={addressBalances}
networkId={this.networkId}
onSelectAddress={this._onSelectAddressIndex.bind(this)}
/>
{errors.connectionError !== undefined && <ErrorParagraph>{errors.connectionError}</ErrorParagraph>}
<DerivationPathInput
path={derivationPath}
onChangePath={this._onChangeDerivationPathAsync.bind(this)}
/>
<ButtonRow>
<Button type="button" onClick={this._onGoBack.bind(this, ConnectSteps.Connect)}>
Back
</Button>
<Button type="button" onClick={this._onSelectedLedgerAddressAsync.bind(this)}>
Next
</Button>
</ButtonRow>
</>
);
}
public async _onChangeDerivationPathAsync(path: string): Promise<void> {
this.setState(
{
derivationPath: path,
},
async () => {
await this._onFetchAddressesForDerivationPathAsync();
},
);
}
public async getUserAccountsAsync(): Promise<string[]> {
utils.assert(this._contractWrappers !== undefined, 'ContractWrappers must be instantiated.');
const provider = this._contractWrappers.getProvider();
const web3Wrapper = new Web3Wrapper(provider);
const userAccountsIfExists = await web3Wrapper.getAvailableAddressesAsync();
return userAccountsIfExists;
}
public getLedgerDerivationPathIfExists(): string | undefined {
if (this._ledgerSubprovider === undefined) {
return undefined;
}
const path = this._ledgerSubprovider.getPath();
return path;
}
public updateLedgerDerivationPathIfExists(path: string): void {
if (this._ledgerSubprovider === undefined) {
return; // noop
}
this._ledgerSubprovider.setPath(path);
}
public async getZrxBalanceAsync(owner: string): Promise<BigNumber> {
utils.assert(this._contractWrappers !== undefined, 'ContractWrappers must be instantiated.');
const contractAddresses = getContractAddressesForNetworkOrThrow(this.networkId);
const tokenAddress: string = contractAddresses.zrxToken;
try {
const amount = await this._contractWrappers.erc20Token.getBalanceAsync(tokenAddress, owner);
return amount;
} catch (error) {
return ZERO;
}
}
private async _onConnectWalletClickAsync(): Promise<boolean> {
const shouldUseLedgerProvider = false;
const networkIdIfExists = await this._getInjectedProviderNetworkIdIfExistsAsync();
this.networkId = networkIdIfExists !== undefined ? networkIdIfExists : constants.NETWORK_ID_MAINNET;
await this._resetOrInitializeAsync(this.networkId, shouldUseLedgerProvider);
const didSucceed = await this._fetchAddressesAndBalancesAsync();
if (didSucceed) {
this.setState(
{
errors: {},
preferredNetworkId: this.networkId,
},
async () => {
// Always assume selected index is 0 for Metamask
await this._updateSelectedAddressAsync(0);
},
);
}
return didSucceed;
}
private async _onConnectLedgerClickAsync(): Promise<boolean> {
const isU2FSupported = await utils.isU2FSupportedAsync();
if (!isU2FSupported) {
const errorMessage = 'U2F not supported by this browser. Try using Chrome.';
this.props.onError
? this.props.onError(errorMessage)
: this.setState({
errors: {
connectionError: errorMessage,
},
});
return false;
}
// We don't want to be out of sync with the network the injected provider declares.
const networkId = constants.NETWORK_ID_MAINNET;
await this._updateProviderToLedgerAsync(networkId);
const didSucceed = await this._fetchAddressesAndBalancesAsync();
if (didSucceed) {
this.setState({
errors: {},
isLedgerConnected: true,
});
}
return didSucceed;
}
private _onSelectAddressIndex(index: number): void {
this.setState({
selectedUserAddressIndex: index,
});
}
private async _onSelectedLedgerAddressAsync(): Promise<void> {
await this._updateSelectedAddressAsync(this.state.selectedUserAddressIndex);
}
private async _onFetchAddressesForDerivationPathAsync(): Promise<boolean> {
const currentlySetPath = this.getLedgerDerivationPathIfExists();
let didSucceed;
if (currentlySetPath === this.state.derivationPath) {
didSucceed = true;
return didSucceed;
}
this.updateLedgerDerivationPathIfExists(this.state.derivationPath);
didSucceed = await this._fetchAddressesAndBalancesAsync();
if (!didSucceed) {
const errorMessage = 'Failed to connect to Ledger.';
this.props.onError
? this.props.onError(errorMessage)
: this.setState({
errors: {
connectionError: errorMessage,
},
});
}
return didSucceed;
}
private async _fetchAddressesAndBalancesAsync(): Promise<boolean> {
let userAddresses: string[];
const addressBalances: BigNumber[] = [];
try {
userAddresses = await this._getUserAddressesAsync();
for (const address of userAddresses) {
const balanceInZrx = await this.getZrxBalanceAsync(address);
addressBalances.push(balanceInZrx);
}
} catch (err) {
const errorMessage = 'Failed to connect. Follow the instructions and try again.';
this.props.onError
? this.props.onError(errorMessage)
: this.setState({
errors: {
connectionError: errorMessage,
},
});
return false;
}
this.setState({
userAddresses,
addressBalances,
});
return true;
}
private async _updateSelectedAddressAsync(index: number): Promise<void> {
const { userAddresses, addressBalances, isLedgerConnected } = this.state;
const injectedProviderIfExists = await this._getInjectedProviderIfExistsAsync();
if (this.props.onWalletConnected && userAddresses[index] !== undefined) {
const walletInfo: WalletConnectedProps = {
contractWrappers: this._contractWrappers,
injectedProviderIfExists,
ledgerSubproviderIfExists: this._ledgerSubprovider,
selectedAddress: userAddresses[index],
currentBalance: addressBalances[index],
providerEngine: this._providerEngine,
providerName: this._providerName,
web3Wrapper: this._web3Wrapper,
isLedger: isLedgerConnected,
};
this.props.onWalletConnected(walletInfo);
}
}
private async _updateProviderToLedgerAsync(networkId: number): Promise<void> {
const shouldUserLedgerProvider = true;
await this._resetOrInitializeAsync(networkId, shouldUserLedgerProvider);
}
private _getNameGivenProvider(provider: ZeroExProvider): string {
const providerType = utils.getProviderType(provider);
const providerNameIfExists = providerToName[providerType];
if (providerNameIfExists === undefined) {
return constants.PROVIDER_NAME_GENERIC;
}
return providerNameIfExists;
}
private async _getProviderAsync(
injectedProviderIfExists?: InjectedProvider,
networkIdIfExists?: number,
shouldUserLedgerProvider: boolean = false,
): Promise<[ZeroExProvider, LedgerSubprovider | undefined]> {
// This code is based off of the Blockchain.ts code.
// TODO refactor to re-use this utility outside of Blockchain.ts
const doesInjectedProviderExist = injectedProviderIfExists !== undefined;
const isNetworkIdAvailable = networkIdIfExists !== undefined;
const publicNodeUrlsIfExistsForNetworkId = configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists];
const isPublicNodeAvailableForNetworkId = publicNodeUrlsIfExistsForNetworkId !== undefined;
const provider = new Web3ProviderEngine();
const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists], publicNodeUrl => {
return new RPCSubprovider(publicNodeUrl);
});
if (shouldUserLedgerProvider && isNetworkIdAvailable) {
const isU2FSupported = await utils.isU2FSupportedAsync();
if (!isU2FSupported) {
throw new Error('Cannot update providerType to LEDGER without U2F support');
}
const ledgerWalletConfigs = {
networkId: networkIdIfExists,
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
};
const ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
provider.addProvider(ledgerSubprovider);
provider.addProvider(new RedundantSubprovider(rpcSubproviders));
provider.start();
return [provider, ledgerSubprovider];
} else if (doesInjectedProviderExist && isPublicNodeAvailableForNetworkId) {
// We catch all requests involving a users account and send it to the injectedWeb3
// instance. All other requests go to the public hosted node.
const providerName = this._getNameGivenProvider(injectedProviderIfExists);
// Wrap Metamask in a compatability wrapper MetamaskSubprovider (to handle inconsistencies)
const signerSubprovider =
providerName === constants.PROVIDER_NAME_METAMASK || constants.PROVIDER_NAME_COINBASE_WALLET
? new MetamaskSubprovider(injectedProviderIfExists)
: new SignerSubprovider(injectedProviderIfExists);
provider.addProvider(signerSubprovider);
provider.addProvider(new RedundantSubprovider(rpcSubproviders));
provider.start();
return [provider, undefined];
} else if (doesInjectedProviderExist) {
// Since no public node for this network, all requests go to injectedWeb3 instance
return [injectedProviderIfExists, undefined];
} else {
// If no injectedWeb3 instance, all requests fallback to our public hosted mainnet/testnet node
// We do this so that users can still browse the 0x Portal DApp even if they do not have web3
// injected into their browser.
const networkId = constants.NETWORK_ID_MAINNET;
const defaultRpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId], publicNodeUrl => {
return new RPCSubprovider(publicNodeUrl);
});
provider.addProvider(new RedundantSubprovider(defaultRpcSubproviders));
provider.start();
return [provider, undefined];
}
}
private async _getInjectedProviderIfExistsAsync(): Promise<InjectedProvider | undefined> {
if (this._injectedProviderIfExists !== undefined) {
return this._injectedProviderIfExists;
}
let injectedProviderIfExists = (window as any).ethereum;
if (injectedProviderIfExists !== undefined) {
if (injectedProviderIfExists.enable !== undefined) {
await injectedProviderIfExists.enable();
}
} else {
const injectedWeb3IfExists = (window as any).web3;
if (injectedWeb3IfExists !== undefined && injectedWeb3IfExists.currentProvider !== undefined) {
injectedProviderIfExists = injectedWeb3IfExists.currentProvider;
} else {
return undefined;
}
}
this._injectedProviderIfExists = injectedProviderIfExists;
return injectedProviderIfExists;
}
private async _getInjectedProviderNetworkIdIfExistsAsync(): Promise<number | undefined> {
// If the user has an injectedWeb3 instance that is disconnected from a backing
// Ethereum node, this call will throw. We need to handle this case gracefully
const injectedProviderIfExists = await this._getInjectedProviderIfExistsAsync();
let networkIdIfExists: number;
if (injectedProviderIfExists !== undefined) {
try {
const injectedWeb3Wrapper = new Web3Wrapper(injectedProviderIfExists);
networkIdIfExists = await injectedWeb3Wrapper.getNetworkIdAsync();
} catch (err) {
// Ignore error and proceed with networkId undefined
}
}
return networkIdIfExists;
}
private async _resetOrInitializeAsync(networkId: number, shouldUserLedgerProvider: boolean = false): Promise<void> {
this.networkId = networkId;
const injectedProviderIfExists = await this._getInjectedProviderIfExistsAsync();
const [provider, ledgerSubproviderIfExists] = await this._getProviderAsync(
injectedProviderIfExists,
networkId,
shouldUserLedgerProvider,
);
this._web3Wrapper = new Web3Wrapper(provider);
this._providerEngine = provider;
this.networkId = await this._web3Wrapper.getNetworkIdAsync();
this._providerName = this._getNameGivenProvider(provider);
if (this._contractWrappers !== undefined) {
this._contractWrappers.unsubscribeAll();
}
const contractWrappersConfig = {
networkId,
};
this._contractWrappers = new ContractWrappers(provider, contractWrappersConfig);
if (shouldUserLedgerProvider && ledgerSubproviderIfExists !== undefined) {
delete this._userAddressIfExists;
this._ledgerSubprovider = ledgerSubproviderIfExists;
} else {
delete this._ledgerSubprovider;
const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync();
this._userAddressIfExists = userAddresses[0];
}
}
private async _getUserAddressesAsync(): Promise<string[]> {
let userAddresses: string[];
userAddresses = await this.getUserAccountsAsync();
if (_.isEmpty(userAddresses)) {
throw new Error('No addresses retrieved.');
}
return userAddresses;
}
private _onGoBack(step: number): void {
switch (step) {
case ConnectSteps.SelectAddress:
// @todo support going back to select address
this.setState({
isLedgerConnected: false,
});
break;
default:
case ConnectSteps.Connect:
this.setState({
isLedgerConnected: false,
});
}
}
}
const InputRow = styled.div`
width: 100%;
flex: 0 0 auto;
@media (min-width: 768px) {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
}
`;
const ButtonRow = styled(InputRow)`
@media (max-width: 768px) {
display: flex;
flex-direction: column;
button:nth-child(1) {
order: 2;
}
button:nth-child(2) {
order: 1;
margin-bottom: 10px;
}
}
`;
const ButtonHalf = styled(Button)`
width: calc(50% - 15px);
padding: 18px 18px;
`;
// tslint:disable:max-file-line-count
const ErrorParagraph = styled(Paragraph).attrs({
color: colors.red,
isMuted: true,
})`
margin: 10px 0 0 30px;
`;