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

@@ -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();