Initial Ledger support implementation

This commit is contained in:
Fabio Berger
2018-01-28 10:29:15 +01:00
parent 748d805a32
commit dd9f5adc2e
24 changed files with 561 additions and 165 deletions

View File

@@ -8,9 +8,12 @@
"clean": "shx rm -f public/bundle*",
"lint": "tslint --project . 'ts/**/*.ts' 'ts/**/*.tsx'",
"dev": "webpack-dev-server --content-base public --https",
"update_contracts": "for i in ${npm_package_config_artifacts}; do copyfiles -u 4 ../contracts/build/contracts/$i.json ../website/contracts; done;",
"deploy_staging": "npm run build; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers",
"deploy_live": "npm run build; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers"
"update_contracts":
"for i in ${npm_package_config_artifacts}; do copyfiles -u 4 ../contracts/build/contracts/$i.json ../website/contracts; done;",
"deploy_staging":
"npm run build; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers",
"deploy_live":
"npm run build; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers"
},
"config": {
"artifacts": "Mintable"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

View File

@@ -64,6 +64,7 @@ export class Blockchain {
private _exchangeAddress: string;
private _userAddress: string;
private _cachedProvider: Web3.Provider;
private _cachedProviderNetworkId: number;
private _ledgerSubprovider: LedgerWalletSubprovider;
private _zrxPollIntervalId: NodeJS.Timer;
private static async _onPageLoadAsync(): Promise<void> {
@@ -133,14 +134,14 @@ export class Blockchain {
} else if (this.networkId !== newNetworkId) {
this.networkId = newNetworkId;
this._dispatcher.encounteredBlockchainError(BlockchainErrs.NoError);
await this._fetchTokenInformationAsync();
await this.fetchTokenInformationAsync();
await this._rehydrateStoreWithContractEvents();
}
}
public async userAddressUpdatedFireAndForgetAsync(newUserAddress: string) {
if (this._userAddress !== newUserAddress) {
this._userAddress = newUserAddress;
await this._fetchTokenInformationAsync();
await this.fetchTokenInformationAsync();
await this._rehydrateStoreWithContractEvents();
}
}
@@ -180,63 +181,62 @@ export class Blockchain {
}
this._ledgerSubprovider.setPathIndex(pathIndex);
}
public async providerTypeUpdatedFireAndForgetAsync(providerType: ProviderType) {
public async updateProviderToLedgerAsync(networkId: number) {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
// Should actually be Web3.Provider|ProviderEngine union type but it causes issues
// later on in the logic.
let provider;
switch (providerType) {
case ProviderType.Ledger: {
const isU2FSupported = await utils.isU2FSupportedAsync();
if (!isU2FSupported) {
throw new Error('Cannot update providerType to LEDGER without U2F support');
}
// Cache injected provider so that we can switch the user back to it easily
this._cachedProvider = this._web3Wrapper.getProviderObj();
this._dispatcher.updateUserAddress(''); // Clear old userAddress
provider = new ProviderEngine();
const ledgerWalletConfigs = {
networkId: this.networkId,
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
};
this._ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
provider.addProvider(this._ledgerSubprovider);
provider.addProvider(new FilterSubprovider());
const networkId = configs.IS_MAINNET_ENABLED
? constants.NETWORK_ID_MAINNET
: constants.NETWORK_ID_TESTNET;
provider.addProvider(new RedundantRPCSubprovider(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId]));
provider.start();
this._web3Wrapper.destroy();
const shouldPollUserAddress = false;
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
this._zeroEx.setProvider(provider, networkId);
await this._postInstantiationOrUpdatingProviderZeroExAsync();
break;
}
case ProviderType.Injected: {
if (_.isUndefined(this._cachedProvider)) {
return; // Going from injected to injected, so we noop
}
provider = this._cachedProvider;
const shouldPollUserAddress = true;
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
this._zeroEx.setProvider(provider, this.networkId);
await this._postInstantiationOrUpdatingProviderZeroExAsync();
delete this._ledgerSubprovider;
delete this._cachedProvider;
break;
}
default:
throw utils.spawnSwitchErr('providerType', providerType);
const isU2FSupported = await utils.isU2FSupportedAsync();
if (!isU2FSupported) {
throw new Error('Cannot update providerType to LEDGER without U2F support');
}
await this._fetchTokenInformationAsync();
// Cache injected provider so that we can switch the user back to it easily
this._cachedProvider = this._web3Wrapper.getProviderObj();
this._cachedProviderNetworkId = this.networkId;
this._userAddress = '';
this._dispatcher.updateUserAddress(''); // Clear old userAddress
const provider = new ProviderEngine();
const ledgerWalletConfigs = {
networkId,
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
};
this._ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
provider.addProvider(this._ledgerSubprovider);
provider.addProvider(new FilterSubprovider());
provider.addProvider(new RedundantRPCSubprovider(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId]));
provider.start();
this._web3Wrapper.destroy();
this.networkId = networkId;
this._dispatcher.updateNetworkId(this.networkId);
const shouldPollUserAddress = false;
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
this._zeroEx.setProvider(provider, this.networkId);
await this._postInstantiationOrUpdatingProviderZeroExAsync();
this._dispatcher.updateProviderType(ProviderType.Ledger);
}
public async updateProviderToInjectedAsync() {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
if (_.isUndefined(this._cachedProvider)) {
return; // Going from injected to injected, so we noop
}
const provider = this._cachedProvider;
this.networkId = this._cachedProviderNetworkId;
this._dispatcher.updateNetworkId(this.networkId);
this._web3Wrapper.destroy();
const shouldPollUserAddress = true;
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
this._dispatcher.updateUserAddress(this._userAddress);
this._zeroEx.setProvider(provider, this.networkId);
await this._postInstantiationOrUpdatingProviderZeroExAsync();
await this.fetchTokenInformationAsync();
this._dispatcher.updateProviderType(ProviderType.Injected);
delete this._ledgerSubprovider;
delete this._cachedProvider;
}
public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> {
utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid);
@@ -452,6 +452,7 @@ export class Blockchain {
return [balance, allowance];
}
public async updateTokenBalancesAndAllowancesAsync(tokens: Token[]) {
const err = new Error('show stopper');
const tokenStateByAddress: TokenStateByAddress = {};
for (const token of tokens) {
let balance = new BigNumber(0);
@@ -483,6 +484,60 @@ export class Blockchain {
this._web3Wrapper.destroy();
this._stopWatchingExchangeLogFillEvents();
}
public async fetchTokenInformationAsync() {
utils.assert(
!_.isUndefined(this.networkId),
'Cannot call fetchTokenInformationAsync if disconnected from Ethereum node',
);
this._dispatcher.updateBlockchainIsLoaded(false);
// HACK: Without this timeout, the second call to dispatcher somehow causes blockchainIsLoaded
// to flicker... Need to debug further :(((())))
await new Promise(resolve => setTimeout(resolve, 100));
this._dispatcher.clearTokenByAddress();
const tokenRegistryTokensByAddress = await this._getTokenRegistryTokensByAddressAsync();
// HACK: This is a hack so that the loading spinner doesn't show up twice...
// Once for loading the blockchain, another for loading the userAddress
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
if (!_.isEmpty(this._userAddress)) {
this._dispatcher.updateUserAddress(this._userAddress);
}
let trackedTokensIfExists = trackedTokenStorage.getTrackedTokensIfExists(this._userAddress, this.networkId);
const tokenRegistryTokens = _.values(tokenRegistryTokensByAddress);
if (_.isUndefined(trackedTokensIfExists)) {
trackedTokensIfExists = _.map(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, symbol => {
const token = _.find(tokenRegistryTokens, t => t.symbol === symbol);
token.isTracked = true;
return token;
});
_.each(trackedTokensIfExists, token => {
trackedTokenStorage.addTrackedTokenToUser(this._userAddress, this.networkId, token);
});
} else {
// Properly set all tokenRegistry tokens `isTracked` to true if they are in the existing trackedTokens array
_.each(trackedTokensIfExists, trackedToken => {
if (!_.isUndefined(tokenRegistryTokensByAddress[trackedToken.address])) {
tokenRegistryTokensByAddress[trackedToken.address].isTracked = true;
}
});
}
const allTokens = _.uniq([...tokenRegistryTokens, ...trackedTokensIfExists]);
this._dispatcher.updateTokenByAddress(allTokens);
// Get balance/allowance for tracked tokens
await this.updateTokenBalancesAndAllowancesAsync(trackedTokensIfExists);
const mostPopularTradingPairTokens: Token[] = [
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[0] }),
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[1] }),
];
this._dispatcher.updateChosenAssetTokenAddress(Side.Deposit, mostPopularTradingPairTokens[0].address);
this._dispatcher.updateChosenAssetTokenAddress(Side.Receive, mostPopularTradingPairTokens[1].address);
this._dispatcher.updateBlockchainIsLoaded(true);
}
private async _showEtherScanLinkAndAwaitTransactionMinedAsync(
txHash: string,
): Promise<TransactionReceiptWithDecodedLogs> {
@@ -690,60 +745,6 @@ export class Blockchain {
: constants.PROVIDER_NAME_PUBLIC;
this._dispatcher.updateInjectedProviderName(providerName);
}
private async _fetchTokenInformationAsync() {
utils.assert(
!_.isUndefined(this.networkId),
'Cannot call fetchTokenInformationAsync if disconnected from Ethereum node',
);
this._dispatcher.updateBlockchainIsLoaded(false);
this._dispatcher.clearTokenByAddress();
const tokenRegistryTokensByAddress = await this._getTokenRegistryTokensByAddressAsync();
// HACK: We need to fetch the userAddress here because otherwise we cannot save the
// tracked tokens in localStorage under the users address nor fetch the token
// balances and allowances and we need to do this in order not to trigger the blockchain
// loading dialog to show up twice. First to load the contracts, and second to load the
// balances and allowances.
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
if (!_.isEmpty(this._userAddress)) {
this._dispatcher.updateUserAddress(this._userAddress);
}
let trackedTokensIfExists = trackedTokenStorage.getTrackedTokensIfExists(this._userAddress, this.networkId);
const tokenRegistryTokens = _.values(tokenRegistryTokensByAddress);
if (_.isUndefined(trackedTokensIfExists)) {
trackedTokensIfExists = _.map(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, symbol => {
const token = _.find(tokenRegistryTokens, t => t.symbol === symbol);
token.isTracked = true;
return token;
});
_.each(trackedTokensIfExists, token => {
trackedTokenStorage.addTrackedTokenToUser(this._userAddress, this.networkId, token);
});
} else {
// Properly set all tokenRegistry tokens `isTracked` to true if they are in the existing trackedTokens array
_.each(trackedTokensIfExists, trackedToken => {
if (!_.isUndefined(tokenRegistryTokensByAddress[trackedToken.address])) {
tokenRegistryTokensByAddress[trackedToken.address].isTracked = true;
}
});
}
const allTokens = _.uniq([...tokenRegistryTokens, ...trackedTokensIfExists]);
this._dispatcher.updateTokenByAddress(allTokens);
// Get balance/allowance for tracked tokens
await this.updateTokenBalancesAndAllowancesAsync(trackedTokensIfExists);
const mostPopularTradingPairTokens: Token[] = [
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[0] }),
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[1] }),
];
this._dispatcher.updateChosenAssetTokenAddress(Side.Deposit, mostPopularTradingPairTokens[0].address);
this._dispatcher.updateChosenAssetTokenAddress(Side.Receive, mostPopularTradingPairTokens[1].address);
this._dispatcher.updateBlockchainIsLoaded(true);
}
private async _instantiateContractIfExistsAsync(artifact: any, address?: string): Promise<ContractInstance> {
const c = await contract(artifact);
const providerObj = this._web3Wrapper.getProviderObj();

View File

@@ -7,8 +7,10 @@ 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 { colors } from 'ts/utils/colors';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
@@ -27,27 +29,30 @@ interface LedgerConfigDialogProps {
dispatcher: Dispatcher;
blockchain: Blockchain;
networkId: number;
providerType: ProviderType;
}
interface LedgerConfigDialogState {
didConnectFail: boolean;
connectionErrMsg: string;
stepIndex: LedgerSteps;
userAddresses: string[];
addressBalances: BigNumber[];
derivationPath: string;
derivationErrMsg: string;
preferredNetworkId: number;
}
export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, LedgerConfigDialogState> {
constructor(props: LedgerConfigDialogProps) {
super(props);
this.state = {
didConnectFail: false,
connectionErrMsg: '',
stepIndex: LedgerSteps.CONNECT,
userAddresses: [],
addressBalances: [],
derivationPath: configs.DEFAULT_DERIVATION_PATH,
derivationErrMsg: '',
preferredNetworkId: props.networkId,
};
}
public render() {
@@ -77,7 +82,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
return (
<div>
<div className="h4 pt3">Follow these instructions before proceeding:</div>
<ol>
<ol className="mb0">
<li className="pb1">Connect your Ledger Nano S & Open the Ethereum application</li>
<li className="pb1">Verify that Browser Support is enabled in Settings</li>
<li className="pb1">
@@ -86,7 +91,15 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
Firmware >1.2
</a>
</li>
<li>Choose your desired network:</li>
</ol>
<div className="pb2">
<NetworkDropDown
updateSelectedNetwork={this._onSelectedNetworkUpdated.bind(this)}
selectedNetworkId={this.state.preferredNetworkId}
avialableNetworkIds={[1, 42]}
/>
</div>
<div className="center pb3">
<LifeCycleRaisedButton
isPrimary={true}
@@ -95,9 +108,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
labelComplete="Connected!"
onClickAsyncFn={this._onConnectLedgerClickAsync.bind(this, true)}
/>
{this.state.didConnectFail && (
{!_.isEmpty(this.state.connectionErrMsg) && (
<div className="pt2 left-align" style={{ color: colors.red200 }}>
Failed to connect. Follow the instructions and try again.
{this.state.connectionErrMsg}
</div>
)}
</div>
@@ -172,7 +185,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
}
private _onClose() {
this.setState({
didConnectFail: false,
connectionErrMsg: '',
});
const isOpen = false;
this.props.toggleDialogFn(isOpen);
@@ -184,6 +197,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
const selectAddressBalance = this.state.addressBalances[selectedRowIndex];
this.props.dispatcher.updateUserAddress(selectedAddress);
this.props.blockchain.updateWeb3WrapperPrevUserAddress(selectedAddress);
this.props.blockchain.fetchTokenInformationAsync(); // fire and forget
this.props.dispatcher.updateUserEtherBalance(selectAddressBalance);
this.setState({
stepIndex: LedgerSteps.CONNECT,
@@ -219,7 +233,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
} catch (err) {
utils.consoleLog(`Ledger error: ${JSON.stringify(err)}`);
this.setState({
didConnectFail: true,
connectionErrMsg: 'Failed to connect. Follow the instructions and try again.',
});
return false;
}
@@ -241,6 +255,19 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
});
}
private async _onConnectLedgerClickAsync() {
const isU2FSupported = await utils.isU2FSupportedAsync();
if (!isU2FSupported) {
utils.consoleLog(`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) {
await this.props.blockchain.updateProviderToLedgerAsync(this.state.preferredNetworkId);
}
const didSucceed = await this._fetchAddressesAndBalancesAsync();
if (didSucceed) {
this.setState({
@@ -258,4 +285,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
}
return userAddresses;
}
private _onSelectedNetworkUpdated(e: any, index: number, networkId: number) {
this.setState({
preferredNetworkId: networkId,
});
}
}

View File

@@ -0,0 +1,40 @@
import * as _ from 'lodash';
import DropDownMenu from 'material-ui/DropDownMenu';
import MenuItem from 'material-ui/MenuItem';
import * as React from 'react';
import { constants } from 'ts/utils/constants';
interface NetworkDropDownProps {
updateSelectedNetwork: (e: any, index: number, value: number) => void;
selectedNetworkId: number;
avialableNetworkIds: number[];
}
interface NetworkDropDownState {}
export class NetworkDropDown extends React.Component<NetworkDropDownProps, NetworkDropDownState> {
public render() {
return (
<div className="mx-auto" style={{ width: 120 }}>
<DropDownMenu value={this.props.selectedNetworkId} onChange={this.props.updateSelectedNetwork}>
{this._renderDropDownItems()}
</DropDownMenu>
</div>
);
}
private _renderDropDownItems() {
const items = _.map(this.props.avialableNetworkIds, networkId => {
const networkName = constants.NETWORK_NAME_BY_ID[networkId];
const primaryText = (
<div className="flex">
<div className="pr1" style={{ width: 14, paddingTop: 2 }}>
<img src={`/images/network_icons/${networkName.toLowerCase()}.png`} style={{ width: 14 }} />
</div>
<div>{networkName}</div>
</div>
);
return <MenuItem key={networkId} value={networkId} primaryText={primaryText} />;
});
return items;
}
}

View File

@@ -6,6 +6,7 @@ import * as DocumentTitle from 'react-document-title';
import { Route, Switch } from 'react-router-dom';
import { Blockchain } from 'ts/blockchain';
import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog';
import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog';
import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog';
import { WrappedEthSectionNoticeDialog } from 'ts/components/dialogs/wrapped_eth_section_notice_dialog';
import { EthWrappers } from 'ts/components/eth_wrappers';
@@ -13,19 +14,21 @@ import { FillOrder } from 'ts/components/fill_order';
import { Footer } from 'ts/components/footer';
import { PortalMenu } from 'ts/components/portal_menu';
import { TokenBalances } from 'ts/components/token_balances';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { TradeHistory } from 'ts/components/trade_history/trade_history';
import { FlashMessage } from 'ts/components/ui/flash_message';
import { Loading } from 'ts/components/ui/loading';
import { GenerateOrderForm } from 'ts/containers/generate_order_form';
import { localStorage } from 'ts/local_storage/local_storage';
import { Dispatcher } from 'ts/redux/dispatcher';
import { State } from 'ts/redux/reducer';
import { orderSchema } from 'ts/schemas/order_schema';
import { SchemaValidator } from 'ts/schemas/validator';
import {
BlockchainErrs,
HashData,
Order,
ProviderType,
ScreenWidths,
Token,
TokenByAddress,
@@ -46,9 +49,11 @@ export interface PortalAllProps {
blockchainIsLoaded: boolean;
dispatcher: Dispatcher;
hashData: HashData;
injectedProviderName: string;
networkId: number;
nodeVersion: string;
orderFillAmount: BigNumber;
providerType: ProviderType;
screenWidth: ScreenWidths;
tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress;
@@ -67,6 +72,7 @@ interface PortalAllState {
prevPathname: string;
isDisclaimerDialogOpen: boolean;
isWethNoticeDialogOpen: boolean;
isLedgerDialogOpen: boolean;
}
export class Portal extends React.Component<PortalAllProps, PortalAllState> {
@@ -96,6 +102,7 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
prevPathname: this.props.location.pathname,
isDisclaimerDialogOpen: !hasAcceptedDisclaimer,
isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances,
isLedgerDialogOpen: false,
};
}
public componentDidMount() {
@@ -127,8 +134,9 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
this._blockchain.userAddressUpdatedFireAndForgetAsync(nextProps.userAddress);
if (!_.isEmpty(nextProps.userAddress) && nextProps.blockchainIsLoaded) {
const tokens = _.values(nextProps.tokenByAddress);
const trackedTokens = _.filter(tokens, t => t.isTracked);
// tslint:disable-next-line:no-floating-promises
this._updateBalanceAndAllowanceWithLoadingScreenAsync(tokens);
this._updateBalanceAndAllowanceWithLoadingScreenAsync(trackedTokens);
}
this.setState({
prevUserAddress: nextProps.userAddress,
@@ -167,8 +175,14 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
<DocumentTitle title="0x Portal DApp" />
<TopBar
userAddress={this.props.userAddress}
networkId={this.props.networkId}
injectedProviderName={this.props.injectedProviderName}
onToggleLedgerDialog={this.onToggleLedgerDialog.bind(this)}
dispatcher={this.props.dispatcher}
providerType={this.props.providerType}
blockchainIsLoaded={this.props.blockchainIsLoaded}
location={this.props.location}
blockchain={this._blockchain}
/>
<div id="portal" className="mx-auto max-width-4" style={{ width: '100%' }}>
<Paper className="mb3 mt2">
@@ -239,11 +253,26 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)}
/>
<FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} />
{this.props.blockchainIsLoaded && (
<LedgerConfigDialog
providerType={this.props.providerType}
networkId={this.props.networkId}
blockchain={this._blockchain}
dispatcher={this.props.dispatcher}
toggleDialogFn={this.onToggleLedgerDialog.bind(this)}
isOpen={this.state.isLedgerDialogOpen}
/>
)}
</div>
<Footer />
<Footer />;
</div>
);
}
public onToggleLedgerDialog() {
this.setState({
isLedgerDialogOpen: !this.state.isLedgerDialogOpen,
});
}
private _renderEthWrapper() {
return (
<EthWrappers

View File

@@ -0,0 +1,149 @@
import * as _ from 'lodash';
import Menu from 'material-ui/Menu';
import MenuItem from 'material-ui/MenuItem';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import RaisedButton from 'material-ui/RaisedButton';
import * as React from 'react';
import { Link } from 'react-router-dom';
import { Blockchain } from 'ts/blockchain';
import { ProviderPicker } from 'ts/components/top_bar/provider_picker';
import { DropDown } from 'ts/components/ui/drop_down';
import { Identicon } from 'ts/components/ui/identicon';
import { Dispatcher } from 'ts/redux/dispatcher';
import { ProviderType } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
import { utils } from 'ts/utils/utils';
const IDENTICON_DIAMETER = 32;
interface ProviderDisplayProps {
dispatcher: Dispatcher;
userAddress: string;
networkId: number;
injectedProviderName: string;
providerType: ProviderType;
onToggleLedgerDialog: () => void;
blockchain: Blockchain;
}
interface ProviderDisplayState {}
export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> {
public render() {
const isAddressAvailable = !_.isEmpty(this.props.userAddress);
const isExternallyInjectedProvider = ProviderType.Injected && this.props.injectedProviderName !== '0x Public';
const displayAddress = isAddressAvailable
? utils.getAddressBeginAndEnd(this.props.userAddress)
: isExternallyInjectedProvider ? 'Account locked' : '0x0000...0000';
// If the "injected" provider is our fallback public node, then we want to
// show the "connect a wallet" message instead of the providerName
const injectedProviderName = isExternallyInjectedProvider
? this.props.injectedProviderName
: 'Connect a wallet';
const providerTitle =
this.props.providerType === ProviderType.Injected ? injectedProviderName : 'Ledger Nano S';
const hoverActiveNode = (
<div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}>
<div>
<Identicon address={this.props.userAddress} diameter={IDENTICON_DIAMETER} />
</div>
<div style={{ marginLeft: 12, paddingTop: 1 }}>
<div style={{ fontSize: 12, color: '#FF7F00' }}>{providerTitle}</div>
<div style={{ fontSize: 14 }}>{displayAddress}</div>
</div>
<div style={{ borderLeft: '1px solid #E0E0E0', marginLeft: 17, paddingTop: 1 }} className="px2">
<i style={{ fontSize: 30, color: '#E0E0E0' }} className="zmdi zmdi zmdi-chevron-down" />
</div>
</div>
);
const hasInjectedProvider =
this.props.injectedProviderName !== '0x Public' && this.props.providerType === ProviderType.Injected;
const hasLedgerProvider = this.props.providerType === ProviderType.Ledger;
const horizontalPosition = hasInjectedProvider || hasLedgerProvider ? 'left' : 'middle';
return (
<div style={{ width: 'fit-content', height: 48, float: 'right' }}>
<DropDown
hoverActiveNode={hoverActiveNode}
popoverContent={this.renderPopoverContent(hasInjectedProvider, hasLedgerProvider)}
anchorOrigin={{ horizontal: horizontalPosition, vertical: 'bottom' }}
targetOrigin={{ horizontal: horizontalPosition, vertical: 'top' }}
zDepth={1}
/>
</div>
);
}
public renderPopoverContent(hasInjectedProvider: boolean, hasLedgerProvider: boolean) {
if (hasInjectedProvider || hasLedgerProvider) {
return (
<ProviderPicker
dispatcher={this.props.dispatcher}
networkId={this.props.networkId}
injectedProviderName={this.props.injectedProviderName}
providerType={this.props.providerType}
onToggleLedgerDialog={this.props.onToggleLedgerDialog}
blockchain={this.props.blockchain}
/>
);
} else {
// Nothing to connect to, show install/info popover
return (
<div className="px2" style={{ maxWidth: 420 }}>
<div className="center h4 py2" style={{ color: colors.grey700 }}>
Choose a wallet:
</div>
<div className="flex pb3">
<div className="center px2">
<div style={{ color: colors.darkGrey }}>Install a browser wallet</div>
<div className="py2">
<img src="/images/metamask_or_parity.png" width="135" />
</div>
<div>
Use{' '}
<a
href={constants.URL_METAMASK_CHROME_STORE}
target="_blank"
style={{ color: colors.lightBlueA700 }}
>
Metamask
</a>{' '}
or{' '}
<a
href={constants.URL_PARITY_CHROME_STORE}
target="_blank"
style={{ color: colors.lightBlueA700 }}
>
Parity Signer
</a>
</div>
</div>
<div>
<div
className="pl1 ml1"
style={{ borderLeft: `1px solid ${colors.grey300}`, height: 65 }}
/>
<div className="py1">or</div>
<div
className="pl1 ml1"
style={{ borderLeft: `1px solid ${colors.grey300}`, height: 68 }}
/>
</div>
<div className="px2 center">
<div style={{ color: colors.darkGrey }}>Connect to a ledger hardware wallet</div>
<div style={{ paddingTop: 21, paddingBottom: 29 }}>
<img src="/images/ledger_icon.png" style={{ width: 80 }} />
</div>
<div>
<RaisedButton
style={{ width: '100%' }}
label="Use Ledger"
onClick={this.props.onToggleLedgerDialog}
/>
</div>
</div>
</div>
</div>
);
}
}
}

View File

@@ -0,0 +1,88 @@
import * as _ from 'lodash';
import Menu from 'material-ui/Menu';
import MenuItem from 'material-ui/MenuItem';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import * as React from 'react';
import { Link } from 'react-router-dom';
import { Blockchain } from 'ts/blockchain';
import { DropDown } from 'ts/components/ui/drop_down';
import { Identicon } from 'ts/components/ui/identicon';
import { Dispatcher } from 'ts/redux/dispatcher';
import { ProviderType } from 'ts/types';
import { constants } from 'ts/utils/constants';
import { utils } from 'ts/utils/utils';
const IDENTICON_DIAMETER = 32;
const SELECTED_BG_COLOR = '#F7F7F7';
interface ProviderPickerProps {
networkId: number;
injectedProviderName: string;
providerType: ProviderType;
onToggleLedgerDialog: () => void;
dispatcher: Dispatcher;
blockchain: Blockchain;
}
interface ProviderPickerState {}
export class ProviderPicker extends React.Component<ProviderPickerProps, ProviderPickerState> {
public render() {
const isLedgerSelected = this.props.providerType === ProviderType.Ledger;
const menuStyle = {
padding: 10,
paddingTop: 15,
paddingBottom: 15,
};
const injectedLabel = (
<div className="flex">
<div>{this.props.injectedProviderName}</div>
{this._renderNetwork()}
</div>
);
// Show dropdown with two options
return (
<div style={{ width: 225, overflow: 'hidden' }}>
<RadioButtonGroup
name="provider"
defaultSelected={this.props.providerType}
onChange={this._onProviderRadioChanged.bind(this)}
>
<RadioButton
style={{ ...menuStyle, backgroundColor: !isLedgerSelected && SELECTED_BG_COLOR }}
value={ProviderType.Injected}
label={injectedLabel}
/>
<RadioButton
style={{ ...menuStyle, backgroundColor: isLedgerSelected && SELECTED_BG_COLOR }}
value={ProviderType.Ledger}
label="Ledger Nano S"
/>
</RadioButtonGroup>
</div>
);
}
private _renderNetwork() {
const networkName = constants.NETWORK_NAME_BY_ID[this.props.networkId];
return (
<div className="flex">
<div className="pr1 relative" style={{ width: 14, paddingLeft: 14 }}>
<img
src={`/images/network_icons/${networkName.toLowerCase()}.png`}
className="absolute"
style={{ top: 4, width: 14 }}
/>
</div>
<div style={{ color: '#BBBBBB' }}>{networkName}</div>
</div>
);
}
private _onProviderRadioChanged(e: any, value: string) {
if (value === ProviderType.Ledger) {
this.props.onToggleLedgerDialog();
} else {
// Fire and forget
this.props.blockchain.updateProviderToInjectedAsync();
}
}
}

View File

@@ -1,21 +1,32 @@
import * as _ from 'lodash';
import Drawer from 'material-ui/Drawer';
import Menu from 'material-ui/Menu';
import MenuItem from 'material-ui/MenuItem';
import * as React from 'react';
import { Link } from 'react-router-dom';
import ReactTooltip = require('react-tooltip');
import { Blockchain } from 'ts/blockchain';
import { PortalMenu } from 'ts/components/portal_menu';
import { TopBarMenuItem } from 'ts/components/top_bar_menu_item';
import { DropDownMenuItem } from 'ts/components/ui/drop_down_menu_item';
import { ProviderDisplay } from 'ts/components/top_bar/provider_display';
import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item';
import { DropDown } from 'ts/components/ui/drop_down';
import { Identicon } from 'ts/components/ui/identicon';
import { DocsInfo } from 'ts/pages/documentation/docs_info';
import { NestedSidebarMenu } from 'ts/pages/shared/nested_sidebar_menu';
import { DocsMenu, MenuSubsectionsBySection, Styles, WebsitePaths } from 'ts/types';
import { Dispatcher } from 'ts/redux/dispatcher';
import { DocsMenu, MenuSubsectionsBySection, ProviderType, Styles, TypeDocNode, WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
interface TopBarProps {
userAddress?: string;
networkId?: number;
injectedProviderName?: string;
providerType?: ProviderType;
onToggleLedgerDialog?: () => void;
blockchain?: Blockchain;
dispatcher?: Dispatcher;
blockchainIsLoaded: boolean;
location: Location;
docsVersion?: string;
@@ -125,6 +136,15 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
cursor: 'pointer',
paddingTop: 16,
};
const hoverActiveNode = (
<div className="flex relative" style={{ color: menuIconStyle.color }}>
<div style={{ paddingRight: 10 }}>Developers</div>
<div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}>
<i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} />
</div>
</div>
);
const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>;
return (
<div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style }} className="pb1">
<div className={parentClassNames}>
@@ -138,11 +158,12 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
{!this._isViewingPortal() && (
<div className={menuClasses}>
<div className="flex justify-between">
<DropDownMenuItem
title="Developers"
subMenuItems={developerSectionMenuItems}
<DropDown
hoverActiveNode={hoverActiveNode}
popoverContent={popoverContent}
anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }}
targetOrigin={{ horizontal: 'middle', vertical: 'top' }}
style={styles.menuItem}
isNightVersion={isNightVersion}
/>
<TopBarMenuItem
title="Wiki"
@@ -167,10 +188,19 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
</div>
</div>
)}
{this.props.blockchainIsLoaded &&
!_.isEmpty(this.props.userAddress) && (
<div className="col col-5 sm-hide xs-hide">{this._renderUser()}</div>
)}
{this.props.blockchainIsLoaded && (
<div className="col col-5">
<ProviderDisplay
dispatcher={this.props.dispatcher}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
injectedProviderName={this.props.injectedProviderName}
providerType={this.props.providerType}
onToggleLedgerDialog={this.props.onToggleLedgerDialog}
blockchain={this.props.blockchain}
/>
</div>
)}
<div className={`col ${isFullWidthPage ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}>
<div style={menuIconStyle}>
<i className="zmdi zmdi-menu" onClick={this._onMenuButtonClick.bind(this)} />

View File

@@ -1,7 +1,8 @@
import * as _ from 'lodash';
import Menu from 'material-ui/Menu';
import Popover from 'material-ui/Popover';
import Popover, { PopoverAnimationVertical } from 'material-ui/Popover';
import * as React from 'react';
import { MaterialUIPosition, Styles, WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';
const CHECK_CLOSE_POPOVER_INTERVAL_MS = 300;
@@ -9,28 +10,28 @@ const DEFAULT_STYLE = {
fontSize: 14,
};
interface DropDownMenuItemProps {
title: string;
subMenuItems: React.ReactNode[];
interface DropDownProps {
hoverActiveNode: React.ReactNode;
popoverContent: React.ReactNode;
anchorOrigin: MaterialUIPosition;
targetOrigin: MaterialUIPosition;
style?: React.CSSProperties;
menuItemStyle?: React.CSSProperties;
isNightVersion?: boolean;
zDepth?: number;
}
interface DropDownMenuItemState {
interface DropDownState {
isDropDownOpen: boolean;
anchorEl?: HTMLInputElement;
}
export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, DropDownMenuItemState> {
public static defaultProps: Partial<DropDownMenuItemProps> = {
export class DropDown extends React.Component<DropDownProps, DropDownState> {
public static defaultProps: Partial<DropDownProps> = {
style: DEFAULT_STYLE,
menuItemStyle: DEFAULT_STYLE,
isNightVersion: false,
zDepth: 1,
};
private _isHovering: boolean;
private _popoverCloseCheckIntervalId: number;
constructor(props: DropDownMenuItemProps) {
constructor(props: DropDownProps) {
super(props);
this.state = {
isDropDownOpen: false,
@@ -44,30 +45,35 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro
public componentWillUnmount() {
window.clearInterval(this._popoverCloseCheckIntervalId);
}
public componentWillReceiveProps(nextProps: DropDownProps) {
// HACK: If the popoverContent is updated to a different dimension and the users
// mouse is no longer above it, the dropdown can enter an inconsistent state where
// it believes the user is still hovering over it. In order to remedy this, we
// call hoverOff whenever the dropdown receives updated props. This is a hack
// because it will effectively close the dropdown on any prop update, barring
// dropdowns from having dynamic content.
this._onHoverOff();
}
public render() {
const colorStyle = this.props.isNightVersion ? 'white' : this.props.style.color;
return (
<div
style={{ ...this.props.style, color: colorStyle }}
style={{ ...this.props.style, width: 'fit-content', height: '100%' }}
onMouseEnter={this._onHover.bind(this)}
onMouseLeave={this._onHoverOff.bind(this)}
>
<div className="flex relative">
<div style={{ paddingRight: 10 }}>{this.props.title}</div>
<div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}>
<i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} />
</div>
</div>
{this.props.hoverActiveNode}
<Popover
open={this.state.isDropDownOpen}
anchorEl={this.state.anchorEl}
anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }}
targetOrigin={{ horizontal: 'middle', vertical: 'top' }}
anchorOrigin={this.props.anchorOrigin}
targetOrigin={this.props.targetOrigin}
onRequestClose={this._closePopover.bind(this)}
useLayerForClickAway={false}
animation={PopoverAnimationVertical}
zDepth={this.props.zDepth}
>
<div onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)}>
<Menu style={{ color: colors.grey }}>{this.props.subMenuItems}</Menu>
{this.props.popoverContent}
</div>
</Popover>
</div>
@@ -87,7 +93,7 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro
anchorEl: event.currentTarget,
});
}
private _onHoverOff(event: React.FormEvent<HTMLInputElement>) {
private _onHoverOff() {
this._isHovering = false;
}
private _checkIfShouldClosePopover() {

View File

@@ -6,16 +6,27 @@ import { Dispatch } from 'redux';
import { Portal as PortalComponent, PortalAllProps as PortalComponentAllProps } from 'ts/components/portal';
import { Dispatcher } from 'ts/redux/dispatcher';
import { State } from 'ts/redux/reducer';
import { BlockchainErrs, HashData, Order, ScreenWidths, Side, TokenByAddress, TokenStateByAddress } from 'ts/types';
import {
BlockchainErrs,
HashData,
Order,
ProviderType,
ScreenWidths,
Side,
TokenByAddress,
TokenStateByAddress,
} from 'ts/types';
import { constants } from 'ts/utils/constants';
interface ConnectedState {
blockchainErr: BlockchainErrs;
blockchainIsLoaded: boolean;
hashData: HashData;
injectedProviderName: string;
networkId: number;
nodeVersion: string;
orderFillAmount: BigNumber;
providerType: ProviderType;
tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress;
userEtherBalance: BigNumber;
@@ -57,10 +68,12 @@ const mapStateToProps = (state: State, ownProps: PortalComponentAllProps): Conne
return {
blockchainErr: state.blockchainErr,
blockchainIsLoaded: state.blockchainIsLoaded,
hashData,
injectedProviderName: state.injectedProviderName,
networkId: state.networkId,
nodeVersion: state.nodeVersion,
orderFillAmount: state.orderFillAmount,
hashData,
providerType: state.providerType,
screenWidth: state.screenWidth,
shouldBlockchainErrDialogBeOpen: state.shouldBlockchainErrDialogBeOpen,
tokenByAddress: state.tokenByAddress,

View File

@@ -2,7 +2,7 @@ import * as _ from 'lodash';
import * as React from 'react';
import * as DocumentTitle from 'react-document-title';
import { Footer } from 'ts/components/footer';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { Profile } from 'ts/pages/about/profile';
import { ProfileInfo, Styles } from 'ts/types';
import { colors } from 'ts/utils/colors';

View File

@@ -5,7 +5,7 @@ import * as React from 'react';
import DocumentTitle = require('react-document-title');
import { scroller } from 'react-scroll';
import semverSort = require('semver-sort');
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { Badge } from 'ts/components/ui/badge';
import { Comment } from 'ts/pages/documentation/comment';
import { DocsInfo } from 'ts/pages/documentation/docs_info';

View File

@@ -2,7 +2,7 @@ import * as _ from 'lodash';
import * as React from 'react';
import * as DocumentTitle from 'react-document-title';
import { Footer } from 'ts/components/footer';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { Question } from 'ts/pages/faq/question';
import { FAQQuestion, FAQSection, Styles, WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';

View File

@@ -4,7 +4,7 @@ import * as React from 'react';
import DocumentTitle = require('react-document-title');
import { Link } from 'react-router-dom';
import { Footer } from 'ts/components/footer';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { ScreenWidths, WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';

View File

@@ -1,7 +1,7 @@
import * as _ from 'lodash';
import * as React from 'react';
import { Footer } from 'ts/components/footer';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { Styles } from 'ts/types';
export interface NotFoundProps {

View File

@@ -3,7 +3,7 @@ import CircularProgress from 'material-ui/CircularProgress';
import * as React from 'react';
import DocumentTitle = require('react-document-title');
import { scroller } from 'react-scroll';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { MarkdownSection } from 'ts/pages/shared/markdown_section';
import { NestedSidebarMenu } from 'ts/pages/shared/nested_sidebar_menu';
import { SectionHeader } from 'ts/pages/shared/section_header';

View File

@@ -678,4 +678,9 @@ export enum SmartContractDocSections {
ZRXToken = 'ZRXToken',
}
export interface MaterialUIPosition {
vertical: 'bottom' | 'top' | 'center';
horizontal: 'left' | 'middle' | 'right';
}
// tslint:disable:max-file-line-count