Have basic network switching working

This commit is contained in:
fragosti
2018-06-25 16:26:34 -07:00
parent 53e2cda4c8
commit c4e2dcafa4
6 changed files with 158 additions and 120 deletions

View File

@@ -50,6 +50,8 @@ import {
SideToAssetToken, SideToAssetToken,
Token, Token,
TokenByAddress, TokenByAddress,
InjectedProviderObservable,
InjectedProviderUpdate,
} from 'ts/types'; } from 'ts/types';
import { backendClient } from 'ts/utils/backend_client'; import { backendClient } from 'ts/utils/backend_client';
import { configs } from 'ts/utils/configs'; import { configs } from 'ts/utils/configs';
@@ -79,9 +81,8 @@ export class Blockchain {
private _dispatcher: Dispatcher; private _dispatcher: Dispatcher;
private _web3Wrapper?: Web3Wrapper; private _web3Wrapper?: Web3Wrapper;
private _blockchainWatcher?: BlockchainWatcher; private _blockchainWatcher?: BlockchainWatcher;
private _injectedProviderObservable?: InjectedProviderObservable;
private _userAddressIfExists: string; private _userAddressIfExists: string;
private _cachedProvider: Provider;
private _cachedProviderNetworkId: number;
private _ledgerSubprovider: LedgerSubprovider; private _ledgerSubprovider: LedgerSubprovider;
private _defaultGasPrice: BigNumber; private _defaultGasPrice: BigNumber;
private static _getNameGivenProvider(provider: Provider): string { private static _getNameGivenProvider(provider: Provider): string {
@@ -92,13 +93,62 @@ export class Blockchain {
} }
return providerNameIfExists; return providerNameIfExists;
} }
private static async _getProviderAsync(injectedWeb3: Web3, networkIdIfExists: number): Promise<Provider> { private static _getInjectedWeb3(): any {
return (window as any).web3;
}
private static _getFallbackNetworkId(): number {
return configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_KOVAN;
}
private static async _getInjectedWeb3ProviderNetworkIdIfExistsAsync(): Promise<number> {
// Hack: We need to know the networkId the injectedWeb3 is connected to (if it is defined) in
// order to properly instantiate the web3Wrapper. Since we must use the async call, we cannot
// retrieve it from within the web3Wrapper constructor. This is and should remain the only
// call to a web3 instance outside of web3Wrapper in the entire dapp.
// In addition, 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 injectedWeb3 = Blockchain._getInjectedWeb3();
let networkIdIfExists: number;
if (!_.isUndefined(injectedWeb3)) {
try {
networkIdIfExists = _.parseInt(await promisify<string>(injectedWeb3.version.getNetwork)());
} catch (err) {
// Ignore error and proceed with networkId undefined
}
}
return networkIdIfExists;
}
private static async _getProviderAsync(
injectedWeb3: Web3,
networkIdIfExists: number,
userLedgerProvider: boolean = false,
): Promise<Provider> {
const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3); const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3);
const isNetworkIdDefined = !_.isUndefined(networkIdIfExists);
const publicNodeUrlsIfExistsForNetworkId = configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists]; const publicNodeUrlsIfExistsForNetworkId = configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists];
const isPublicNodeAvailableForNetworkId = !_.isUndefined(publicNodeUrlsIfExistsForNetworkId); const isPublicNodeAvailableForNetworkId = !_.isUndefined(publicNodeUrlsIfExistsForNetworkId);
let provider; let provider;
if (doesInjectedWeb3Exist && isPublicNodeAvailableForNetworkId) { if (userLedgerProvider && isNetworkIdDefined) {
const isU2FSupported = await utils.isU2FSupportedAsync();
if (!isU2FSupported) {
throw new Error('Cannot update providerType to LEDGER without U2F support');
}
provider = new ProviderEngine();
const ledgerWalletConfigs = {
networkId: networkIdIfExists,
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
};
const ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
provider.addProvider(ledgerSubprovider);
provider.addProvider(new FilterSubprovider());
const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists], publicNodeUrl => {
return new RpcSubprovider({
rpcUrl: publicNodeUrl,
});
});
provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[]));
provider.start();
} else if (doesInjectedWeb3Exist && isPublicNodeAvailableForNetworkId) {
// We catch all requests involving a users account and send it to the injectedWeb3 // We catch all requests involving a users account and send it to the injectedWeb3
// instance. All other requests go to the public hosted node. // instance. All other requests go to the public hosted node.
provider = new ProviderEngine(); provider = new ProviderEngine();
@@ -120,7 +170,7 @@ export class Blockchain {
// injected into their browser. // injected into their browser.
provider = new ProviderEngine(); provider = new ProviderEngine();
provider.addProvider(new FilterSubprovider()); provider.addProvider(new FilterSubprovider());
const networkId = configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_KOVAN; const networkId = Blockchain._getFallbackNetworkId();
const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId], publicNodeUrl => { const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId], publicNodeUrl => {
return new RpcSubprovider({ return new RpcSubprovider({
rpcUrl: publicNodeUrl, rpcUrl: publicNodeUrl,
@@ -141,6 +191,7 @@ export class Blockchain {
// tslint:disable-next-line:no-floating-promises // tslint:disable-next-line:no-floating-promises
this._onPageLoadInitFireAndForgetAsync(); this._onPageLoadInitFireAndForgetAsync();
} }
// TODO: Investigate if we need this.
public async networkIdUpdatedFireAndForgetAsync(newNetworkId: number): Promise<void> { public async networkIdUpdatedFireAndForgetAsync(newNetworkId: number): Promise<void> {
const isConnected = !_.isUndefined(newNetworkId); const isConnected = !_.isUndefined(newNetworkId);
if (!isConnected) { if (!isConnected) {
@@ -185,84 +236,14 @@ export class Blockchain {
this._ledgerSubprovider.setPath(path); this._ledgerSubprovider.setPath(path);
} }
public async updateProviderToLedgerAsync(networkId: number): Promise<void> { public async updateProviderToLedgerAsync(networkId: number): Promise<void> {
utils.assert(!_.isUndefined(this._contractWrappers), 'Contract Wrappers must be instantiated.');
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
if (_.isUndefined(this._cachedProvider)) {
this._cachedProvider = this._web3Wrapper.getProvider();
this._cachedProviderNetworkId = this.networkId;
}
this._blockchainWatcher.destroy();
delete this._userAddressIfExists;
this._dispatcher.updateUserAddress(undefined); // 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());
const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId], publicNodeUrl => {
return new RpcSubprovider({
rpcUrl: publicNodeUrl,
});
});
provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[]));
provider.start();
this.networkId = networkId;
this._dispatcher.updateNetworkId(this.networkId);
const shouldPollUserAddress = false; const shouldPollUserAddress = false;
this._web3Wrapper = new Web3Wrapper(provider); const useLedgerProvider = true;
this._blockchainWatcher = new BlockchainWatcher( await this._resetOrInitializeAsync(networkId, shouldPollUserAddress, useLedgerProvider);
this._dispatcher,
this._web3Wrapper,
this.networkId,
shouldPollUserAddress,
);
this._contractWrappers.setProvider(provider, this.networkId);
await this._blockchainWatcher.startEmittingUserBalanceStateAsync();
this._dispatcher.updateProviderType(ProviderType.Ledger);
} }
public async updateProviderToInjectedAsync(): Promise<void> { public async updateProviderToInjectedAsync(): Promise<void> {
utils.assert(!_.isUndefined(this._contractWrappers), 'Contract Wrappers must be instantiated.');
if (_.isUndefined(this._cachedProvider)) {
return; // Going from injected to injected, so we noop
}
this._blockchainWatcher.destroy();
const provider = this._cachedProvider;
this.networkId = this._cachedProviderNetworkId;
const shouldPollUserAddress = true; const shouldPollUserAddress = true;
this._web3Wrapper = new Web3Wrapper(provider); const userLedgerProvider = false;
this._blockchainWatcher = new BlockchainWatcher( await this._resetOrInitializeAsync(this.networkId, shouldPollUserAddress, userLedgerProvider);
this._dispatcher,
this._web3Wrapper,
this.networkId,
shouldPollUserAddress,
);
const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync();
this._userAddressIfExists = userAddresses[0];
this._contractWrappers.setProvider(provider, this.networkId);
await this.fetchTokenInformationAsync();
await this._blockchainWatcher.startEmittingUserBalanceStateAsync();
this._dispatcher.updateProviderType(ProviderType.Injected);
delete this._ledgerSubprovider;
delete this._cachedProvider;
} }
public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> { public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> {
utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid); utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid);
@@ -632,6 +613,19 @@ export class Blockchain {
private _doesUserAddressExist(): boolean { private _doesUserAddressExist(): boolean {
return !_.isUndefined(this._userAddressIfExists); return !_.isUndefined(this._userAddressIfExists);
} }
private async _handleInjectedProviderUpdateAsync(update: InjectedProviderUpdate): Promise<void> {
if (update.networkVersion === 'loading') {
// Who comes up with this stuff.
return;
}
const updatedNetworkId = _.parseInt(update.networkVersion);
if (this.networkId === updatedNetworkId) {
return;
}
const shouldPollUserAddress = true;
const useLedgerProvider = false;
await this._resetOrInitializeAsync(updatedNetworkId, shouldPollUserAddress, useLedgerProvider);
}
private async _rehydrateStoreWithContractEventsAsync(): Promise<void> { private async _rehydrateStoreWithContractEventsAsync(): Promise<void> {
// Ensure we are only ever listening to one set of events // Ensure we are only ever listening to one set of events
this._stopWatchingExchangeLogFillEvents(); this._stopWatchingExchangeLogFillEvents();
@@ -774,36 +768,37 @@ export class Blockchain {
} }
private async _onPageLoadInitFireAndForgetAsync(): Promise<void> { private async _onPageLoadInitFireAndForgetAsync(): Promise<void> {
await utils.onPageLoadAsync(); // wait for page to load await utils.onPageLoadAsync(); // wait for page to load
const networkIdIfExists = await Blockchain._getInjectedWeb3ProviderNetworkIdIfExistsAsync();
// Hack: We need to know the networkId the injectedWeb3 is connected to (if it is defined) in this.networkId = !_.isUndefined(networkIdIfExists) ? networkIdIfExists : Blockchain._getFallbackNetworkId();
// order to properly instantiate the web3Wrapper. Since we must use the async call, we cannot const injectedWeb3 = Blockchain._getInjectedWeb3();
// retrieve it from within the web3Wrapper constructor. This is and should remain the only if (injectedWeb3) {
// call to a web3 instance outside of web3Wrapper in the entire dapp. const injectedProviderObservable = injectedWeb3.currentProvider.publicConfigStore;
// In addition, if the user has an injectedWeb3 instance that is disconnected from a backing if (injectedProviderObservable && !this._injectedProviderObservable) {
// Ethereum node, this call will throw. We need to handle this case gracefully this._injectedProviderObservable = injectedProviderObservable;
const injectedWeb3 = (window as any).web3; this._injectedProviderObservable.subscribe(this._handleInjectedProviderUpdateAsync.bind(this));
let networkIdIfExists: number;
if (!_.isUndefined(injectedWeb3)) {
try {
networkIdIfExists = _.parseInt(await promisify<string>(injectedWeb3.version.getNetwork)());
} catch (err) {
// Ignore error and proceed with networkId undefined
} }
} }
const provider = await Blockchain._getProviderAsync(injectedWeb3, networkIdIfExists);
this.networkId = !_.isUndefined(networkIdIfExists)
? networkIdIfExists
: configs.IS_MAINNET_ENABLED
? constants.NETWORK_ID_MAINNET
: constants.NETWORK_ID_KOVAN;
this._dispatcher.updateNetworkId(this.networkId);
const zeroExConfigs = {
networkId: this.networkId,
};
this._contractWrappers = new ContractWrappers(provider, zeroExConfigs);
this._updateProviderName(injectedWeb3);
const shouldPollUserAddress = true; const shouldPollUserAddress = true;
const shouldUseLedger = false;
await this._resetOrInitializeAsync(this.networkId, shouldPollUserAddress, shouldUseLedger);
}
private async _resetOrInitializeAsync(
networkId: number,
shouldPollUserAddress: boolean = false,
useLedgerProvider: boolean = false,
): Promise<void> {
this.networkId = networkId;
this._dispatcher.updateNetworkId(networkId);
const injectedWeb3 = Blockchain._getInjectedWeb3();
const provider = await Blockchain._getProviderAsync(injectedWeb3, networkId, useLedgerProvider);
// if (!_.isUndefined(this._contractWrappers)) {
// this._contractWrappers.setProvider(provider, networkId);
// } else {
// }
this._contractWrappers = new ContractWrappers(provider, { networkId });
if (!_.isUndefined(this._blockchainWatcher)) {
this._blockchainWatcher.destroy();
}
this._web3Wrapper = new Web3Wrapper(provider); this._web3Wrapper = new Web3Wrapper(provider);
this._blockchainWatcher = new BlockchainWatcher( this._blockchainWatcher = new BlockchainWatcher(
this._dispatcher, this._dispatcher,
@@ -811,10 +806,19 @@ export class Blockchain {
this.networkId, this.networkId,
shouldPollUserAddress, shouldPollUserAddress,
); );
if (useLedgerProvider) {
// TODO: why?
delete this._userAddressIfExists;
this._dispatcher.updateUserAddress(undefined);
this._dispatcher.updateProviderType(ProviderType.Ledger);
} else {
delete this._ledgerSubprovider;
const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync(); const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync();
this._userAddressIfExists = userAddresses[0]; this._userAddressIfExists = userAddresses[0];
this._dispatcher.updateUserAddress(this._userAddressIfExists); this._dispatcher.updateUserAddress(this._userAddressIfExists);
this._updateProviderName(injectedWeb3);
}
// TOOD: should not call this in ledger case?
await this.fetchTokenInformationAsync(); await this.fetchTokenInformationAsync();
await this._blockchainWatcher.startEmittingUserBalanceStateAsync(); await this._blockchainWatcher.startEmittingUserBalanceStateAsync();
await this._rehydrateStoreWithContractEventsAsync(); await this._rehydrateStoreWithContractEventsAsync();

View File

@@ -7,6 +7,7 @@ export class BlockchainWatcher {
private _dispatcher: Dispatcher; private _dispatcher: Dispatcher;
private _web3Wrapper: Web3Wrapper; private _web3Wrapper: Web3Wrapper;
private _prevNetworkId: number; private _prevNetworkId: number;
private _isWatchingNetworkId: boolean = false;
private _shouldPollUserAddress: boolean; private _shouldPollUserAddress: boolean;
private _watchBalanceIntervalId: NodeJS.Timer; private _watchBalanceIntervalId: NodeJS.Timer;
private _prevUserEtherBalanceInWei?: BigNumber; private _prevUserEtherBalanceInWei?: BigNumber;
@@ -18,8 +19,8 @@ export class BlockchainWatcher {
shouldPollUserAddress: boolean, shouldPollUserAddress: boolean,
) { ) {
this._dispatcher = dispatcher; this._dispatcher = dispatcher;
this._prevNetworkId = networkIdIfExists;
this._shouldPollUserAddress = shouldPollUserAddress; this._shouldPollUserAddress = shouldPollUserAddress;
this._prevNetworkId = networkIdIfExists;
this._web3Wrapper = web3Wrapper; this._web3Wrapper = web3Wrapper;
} }
public destroy(): void { public destroy(): void {
@@ -34,6 +35,16 @@ export class BlockchainWatcher {
public updatePrevUserAddress(userAddress: string): void { public updatePrevUserAddress(userAddress: string): void {
this._prevUserAddressIfExists = userAddress; this._prevUserAddressIfExists = userAddress;
} }
// public async startEmittingInjectedProviderNetworkIdAsync(injectedProvider: any): Promise<void> {
// if (this._isWatchingNetworkId) {
// return; // we are already watching the network id
// }
// const observable = injectedProvider.publicConfigStore;
// if (observable && observable.subscribe) {
// observable.subscribe(this._handleInjectedProviderUpdate.bind(this));
// this._isWatchingNetworkId = true;
// }
// }
public async startEmittingUserBalanceStateAsync(): Promise<void> { public async startEmittingUserBalanceStateAsync(): Promise<void> {
if (!_.isUndefined(this._watchBalanceIntervalId)) { if (!_.isUndefined(this._watchBalanceIntervalId)) {
return; // we are already emitting the state return; // we are already emitting the state
@@ -49,6 +60,15 @@ export class BlockchainWatcher {
}, },
); );
} }
// private _handleInjectedProviderUpdate(update: InjectedProviderUpdate): void {
// const updatedNetworkId = _.parseInt(update.networkVersion);
// if (this._prevNetworkId === updatedNetworkId) {
// return;
// }
// this._prevNetworkId = updatedNetworkId;
// this._dispatcher.updateNetworkId(updatedNetworkId);
// this._updateBalanceAsync();
// }
private async _updateBalanceAsync(): Promise<void> { private async _updateBalanceAsync(): Promise<void> {
let prevNodeVersion: string; let prevNodeVersion: string;
// Check for node version changes // Check for node version changes

View File

@@ -159,9 +159,11 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp
const ethToken = utils.getEthToken(this.props.tokenByAddress); const ethToken = utils.getEthToken(this.props.tokenByAddress);
const zrxToken = utils.getZrxToken(this.props.tokenByAddress); const zrxToken = utils.getZrxToken(this.props.tokenByAddress);
if (ethToken && zrxToken) { if (ethToken && zrxToken) {
const ethTokenAllowance = this.props.trackedTokenStateByAddress[ethToken.address].allowance; const ethTokenState = this.props.trackedTokenStateByAddress[ethToken.address];
const zrxTokenAllowance = this.props.trackedTokenStateByAddress[zrxToken.address].allowance; const zrxTokenState = this.props.trackedTokenStateByAddress[zrxToken.address];
return ethTokenAllowance > new BigNumber(0) && zrxTokenAllowance > new BigNumber(0); if (ethTokenState && zrxTokenState) {
return ethTokenState.allowance > new BigNumber(0) && zrxTokenState.allowance > new BigNumber(0);
}
} }
return false; return false;
} }
@@ -222,6 +224,9 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp
return null; return null;
} }
const tokenState = this.props.trackedTokenStateByAddress[token.address]; const tokenState = this.props.trackedTokenStateByAddress[token.address];
if (!tokenState) {
return null;
}
return ( return (
<AllowanceToggle <AllowanceToggle
token={token} token={token}

View File

@@ -151,11 +151,11 @@ export class Portal extends React.Component<PortalProps, PortalState> {
this.props.dispatcher.resetState(); this.props.dispatcher.resetState();
} }
public componentDidUpdate(prevProps: PortalProps): void { public componentDidUpdate(prevProps: PortalProps): void {
if (!prevProps.blockchainIsLoaded && this.props.blockchainIsLoaded) { // if (!prevProps.blockchainIsLoaded && this.props.blockchainIsLoaded) {
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); // const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
// tslint:disable-next-line:no-floating-promises // // tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); // this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
} // }
} }
public componentWillReceiveProps(nextProps: PortalProps): void { public componentWillReceiveProps(nextProps: PortalProps): void {
if (nextProps.networkId !== this.state.prevNetworkId) { if (nextProps.networkId !== this.state.prevNetworkId) {

View File

@@ -58,7 +58,6 @@ const styles: Styles = {
width: '100%', width: '100%',
position: 'relative', position: 'relative',
top: 0, top: 0,
zIndex: zIndex.topBar,
paddingBottom: 1, paddingBottom: 1,
}, },
bottomBar: { bottomBar: {

View File

@@ -488,6 +488,16 @@ export enum Providers {
Mist = 'MIST', Mist = 'MIST',
} }
export interface InjectedProviderUpdate {
selectedAddress: string;
networkVersion: string;
}
export interface InjectedProviderObservable {
subscribe(updateHandler: (update: InjectedProviderUpdate) => void): void;
unsubscribe(updateHandler: (update: InjectedProviderUpdate) => void): void;
}
export interface TimestampMsRange { export interface TimestampMsRange {
startTimestampMs: number; startTimestampMs: number;
endTimestampMs: number; endTimestampMs: number;