import { BlockRange, ContractWrappers, DecodedLogEvent, ExchangeContractEventArgs, ExchangeEvents, IndexedFilterValues, LogCancelContractEventArgs, LogFillContractEventArgs, Token as ZeroExToken, } from '@0xproject/contract-wrappers'; import { isValidOrderHash, signOrderHashAsync } from '@0xproject/order-utils'; import { EtherscanLinkSuffixes, utils as sharedUtils } from '@0xproject/react-shared'; import { ledgerEthereumBrowserClientFactoryAsync, LedgerSubprovider, RedundantSubprovider, SignerSubprovider, Subprovider, } from '@0xproject/subproviders'; import { BlockParam, ECSignature, LogWithDecodedArgs, Order, Provider, SignedOrder, TransactionReceiptWithDecodedLogs, } from '@0xproject/types'; import { BigNumber, intervalUtils, logUtils, promisify } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as _ from 'lodash'; import * as moment from 'moment'; import * as React from 'react'; import contract = require('truffle-contract'); import { BlockchainWatcher } from 'ts/blockchain_watcher'; import { TokenSendCompleted } from 'ts/components/flash_messages/token_send_completed'; import { TransactionSubmitted } from 'ts/components/flash_messages/transaction_submitted'; import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; import { tradeHistoryStorage } from 'ts/local_storage/trade_history_storage'; import { Dispatcher } from 'ts/redux/dispatcher'; import { BlockchainCallErrs, BlockchainErrs, ContractInstance, Fill, InjectedProviderObservable, InjectedProviderUpdate, InjectedWeb3, Order as PortalOrder, Providers, ProviderType, Side, SideToAssetToken, Token, TokenByAddress, } from 'ts/types'; import { backendClient } from 'ts/utils/backend_client'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; import { errorReporter } from 'ts/utils/error_reporter'; import { utils } from 'ts/utils/utils'; import ProviderEngine = require('web3-provider-engine'); import FilterSubprovider = require('web3-provider-engine/subproviders/filters'); import RpcSubprovider = require('web3-provider-engine/subproviders/rpc'); import * as MintableArtifacts from '../contracts/Mintable.json'; // HACK: remove this hard-coded abi and use @0xproject/contract-wrappers import * as Exchange from './artifacts/Exchange.json'; const BLOCK_NUMBER_BACK_TRACK = 50; const GWEI_IN_WEI = 1000000000; 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.Toshi]: constants.PROVIDER_NAME_TOSHI, [Providers.Cipher]: constants.PROVIDER_NAME_CIPHER, }; export class Blockchain { public networkId: number; public nodeVersion: string; private _contractWrappers: ContractWrappers; private _dispatcher: Dispatcher; private _web3Wrapper?: Web3Wrapper; private _blockchainWatcher?: BlockchainWatcher; private _injectedProviderObservable?: InjectedProviderObservable; private _injectedProviderUpdateHandler: (update: InjectedProviderUpdate) => Promise; private _userAddressIfExists: string; private _ledgerSubprovider: LedgerSubprovider; private _defaultGasPrice: BigNumber; private _watchGasPriceIntervalId: NodeJS.Timer; private static _getNameGivenProvider(provider: Provider): string { const providerType = utils.getProviderType(provider); const providerNameIfExists = providerToName[providerType]; if (_.isUndefined(providerNameIfExists)) { return constants.PROVIDER_NAME_GENERIC; } return providerNameIfExists; } private static _getInjectedWeb3(): InjectedWeb3 { const injectedWeb3IfExists = (window as any).web3; // Our core assumptions about the injected web3 object is that it has the following // properties and methods. if ( _.isUndefined(injectedWeb3IfExists) || _.isUndefined(injectedWeb3IfExists.version) || _.isUndefined(injectedWeb3IfExists.version.getNetwork) || _.isUndefined(injectedWeb3IfExists.currentProvider) ) { return undefined; } return injectedWeb3IfExists; } private static async _getInjectedWeb3ProviderNetworkIdIfExistsAsync(): Promise { // 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 injectedWeb3IfExists = Blockchain._getInjectedWeb3(); let networkIdIfExists: number; if (!_.isUndefined(injectedWeb3IfExists)) { try { networkIdIfExists = _.parseInt(await promisify(injectedWeb3IfExists.version.getNetwork)()); } catch (err) { // Ignore error and proceed with networkId undefined } } return networkIdIfExists; } private static async _getProviderAsync( injectedWeb3: InjectedWeb3, networkIdIfExists: number, shouldUserLedgerProvider: boolean = false, ): Promise<[Provider, LedgerSubprovider | undefined]> { const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3); const isNetworkIdAvailable = !_.isUndefined(networkIdIfExists); const publicNodeUrlsIfExistsForNetworkId = configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists]; const isPublicNodeAvailableForNetworkId = !_.isUndefined(publicNodeUrlsIfExistsForNetworkId); if (shouldUserLedgerProvider && isNetworkIdAvailable) { const isU2FSupported = await utils.isU2FSupportedAsync(); if (!isU2FSupported) { throw new Error('Cannot update providerType to LEDGER without U2F support'); } const 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(); return [provider, ledgerSubprovider]; } else if (doesInjectedWeb3Exist && 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 provider = new ProviderEngine(); provider.addProvider(new SignerSubprovider(injectedWeb3.currentProvider)); provider.addProvider(new FilterSubprovider()); const rpcSubproviders = _.map(publicNodeUrlsIfExistsForNetworkId, publicNodeUrl => { return new RpcSubprovider({ rpcUrl: publicNodeUrl, }); }); provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[])); provider.start(); return [provider, undefined]; } else if (doesInjectedWeb3Exist) { // Since no public node for this network, all requests go to injectedWeb3 instance return [injectedWeb3.currentProvider, 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 provider = new ProviderEngine(); provider.addProvider(new FilterSubprovider()); const networkId = constants.NETWORK_ID_MAINNET; 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(); return [provider, undefined]; } } constructor(dispatcher: Dispatcher) { this._dispatcher = dispatcher; const defaultGasPrice = GWEI_IN_WEI * 40; this._defaultGasPrice = new BigNumber(defaultGasPrice); // We need a unique reference to this function so we can use it to unsubcribe. this._injectedProviderUpdateHandler = this._handleInjectedProviderUpdateAsync.bind(this); // tslint:disable-next-line:no-floating-promises this._onPageLoadInitFireAndForgetAsync(); } public async networkIdUpdatedFireAndForgetAsync(newNetworkId: number): Promise { const isConnected = !_.isUndefined(newNetworkId); if (!isConnected) { this.networkId = newNetworkId; this._dispatcher.encounteredBlockchainError(BlockchainErrs.DisconnectedFromEthereumNode); this._dispatcher.updateShouldBlockchainErrDialogBeOpen(true); } else if (this.networkId !== newNetworkId) { this.networkId = newNetworkId; this._dispatcher.encounteredBlockchainError(BlockchainErrs.NoError); await this.fetchTokenInformationAsync(); await this._rehydrateStoreWithContractEventsAsync(); } } public async userAddressUpdatedFireAndForgetAsync(newUserAddress: string): Promise { if (this._userAddressIfExists !== newUserAddress) { this._userAddressIfExists = newUserAddress; await this.fetchTokenInformationAsync(); await this._rehydrateStoreWithContractEventsAsync(); } } public async nodeVersionUpdatedFireAndForgetAsync(nodeVersion: string): Promise { if (this.nodeVersion !== nodeVersion) { this.nodeVersion = nodeVersion; } } public async isAddressInTokenRegistryAsync(tokenAddress: string): Promise { utils.assert(!_.isUndefined(this._contractWrappers), 'Contract Wrappers must be instantiated.'); const tokenIfExists = await this._contractWrappers.tokenRegistry.getTokenIfExistsAsync(tokenAddress); return !_.isUndefined(tokenIfExists); } public getLedgerDerivationPathIfExists(): string { if (_.isUndefined(this._ledgerSubprovider)) { return undefined; } const path = this._ledgerSubprovider.getPath(); return path; } public updateLedgerDerivationPathIfExists(path: string): void { if (_.isUndefined(this._ledgerSubprovider)) { return; // noop } this._ledgerSubprovider.setPath(path); } public async updateProviderToLedgerAsync(networkId: number): Promise { const shouldPollUserAddress = false; const shouldUserLedgerProvider = true; await this._resetOrInitializeAsync(networkId, shouldPollUserAddress, shouldUserLedgerProvider); } public async updateProviderToInjectedAsync(): Promise { const shouldPollUserAddress = true; const shouldUserLedgerProvider = false; this._dispatcher.updateBlockchainIsLoaded(false); // We don't want to be out of sync with the network the injected provider declares. const networkId = await Blockchain._getInjectedWeb3ProviderNetworkIdIfExistsAsync(); await this._resetOrInitializeAsync(networkId, shouldPollUserAddress, shouldUserLedgerProvider); } public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise { utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid); utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); utils.assert(!_.isUndefined(this._contractWrappers), 'Contract Wrappers must be instantiated.'); this._showFlashMessageIfLedger(); const txHash = await this._contractWrappers.token.setProxyAllowanceAsync( token.address, this._userAddressIfExists, amountInBaseUnits, { gasPrice: this._defaultGasPrice, }, ); await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); } public async transferAsync(token: Token, toAddress: string, amountInBaseUnits: BigNumber): Promise { utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); this._showFlashMessageIfLedger(); const txHash = await this._contractWrappers.token.transferAsync( token.address, this._userAddressIfExists, toAddress, amountInBaseUnits, { gasPrice: this._defaultGasPrice, }, ); await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); const etherScanLinkIfExists = sharedUtils.getEtherScanLinkIfExists( txHash, this.networkId, EtherscanLinkSuffixes.Tx, ); this._dispatcher.showFlashMessage( React.createElement(TokenSendCompleted, { etherScanLinkIfExists, token, toAddress, amountInBaseUnits, }), ); } public portalOrderToZeroExOrder(portalOrder: PortalOrder): SignedOrder { const exchangeContractAddress = this.getExchangeContractAddressIfExists(); const zeroExSignedOrder = { exchangeContractAddress, maker: portalOrder.signedOrder.maker, taker: portalOrder.signedOrder.taker, makerTokenAddress: portalOrder.signedOrder.makerTokenAddress, takerTokenAddress: portalOrder.signedOrder.takerTokenAddress, makerTokenAmount: new BigNumber(portalOrder.signedOrder.makerTokenAmount), takerTokenAmount: new BigNumber(portalOrder.signedOrder.takerTokenAmount), makerFee: new BigNumber(portalOrder.signedOrder.makerFee), takerFee: new BigNumber(portalOrder.signedOrder.takerFee), expirationUnixTimestampSec: new BigNumber(portalOrder.signedOrder.expirationUnixTimestampSec), feeRecipient: portalOrder.signedOrder.feeRecipient, ecSignature: portalOrder.signedOrder.ecSignature, salt: new BigNumber(portalOrder.signedOrder.salt), }; return zeroExSignedOrder; } public async fillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber): Promise { utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); const shouldThrowOnInsufficientBalanceOrAllowance = true; this._showFlashMessageIfLedger(); const txHash = await this._contractWrappers.exchange.fillOrderAsync( signedOrder, fillTakerTokenAmount, shouldThrowOnInsufficientBalanceOrAllowance, this._userAddressIfExists, { gasPrice: this._defaultGasPrice, }, ); const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); const logs: Array> = receipt.logs as any; this._contractWrappers.exchange.throwLogErrorsAsErrors(logs); const logFill = _.find(logs, { event: 'LogFill' }); const args = (logFill.args as any) as LogFillContractEventArgs; const filledTakerTokenAmount = args.filledTakerTokenAmount; return filledTakerTokenAmount; } public async cancelOrderAsync(signedOrder: SignedOrder, cancelTakerTokenAmount: BigNumber): Promise { this._showFlashMessageIfLedger(); const txHash = await this._contractWrappers.exchange.cancelOrderAsync(signedOrder, cancelTakerTokenAmount, { gasPrice: this._defaultGasPrice, }); const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); const logs: Array> = receipt.logs as any; this._contractWrappers.exchange.throwLogErrorsAsErrors(logs); const logCancel = _.find(logs, { event: ExchangeEvents.LogCancel }); const args = (logCancel.args as any) as LogCancelContractEventArgs; const cancelledTakerTokenAmount = args.cancelledTakerTokenAmount; return cancelledTakerTokenAmount; } public async getUnavailableTakerAmountAsync(orderHash: string): Promise { utils.assert(isValidOrderHash(orderHash), 'Must be valid orderHash'); utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); const unavailableTakerAmount = await this._contractWrappers.exchange.getUnavailableTakerAmountAsync(orderHash); return unavailableTakerAmount; } public getExchangeContractAddressIfExists(): string | undefined { return this._contractWrappers.exchange.getContractAddress(); } public async validateFillOrderThrowIfInvalidAsync( signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber, takerAddress: string, ): Promise { await this._contractWrappers.exchange.validateFillOrderThrowIfInvalidAsync( signedOrder, fillTakerTokenAmount, takerAddress, ); } public async validateCancelOrderThrowIfInvalidAsync( order: Order, cancelTakerTokenAmount: BigNumber, ): Promise { await this._contractWrappers.exchange.validateCancelOrderThrowIfInvalidAsync(order, cancelTakerTokenAmount); } public isValidAddress(address: string): boolean { const lowercaseAddress = address.toLowerCase(); return Web3Wrapper.isAddress(lowercaseAddress); } public async pollTokenBalanceAsync(token: Token): Promise { utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); const [currBalance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddressIfExists, token.address); const newTokenBalancePromise = new Promise((resolve: (balance: BigNumber) => void, reject) => { const tokenPollInterval = intervalUtils.setAsyncExcludingInterval( async () => { const [balance] = await this.getTokenBalanceAndAllowanceAsync( this._userAddressIfExists, token.address, ); if (!balance.eq(currBalance)) { intervalUtils.clearAsyncExcludingInterval(tokenPollInterval); resolve(balance); } }, 5000, (err: Error) => { logUtils.log(`Polling tokenBalance failed: ${err}`); intervalUtils.clearAsyncExcludingInterval(tokenPollInterval); reject(err); }, ); }); return newTokenBalancePromise; } public async signOrderHashAsync(orderHash: string): Promise { utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); const makerAddress = this._userAddressIfExists; // If makerAddress is undefined, this means they have a web3 instance injected into their browser // but no account addresses associated with it. if (_.isUndefined(makerAddress)) { throw new Error('Tried to send a sign request but user has no associated addresses'); } this._showFlashMessageIfLedger(); const nodeVersion = await this._web3Wrapper.getNodeVersionAsync(); const isParityNode = utils.isParityNode(nodeVersion); const isTestRpc = utils.isTestRpc(nodeVersion); const isLedgerSigner = !_.isUndefined(this._ledgerSubprovider); let shouldAddPersonalMessagePrefix = true; if ((isParityNode && !isLedgerSigner) || isTestRpc || isLedgerSigner) { shouldAddPersonalMessagePrefix = false; } const provider = this._contractWrappers.getProvider(); const ecSignature = await signOrderHashAsync(provider, orderHash, makerAddress, shouldAddPersonalMessagePrefix); this._dispatcher.updateECSignature(ecSignature); return ecSignature; } public async mintTestTokensAsync(token: Token): Promise { utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); const mintableContract = await this._instantiateContractIfExistsAsync(MintableArtifacts, token.address); this._showFlashMessageIfLedger(); await mintableContract.mint(constants.MINT_AMOUNT, { from: this._userAddressIfExists, gasPrice: this._defaultGasPrice, }); } public async getBalanceInWeiAsync(owner: string): Promise { const balanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(owner); return balanceInWei; } public async convertEthToWrappedEthTokensAsync(etherTokenAddress: string, amount: BigNumber): Promise { utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); this._showFlashMessageIfLedger(); const txHash = await this._contractWrappers.etherToken.depositAsync( etherTokenAddress, amount, this._userAddressIfExists, { gasPrice: this._defaultGasPrice, }, ); await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); } public async convertWrappedEthTokensToEthAsync(etherTokenAddress: string, amount: BigNumber): Promise { utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); this._showFlashMessageIfLedger(); const txHash = await this._contractWrappers.etherToken.withdrawAsync( etherTokenAddress, amount, this._userAddressIfExists, { gasPrice: this._defaultGasPrice, }, ); await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); } public async doesContractExistAtAddressAsync(address: string): Promise { const doesContractExist = await this._web3Wrapper.doesContractExistAtAddressAsync(address); return doesContractExist; } public async getCurrentUserTokenBalanceAndAllowanceAsync(tokenAddress: string): Promise { utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); const tokenBalanceAndAllowance = await this.getTokenBalanceAndAllowanceAsync( this._userAddressIfExists, tokenAddress, ); return tokenBalanceAndAllowance; } public async getTokenBalanceAndAllowanceAsync( ownerAddressIfExists: string, tokenAddress: string, ): Promise<[BigNumber, BigNumber]> { utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); if (_.isUndefined(ownerAddressIfExists)) { const zero = new BigNumber(0); return [zero, zero]; } let balance = new BigNumber(0); let allowance = new BigNumber(0); if (this._doesUserAddressExist()) { [balance, allowance] = await Promise.all([ this._contractWrappers.token.getBalanceAsync(tokenAddress, ownerAddressIfExists), this._contractWrappers.token.getProxyAllowanceAsync(tokenAddress, ownerAddressIfExists), ]); } return [balance, allowance]; } public async getUserAccountsAsync(): Promise { utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); const provider = this._contractWrappers.getProvider(); const web3Wrapper = new Web3Wrapper(provider); const userAccountsIfExists = await web3Wrapper.getAvailableAddressesAsync(); return userAccountsIfExists; } // HACK: When a user is using a Ledger, we simply dispatch the selected userAddress, which // by-passes the web3Wrapper logic for updating the prevUserAddress. We therefore need to // manually update it. This should only be called by the LedgerConfigDialog. public updateWeb3WrapperPrevUserAddress(newUserAddress: string): void { this._blockchainWatcher.updatePrevUserAddress(newUserAddress); } public destroy(): void { this._blockchainWatcher.destroy(); if (this._injectedProviderObservable) { this._injectedProviderObservable.unsubscribe(this._injectedProviderUpdateHandler); } this._stopWatchingExchangeLogFillEvents(); this._stopWatchingGasPrice(); } public async fetchTokenInformationAsync(): Promise { utils.assert( !_.isUndefined(this.networkId), 'Cannot call fetchTokenInformationAsync if disconnected from Ethereum node', ); this._dispatcher.updateBlockchainIsLoaded(false); const tokenRegistryTokensByAddress = await this._getTokenRegistryTokensByAddressAsync(); const trackedTokensByAddress = _.isUndefined(this._userAddressIfExists) ? {} : trackedTokenStorage.getTrackedTokensByAddress(this._userAddressIfExists, this.networkId); const tokenRegistryTokens = _.values(tokenRegistryTokensByAddress); const tokenRegistryTokenSymbols = _.map(tokenRegistryTokens, t => t.symbol); const defaultTrackedTokensInRegistry = _.intersection( tokenRegistryTokenSymbols, configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, ); const currentTimestamp = moment().unix(); if (defaultTrackedTokensInRegistry.length !== configs.DEFAULT_TRACKED_TOKEN_SYMBOLS.length) { this._dispatcher.updateShouldBlockchainErrDialogBeOpen(true); this._dispatcher.encounteredBlockchainError(BlockchainErrs.DefaultTokensNotInTokenRegistry); const err = new Error( `Default tracked tokens (${JSON.stringify( configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, )}) not found in tokenRegistry: ${JSON.stringify(tokenRegistryTokens)}`, ); await errorReporter.reportAsync(err); return; } if (_.isEmpty(trackedTokensByAddress)) { _.each(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, symbol => { const token = _.find(tokenRegistryTokens, t => t.symbol === symbol); token.trackedTimestamp = currentTimestamp; trackedTokensByAddress[token.address] = token; }); if (!_.isUndefined(this._userAddressIfExists)) { _.each(trackedTokensByAddress, (token: Token) => { trackedTokenStorage.addTrackedTokenToUser(this._userAddressIfExists, this.networkId, token); }); } } else { // Properly set all tokenRegistry tokens `trackedTimestamp` if they are in the existing trackedTokens array _.each(trackedTokensByAddress, (trackedToken: Token, address: string) => { if (!_.isUndefined(tokenRegistryTokensByAddress[address])) { tokenRegistryTokensByAddress[address].trackedTimestamp = trackedToken.trackedTimestamp; } }); } const allTokensByAddress = { ...tokenRegistryTokensByAddress, ...trackedTokensByAddress, }; const allTokens = _.values(allTokensByAddress); const mostPopularTradingPairTokens: Token[] = [ _.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[0] }), _.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[1] }), ]; const sideToAssetToken: SideToAssetToken = { [Side.Deposit]: { address: mostPopularTradingPairTokens[0].address, }, [Side.Receive]: { address: mostPopularTradingPairTokens[1].address, }, }; this._dispatcher.batchDispatch(allTokensByAddress, this.networkId, this._userAddressIfExists, sideToAssetToken); this._dispatcher.updateBlockchainIsLoaded(true); } private async _showEtherScanLinkAndAwaitTransactionMinedAsync( txHash: string, ): Promise { const etherScanLinkIfExists = sharedUtils.getEtherScanLinkIfExists( txHash, this.networkId, EtherscanLinkSuffixes.Tx, ); this._dispatcher.showFlashMessage( React.createElement(TransactionSubmitted, { etherScanLinkIfExists, }), ); const provider = this._contractWrappers.getProvider(); const web3Wrapper = new Web3Wrapper(provider); // HACK: remove this hard-coded abi and use @0xproject/contract-wrappers const exchangeAbi = _.get(Exchange, 'abi', []); web3Wrapper.abiDecoder.addABI(exchangeAbi); const receipt = await web3Wrapper.awaitTransactionSuccessAsync(txHash); return receipt; } private _doesUserAddressExist(): boolean { return !_.isUndefined(this._userAddressIfExists); } private async _handleInjectedProviderUpdateAsync(update: InjectedProviderUpdate): Promise { if (update.networkVersion === 'loading' || !_.isUndefined(this._ledgerSubprovider)) { return; } const updatedNetworkId = _.parseInt(update.networkVersion); if (this.networkId === updatedNetworkId) { return; } const shouldPollUserAddress = true; const shouldUserLedgerProvider = false; await this._resetOrInitializeAsync(updatedNetworkId, shouldPollUserAddress, shouldUserLedgerProvider); } private async _rehydrateStoreWithContractEventsAsync(): Promise { // Ensure we are only ever listening to one set of events this._stopWatchingExchangeLogFillEvents(); if (!this._doesUserAddressExist()) { return; // short-circuit } if (!_.isUndefined(this._contractWrappers)) { // Since we do not have an index on the `taker` address and want to show // transactions where an account is either the `maker` or `taker`, we loop // through all fill events, and filter/cache them client-side. const filterIndexObj = {}; await this._startListeningForExchangeLogFillEventsAsync(filterIndexObj); } } private async _startListeningForExchangeLogFillEventsAsync(indexFilterValues: IndexedFilterValues): Promise { utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); // Fetch historical logs await this._fetchHistoricalExchangeLogFillEventsAsync(indexFilterValues); // Start a subscription for new logs this._contractWrappers.exchange.subscribe( ExchangeEvents.LogFill, indexFilterValues, async (err: Error, decodedLogEvent: DecodedLogEvent) => { if (err) { // Note: it's not entirely clear from the documentation which // errors will be thrown by `watch`. For now, let's log the error // to rollbar and stop watching when one occurs // tslint:disable-next-line:no-floating-promises errorReporter.reportAsync(err); // fire and forget return; } else { const decodedLog = decodedLogEvent.log; if (!this._doesLogEventInvolveUser(decodedLog)) { return; // We aren't interested in the fill event } this._updateLatestFillsBlockIfNeeded(decodedLog.blockNumber); const fill = await this._convertDecodedLogToFillAsync(decodedLog); if (decodedLogEvent.isRemoved) { tradeHistoryStorage.removeFillFromUser(this._userAddressIfExists, this.networkId, fill); } else { tradeHistoryStorage.addFillToUser(this._userAddressIfExists, this.networkId, fill); } } }, ); } private async _fetchHistoricalExchangeLogFillEventsAsync(indexFilterValues: IndexedFilterValues): Promise { utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); const fromBlock = tradeHistoryStorage.getFillsLatestBlock(this._userAddressIfExists, this.networkId); const blockRange: BlockRange = { fromBlock, toBlock: 'latest' as BlockParam, }; const decodedLogs = await this._contractWrappers.exchange.getLogsAsync( ExchangeEvents.LogFill, blockRange, indexFilterValues, ); for (const decodedLog of decodedLogs) { if (!this._doesLogEventInvolveUser(decodedLog)) { continue; // We aren't interested in the fill event } this._updateLatestFillsBlockIfNeeded(decodedLog.blockNumber); const fill = await this._convertDecodedLogToFillAsync(decodedLog); tradeHistoryStorage.addFillToUser(this._userAddressIfExists, this.networkId, fill); } } private async _convertDecodedLogToFillAsync( decodedLog: LogWithDecodedArgs, ): Promise { const args = decodedLog.args; const blockTimestamp = await this._web3Wrapper.getBlockTimestampAsync(decodedLog.blockHash); const fill = { filledTakerTokenAmount: args.filledTakerTokenAmount, filledMakerTokenAmount: args.filledMakerTokenAmount, logIndex: decodedLog.logIndex, maker: args.maker, orderHash: args.orderHash, taker: args.taker, makerToken: args.makerToken, takerToken: args.takerToken, paidMakerFee: args.paidMakerFee, paidTakerFee: args.paidTakerFee, transactionHash: decodedLog.transactionHash, blockTimestamp, }; return fill; } private _doesLogEventInvolveUser(decodedLog: LogWithDecodedArgs): boolean { const args = decodedLog.args; const isUserMakerOrTaker = args.maker === this._userAddressIfExists || args.taker === this._userAddressIfExists; return isUserMakerOrTaker; } private _updateLatestFillsBlockIfNeeded(blockNumber: number): void { utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); const isBlockPending = _.isNull(blockNumber); if (!isBlockPending) { // Hack: I've observed the behavior where a client won't register certain fill events // and lowering the cache blockNumber fixes the issue. As a quick fix for now, simply // set the cached blockNumber 50 below the one returned. This way, upon refreshing, a user // would still attempt to re-fetch events from the previous 50 blocks, but won't need to // re-fetch all events in all blocks. // TODO: Debug if this is a race condition, and apply a more precise fix const blockNumberToSet = blockNumber - BLOCK_NUMBER_BACK_TRACK < 0 ? 0 : blockNumber - BLOCK_NUMBER_BACK_TRACK; tradeHistoryStorage.setFillsLatestBlock(this._userAddressIfExists, this.networkId, blockNumberToSet); } } private _stopWatchingExchangeLogFillEvents(): void { this._contractWrappers.exchange.unsubscribeAll(); } private async _getTokenRegistryTokensByAddressAsync(): Promise { utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); const tokenRegistryTokens = await this._contractWrappers.tokenRegistry.getTokensAsync(); const tokenByAddress: TokenByAddress = {}; _.each(tokenRegistryTokens, (t: ZeroExToken) => { // HACK: For now we have a hard-coded list of iconUrls for the dummyTokens // TODO: Refactor this out and pull the iconUrl directly from the TokenRegistry const iconUrl = utils.getTokenIconUrl(t.symbol); const token: Token = { iconUrl, address: t.address, name: t.name, symbol: t.symbol, decimals: t.decimals, trackedTimestamp: undefined, isRegistered: true, }; tokenByAddress[token.address] = token; }); return tokenByAddress; } private async _onPageLoadInitFireAndForgetAsync(): Promise { await utils.onPageLoadAsync(); // wait for page to load const networkIdIfExists = await Blockchain._getInjectedWeb3ProviderNetworkIdIfExistsAsync(); this.networkId = !_.isUndefined(networkIdIfExists) ? networkIdIfExists : constants.NETWORK_ID_MAINNET; const injectedWeb3IfExists = Blockchain._getInjectedWeb3(); if (!_.isUndefined(injectedWeb3IfExists) && !_.isUndefined(injectedWeb3IfExists.currentProvider)) { const injectedProviderObservable = injectedWeb3IfExists.currentProvider.publicConfigStore; if (!_.isUndefined(injectedProviderObservable) && _.isUndefined(this._injectedProviderObservable)) { this._injectedProviderObservable = injectedProviderObservable; this._injectedProviderObservable.subscribe(this._injectedProviderUpdateHandler); } } this._updateProviderName(injectedWeb3IfExists); const shouldPollUserAddress = true; const shouldUseLedgerProvider = false; this._startWatchingGasPrice(); await this._resetOrInitializeAsync(this.networkId, shouldPollUserAddress, shouldUseLedgerProvider); } private _startWatchingGasPrice(): void { if (!_.isUndefined(this._watchGasPriceIntervalId)) { return; // we are already watching } const oneMinuteInMs = 60000; // tslint:disable-next-line:no-floating-promises this._updateDefaultGasPriceAsync(); this._watchGasPriceIntervalId = intervalUtils.setAsyncExcludingInterval( this._updateDefaultGasPriceAsync.bind(this), oneMinuteInMs, (err: Error) => { logUtils.log(`Watching gas price failed: ${err.stack}`); this._stopWatchingGasPrice(); }, ); } private _stopWatchingGasPrice(): void { if (!_.isUndefined(this._watchGasPriceIntervalId)) { intervalUtils.clearAsyncExcludingInterval(this._watchGasPriceIntervalId); } } private async _resetOrInitializeAsync( networkId: number, shouldPollUserAddress: boolean = false, shouldUserLedgerProvider: boolean = false, ): Promise { if (!shouldUserLedgerProvider) { this._dispatcher.updateBlockchainIsLoaded(false); } this._dispatcher.updateUserWeiBalance(undefined); this.networkId = networkId; const injectedWeb3IfExists = Blockchain._getInjectedWeb3(); const [provider, ledgerSubproviderIfExists] = await Blockchain._getProviderAsync( injectedWeb3IfExists, networkId, shouldUserLedgerProvider, ); 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._blockchainWatcher = new BlockchainWatcher(this._dispatcher, this._web3Wrapper, shouldPollUserAddress); if (shouldUserLedgerProvider && !_.isUndefined(ledgerSubproviderIfExists)) { delete this._userAddressIfExists; this._ledgerSubprovider = ledgerSubproviderIfExists; this._dispatcher.updateUserAddress(undefined); this._dispatcher.updateProviderType(ProviderType.Ledger); } else { delete this._ledgerSubprovider; const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync(); this._userAddressIfExists = userAddresses[0]; this._dispatcher.updateUserAddress(this._userAddressIfExists); if (!_.isUndefined(injectedWeb3IfExists)) { this._dispatcher.updateProviderType(ProviderType.Injected); } await this.fetchTokenInformationAsync(); } await this._blockchainWatcher.startEmittingUserBalanceStateAsync(); this._dispatcher.updateNetworkId(networkId); await this._rehydrateStoreWithContractEventsAsync(); } private _updateProviderName(injectedWeb3IfExists: InjectedWeb3): void { const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3IfExists); const providerName = doesInjectedWeb3Exist ? Blockchain._getNameGivenProvider(injectedWeb3IfExists.currentProvider) : constants.PROVIDER_NAME_PUBLIC; this._dispatcher.updateInjectedProviderName(providerName); } private async _instantiateContractIfExistsAsync(artifact: any, address?: string): Promise { const c = await contract(artifact); const providerObj = this._web3Wrapper.getProvider(); c.setProvider(providerObj); const artifactNetworkConfigs = artifact.networks[this.networkId]; let contractAddress; if (!_.isUndefined(address)) { contractAddress = address; } else if (!_.isUndefined(artifactNetworkConfigs)) { contractAddress = artifactNetworkConfigs.address; } if (!_.isUndefined(contractAddress)) { const doesContractExist = await this.doesContractExistAtAddressAsync(contractAddress); if (!doesContractExist) { logUtils.log(`Contract does not exist: ${artifact.contract_name} at ${contractAddress}`); throw new Error(BlockchainCallErrs.ContractDoesNotExist); } } try { const contractInstance = _.isUndefined(address) ? await c.deployed() : await c.at(address); return contractInstance; } catch (err) { const errMsg = `${err}`; logUtils.log(`Notice: Error encountered: ${err} ${err.stack}`); if (_.includes(errMsg, 'not been deployed to detected network')) { throw new Error(BlockchainCallErrs.ContractDoesNotExist); } else { await errorReporter.reportAsync(err); throw new Error(BlockchainCallErrs.UnhandledError); } } } private _showFlashMessageIfLedger(): void { if (!_.isUndefined(this._ledgerSubprovider)) { this._dispatcher.showFlashMessage('Confirm the transaction on your Ledger Nano S'); } } private async _updateDefaultGasPriceAsync(): Promise { try { const gasInfo = await backendClient.getGasInfoAsync(); const gasPriceInGwei = new BigNumber(gasInfo.fast / 10); const gasPriceInWei = gasPriceInGwei.mul(1000000000); this._defaultGasPrice = gasPriceInWei; } catch (err) { return; } } } // tslint:disable:max-file-line-count