import * as _ from 'lodash'; import * as React from 'react'; import { ZeroEx, ZeroExError, ExchangeContractErrs, ExchangeContractEventArgs, ExchangeEvents, SubscriptionOpts, IndexedFilterValues, DecodedLogEvent, BlockParam, LogFillContractEventArgs, LogCancelContractEventArgs, Token as ZeroExToken, LogWithDecodedArgs, TransactionReceiptWithDecodedLogs, SignedOrder, Order, } from '0x.js'; import BigNumber from 'bignumber.js'; import Web3 = require('web3'); import promisify = require('es6-promisify'); import findVersions = require('find-versions'); import compareVersions = require('compare-versions'); import contract = require('truffle-contract'); import ethUtil = require('ethereumjs-util'); import ProviderEngine = require('web3-provider-engine'); import FilterSubprovider = require('web3-provider-engine/subproviders/filters'); import { TransactionSubmitted } from 'ts/components/flash_messages/transaction_submitted'; import {TokenSendCompleted} from 'ts/components/flash_messages/token_send_completed'; import {RedundantRPCSubprovider} from 'ts/subproviders/redundant_rpc_subprovider'; import {InjectedWeb3SubProvider} from 'ts/subproviders/injected_web3_subprovider'; import {ledgerWalletSubproviderFactory} from 'ts/subproviders/ledger_wallet_subprovider_factory'; import {Dispatcher} from 'ts/redux/dispatcher'; import {utils} from 'ts/utils/utils'; import {constants} from 'ts/utils/constants'; import {configs} from 'ts/utils/configs'; import { BlockchainErrs, Token, SignatureData, Side, ContractResponse, BlockchainCallErrs, ContractInstance, ProviderType, LedgerWalletSubprovider, EtherscanLinkSuffixes, TokenByAddress, TokenStateByAddress, } from 'ts/types'; import {Web3Wrapper} from 'ts/web3_wrapper'; import {errorReporter} from 'ts/utils/error_reporter'; import {tradeHistoryStorage} from 'ts/local_storage/trade_history_storage'; import {trackedTokenStorage} from 'ts/local_storage/tracked_token_storage'; import * as MintableArtifacts from '../contracts/Mintable.json'; const ALLOWANCE_TO_ZERO_GAS_AMOUNT = 45730; const BLOCK_NUMBER_BACK_TRACK = 50; export class Blockchain { public networkId: number; public nodeVersion: string; private zeroEx: ZeroEx; private dispatcher: Dispatcher; private web3Wrapper: Web3Wrapper; private exchangeAddress: string; private tokenTransferProxy: ContractInstance; private tokenRegistry: ContractInstance; private userAddress: string; private cachedProvider: Web3.Provider; private ledgerSubProvider: LedgerWalletSubprovider; private zrxPollIntervalId: number; constructor(dispatcher: Dispatcher, isSalePage: boolean = false) { this.dispatcher = dispatcher; this.userAddress = ''; this.onPageLoadInitFireAndForgetAsync(); } public async networkIdUpdatedFireAndForgetAsync(newNetworkId: number) { const isConnected = !_.isUndefined(newNetworkId); if (!isConnected) { this.networkId = newNetworkId; this.dispatcher.encounteredBlockchainError(BlockchainErrs.DISCONNECTED_FROM_ETHEREUM_NODE); this.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); } else if (this.networkId !== newNetworkId) { this.networkId = newNetworkId; this.dispatcher.encounteredBlockchainError(''); await this.fetchTokenInformationAsync(); await this.rehydrateStoreWithContractEvents(); } } public async userAddressUpdatedFireAndForgetAsync(newUserAddress: string) { if (this.userAddress !== newUserAddress) { this.userAddress = newUserAddress; await this.fetchTokenInformationAsync(); await this.rehydrateStoreWithContractEvents(); } } public async nodeVersionUpdatedFireAndForgetAsync(nodeVersion: string) { if (this.nodeVersion !== nodeVersion) { this.nodeVersion = nodeVersion; } } public async isAddressInTokenRegistryAsync(tokenAddress: string): Promise { utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); const tokenIfExists = await this.zeroEx.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) { if (_.isUndefined(this.ledgerSubProvider)) { return; // noop } this.ledgerSubProvider.setPath(path); } public updateLedgerDerivationIndex(pathIndex: number) { if (_.isUndefined(this.ledgerSubProvider)) { return; // noop } this.ledgerSubProvider.setPathIndex(pathIndex); } public async providerTypeUpdatedFireAndForgetAsync(providerType: ProviderType) { 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(); this.ledgerSubProvider = ledgerWalletSubproviderFactory(this.getBlockchainNetworkId.bind(this)); provider.addProvider(this.ledgerSubProvider); provider.addProvider(new FilterSubprovider()); const networkId = configs.isMainnetEnabled ? constants.MAINNET_NETWORK_ID : constants.TESTNET_NETWORK_ID; provider.addProvider(new RedundantRPCSubprovider( constants.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); await this.zeroEx.setProviderAsync(provider); 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); await this.zeroEx.setProviderAsync(provider); await this.postInstantiationOrUpdatingProviderZeroExAsync(); delete this.ledgerSubProvider; delete this.cachedProvider; break; } default: throw utils.spawnSwitchErr('providerType', providerType); } await this.fetchTokenInformationAsync(); } public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise { utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TOKEN_ADDRESS_IS_INVALID); utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES); utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); const txHash = await this.zeroEx.token.setProxyAllowanceAsync( token.address, this.userAddress, amountInBaseUnits, ); await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); const allowance = amountInBaseUnits; this.dispatcher.replaceTokenAllowanceByAddress(token.address, allowance); } public async transferAsync(token: Token, toAddress: string, amountInBaseUnits: BigNumber): Promise { const txHash = await this.zeroEx.token.transferAsync( token.address, this.userAddress, toAddress, amountInBaseUnits, ); await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); const etherScanLinkIfExists = utils.getEtherScanLinkIfExists(txHash, this.networkId, EtherscanLinkSuffixes.tx); this.dispatcher.showFlashMessage(React.createElement(TokenSendCompleted, { etherScanLinkIfExists, token, toAddress, amountInBaseUnits, })); } public portalOrderToSignedOrder(maker: string, taker: string, makerTokenAddress: string, takerTokenAddress: string, makerTokenAmount: BigNumber, takerTokenAmount: BigNumber, makerFee: BigNumber, takerFee: BigNumber, expirationUnixTimestampSec: BigNumber, feeRecipient: string, signatureData: SignatureData, salt: BigNumber): SignedOrder { const ecSignature = signatureData; const exchangeContractAddress = this.getExchangeContractAddressIfExists(); taker = _.isEmpty(taker) ? constants.NULL_ADDRESS : taker; const signedOrder = { ecSignature, exchangeContractAddress, expirationUnixTimestampSec, feeRecipient, maker, makerFee, makerTokenAddress, makerTokenAmount, salt, taker, takerFee, takerTokenAddress, takerTokenAmount, }; return signedOrder; } public async fillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber): Promise { utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES); const shouldThrowOnInsufficientBalanceOrAllowance = true; const txHash = await this.zeroEx.exchange.fillOrderAsync( signedOrder, fillTakerTokenAmount, shouldThrowOnInsufficientBalanceOrAllowance, this.userAddress, ); const receipt = await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); const logs: Array> = receipt.logs as any; this.zeroEx.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 { const txHash = await this.zeroEx.exchange.cancelOrderAsync( signedOrder, cancelTakerTokenAmount, ); const receipt = await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); const logs: Array> = receipt.logs as any; this.zeroEx.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(ZeroEx.isValidOrderHash(orderHash), 'Must be valid orderHash'); utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); const unavailableTakerAmount = await this.zeroEx.exchange.getUnavailableTakerAmountAsync(orderHash); return unavailableTakerAmount; } public getExchangeContractAddressIfExists() { return this.exchangeAddress; } public toHumanReadableErrorMsg(error: ZeroExError|ExchangeContractErrs, takerAddress: string): string { const ZeroExErrorToHumanReadableError: {[error: string]: string} = { [ZeroExError.ContractDoesNotExist]: 'Contract does not exist', [ZeroExError.ExchangeContractDoesNotExist]: 'Exchange contract does not exist', [ZeroExError.UnhandledError]: ' Unhandled error occured', [ZeroExError.UserHasNoAssociatedAddress]: 'User has no addresses available', [ZeroExError.InvalidSignature]: 'Order signature is not valid', [ZeroExError.ContractNotDeployedOnNetwork]: 'Contract is not deployed on the detected network', [ZeroExError.InvalidJump]: 'Invalid jump occured while executing the transaction', [ZeroExError.OutOfGas]: 'Transaction ran out of gas', [ZeroExError.NoNetworkId]: 'No network id detected', }; const exchangeContractErrorToHumanReadableError: {[error: string]: string} = { [ExchangeContractErrs.OrderFillExpired]: 'This order has expired', [ExchangeContractErrs.OrderCancelExpired]: 'This order has expired', [ExchangeContractErrs.OrderCancelAmountZero]: 'Order cancel amount can\'t be 0', [ExchangeContractErrs.OrderAlreadyCancelledOrFilled]: 'This order has already been completely filled or cancelled', [ExchangeContractErrs.OrderFillAmountZero]: 'Order fill amount can\'t be 0', [ExchangeContractErrs.OrderRemainingFillAmountZero]: 'This order has already been completely filled or cancelled', [ExchangeContractErrs.OrderFillRoundingError]: 'Rounding error will occur when filling this order', [ExchangeContractErrs.InsufficientTakerBalance]: 'Taker no longer has a sufficient balance to complete this order', [ExchangeContractErrs.InsufficientTakerAllowance]: 'Taker no longer has a sufficient allowance to complete this order', [ExchangeContractErrs.InsufficientMakerBalance]: 'Maker no longer has a sufficient balance to complete this order', [ExchangeContractErrs.InsufficientMakerAllowance]: 'Maker no longer has a sufficient allowance to complete this order', [ExchangeContractErrs.InsufficientTakerFeeBalance]: 'Taker no longer has a sufficient balance to pay fees', [ExchangeContractErrs.InsufficientTakerFeeAllowance]: 'Taker no longer has a sufficient allowance to pay fees', [ExchangeContractErrs.InsufficientMakerFeeBalance]: 'Maker no longer has a sufficient balance to pay fees', [ExchangeContractErrs.InsufficientMakerFeeAllowance]: 'Maker no longer has a sufficient allowance to pay fees', [ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker]: `This order can only be filled by ${takerAddress}`, [ExchangeContractErrs.InsufficientRemainingFillAmount]: 'Insufficient remaining fill amount', }; const humanReadableErrorMsg = exchangeContractErrorToHumanReadableError[error] || ZeroExErrorToHumanReadableError[error]; return humanReadableErrorMsg; } public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber, takerAddress: string): Promise { await this.zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( signedOrder, fillTakerTokenAmount, takerAddress); } public async validateCancelOrderThrowIfInvalidAsync(order: Order, cancelTakerTokenAmount: BigNumber): Promise { await this.zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(order, cancelTakerTokenAmount); } public isValidAddress(address: string): boolean { const lowercaseAddress = address.toLowerCase(); return this.web3Wrapper.isAddress(lowercaseAddress); } public async pollTokenBalanceAsync(token: Token) { utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES); const [currBalance] = await this.getTokenBalanceAndAllowanceAsync(this.userAddress, token.address); this.zrxPollIntervalId = window.setInterval(async () => { const [balance] = await this.getTokenBalanceAndAllowanceAsync(this.userAddress, token.address); if (!balance.eq(currBalance)) { this.dispatcher.replaceTokenBalanceByAddress(token.address, balance); clearInterval(this.zrxPollIntervalId); delete this.zrxPollIntervalId; } }, 5000); } public async signOrderHashAsync(orderHash: string): Promise { utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); const makerAddress = this.userAddress; // 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'); } const ecSignature = await this.zeroEx.signOrderHashAsync(orderHash, makerAddress); const signatureData = _.extend({}, ecSignature, { hash: orderHash, }); this.dispatcher.updateSignatureData(signatureData); return signatureData; } public async mintTestTokensAsync(token: Token) { utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES); const mintableContract = await this.instantiateContractIfExistsAsync(MintableArtifacts, token.address); await mintableContract.mint(constants.MINT_AMOUNT, { from: this.userAddress, }); const balanceDelta = constants.MINT_AMOUNT; this.dispatcher.updateTokenBalanceByAddress(token.address, balanceDelta); } public async getBalanceInEthAsync(owner: string): Promise { const balance = await this.web3Wrapper.getBalanceInEthAsync(owner); return balance; } public async convertEthToWrappedEthTokensAsync(amount: BigNumber): Promise { utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES); const txHash = await this.zeroEx.etherToken.depositAsync(amount, this.userAddress); await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); } public async convertWrappedEthTokensToEthAsync(amount: BigNumber): Promise { utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES); const txHash = await this.zeroEx.etherToken.withdrawAsync(amount, this.userAddress); await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); } public async doesContractExistAtAddressAsync(address: string) { const doesContractExist = await this.web3Wrapper.doesContractExistAtAddressAsync(address); return doesContractExist; } public async getCurrentUserTokenBalanceAndAllowanceAsync(tokenAddress: string): Promise { const tokenBalanceAndAllowance = await this.getTokenBalanceAndAllowanceAsync(this.userAddress, tokenAddress); return tokenBalanceAndAllowance; } public async getTokenBalanceAndAllowanceAsync(ownerAddress: string, tokenAddress: string): Promise { utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); if (_.isEmpty(ownerAddress)) { const zero = new BigNumber(0); return [zero, zero]; } let balance = new BigNumber(0); let allowance = new BigNumber(0); if (this.doesUserAddressExist()) { balance = await this.zeroEx.token.getBalanceAsync(tokenAddress, ownerAddress); allowance = await this.zeroEx.token.getProxyAllowanceAsync(tokenAddress, ownerAddress); } return [balance, allowance]; } public async updateTokenBalancesAndAllowancesAsync(tokens: Token[]) { const tokenStateByAddress: TokenStateByAddress = {}; for (const token of tokens) { let balance = new BigNumber(0); let allowance = new BigNumber(0); if (this.doesUserAddressExist()) { [ balance, allowance, ] = await this.getTokenBalanceAndAllowanceAsync(this.userAddress, token.address); } const tokenState = { balance, allowance, }; tokenStateByAddress[token.address] = tokenState; } this.dispatcher.updateTokenStateByAddress(tokenStateByAddress); } public async getUserAccountsAsync() { utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); const userAccountsIfExists = await this.zeroEx.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) { this.web3Wrapper.updatePrevUserAddress(newUserAddress); } public destroy() { clearInterval(this.zrxPollIntervalId); this.web3Wrapper.destroy(); this.stopWatchingExchangeLogFillEventsAsync(); // fire and forget } private async showEtherScanLinkAndAwaitTransactionMinedAsync( txHash: string): Promise { const etherScanLinkIfExists = utils.getEtherScanLinkIfExists(txHash, this.networkId, EtherscanLinkSuffixes.tx); this.dispatcher.showFlashMessage(React.createElement(TransactionSubmitted, { etherScanLinkIfExists, })); const receipt = await this.zeroEx.awaitTransactionMinedAsync(txHash); return receipt; } private doesUserAddressExist(): boolean { return this.userAddress !== ''; } private async rehydrateStoreWithContractEvents() { // Ensure we are only ever listening to one set of events await this.stopWatchingExchangeLogFillEventsAsync(); if (!this.doesUserAddressExist()) { return; // short-circuit } if (!_.isUndefined(this.zeroEx)) { // 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.zeroEx), 'ZeroEx must be instantiated.'); utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES); // Fetch historical logs await this.fetchHistoricalExchangeLogFillEventsAsync(indexFilterValues); // Start a subscription for new logs const exchangeAddress = this.getExchangeContractAddressIfExists(); const subscriptionId = await this.zeroEx.exchange.subscribeAsync( 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 errorReporter.reportAsync(err); // fire and forget this.stopWatchingExchangeLogFillEventsAsync(); // fire and forget return; } else { if (!this.doesLogEventInvolveUser(decodedLogEvent)) { return; // We aren't interested in the fill event } this.updateLatestFillsBlockIfNeeded(decodedLogEvent.blockNumber); const fill = await this.convertDecodedLogToFillAsync(decodedLogEvent); if (decodedLogEvent.removed) { tradeHistoryStorage.removeFillFromUser(this.userAddress, this.networkId, fill); } else { tradeHistoryStorage.addFillToUser(this.userAddress, this.networkId, fill); } } }); } private async fetchHistoricalExchangeLogFillEventsAsync(indexFilterValues: IndexedFilterValues) { const fromBlock = tradeHistoryStorage.getFillsLatestBlock(this.userAddress, this.networkId); const subscriptionOpts: SubscriptionOpts = { fromBlock, toBlock: 'latest' as BlockParam, }; const decodedLogs = await this.zeroEx.exchange.getLogsAsync( ExchangeEvents.LogFill, subscriptionOpts, 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.userAddress, this.networkId, fill); } } private async convertDecodedLogToFillAsync(decodedLog: LogWithDecodedArgs) { const args = decodedLog.args as LogFillContractEventArgs; 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) { const args = decodedLog.args as LogFillContractEventArgs; const isUserMakerOrTaker = args.maker === this.userAddress || args.taker === this.userAddress; return isUserMakerOrTaker; } private updateLatestFillsBlockIfNeeded(blockNumber: number) { 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.userAddress, this.networkId, blockNumberToSet); } } private async stopWatchingExchangeLogFillEventsAsync() { this.zeroEx.exchange.unsubscribeAll(); } private async getTokenRegistryTokensByAddressAsync(): Promise { utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); const tokenRegistryTokens = await this.zeroEx.tokenRegistry.getTokensAsync(); const tokenByAddress: TokenByAddress = {}; _.each(tokenRegistryTokens, (t: ZeroExToken, i: number) => { // 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 = constants.iconUrlBySymbol[t.symbol]; const token: Token = { iconUrl, address: t.address, name: t.name, symbol: t.symbol, decimals: t.decimals, isTracked: false, isRegistered: true, }; tokenByAddress[token.address] = token; }); return tokenByAddress; } private async onPageLoadInitFireAndForgetAsync() { await this.onPageLoadAsync(); // wait for page to load // 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 = (window as any).web3; let networkId: number; if (!_.isUndefined(injectedWeb3)) { try { networkId = _.parseInt(await promisify(injectedWeb3.version.getNetwork)()); } catch (err) { // Ignore error and proceed with networkId undefined } } const provider = await this.getProviderAsync(injectedWeb3, networkId); this.zeroEx = new ZeroEx(provider); await this.updateProviderName(injectedWeb3); const shouldPollUserAddress = true; this.web3Wrapper = new Web3Wrapper(this.dispatcher, provider, networkId, shouldPollUserAddress); await this.postInstantiationOrUpdatingProviderZeroExAsync(); } // This method should always be run after instantiating or updating the provider // of the ZeroEx instance. private async postInstantiationOrUpdatingProviderZeroExAsync() { utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.'); this.exchangeAddress = await this.zeroEx.exchange.getContractAddressAsync(); } private updateProviderName(injectedWeb3: Web3) { const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3); const providerName = doesInjectedWeb3Exist ? this.getNameGivenProvider(injectedWeb3.currentProvider) : constants.PUBLIC_PROVIDER_NAME; this.dispatcher.updateInjectedProviderName(providerName); } // This is only ever called by the LedgerWallet subprovider in order to retrieve // the current networkId without this value going stale. private getBlockchainNetworkId() { return this.networkId; } private async getProviderAsync(injectedWeb3: Web3, networkIdIfExists: number) { const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3); const publicNodeUrlsIfExistsForNetworkId = constants.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists]; const isPublicNodeAvailableForNetworkId = !_.isUndefined(publicNodeUrlsIfExistsForNetworkId); let provider; 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. provider = new ProviderEngine(); provider.addProvider(new InjectedWeb3SubProvider(injectedWeb3)); provider.addProvider(new FilterSubprovider()); provider.addProvider(new RedundantRPCSubprovider( publicNodeUrlsIfExistsForNetworkId, )); provider.start(); } else if (doesInjectedWeb3Exist) { // Since no public node for this network, all requests go to injectedWeb3 instance provider = injectedWeb3.currentProvider; } 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. provider = new ProviderEngine(); provider.addProvider(new FilterSubprovider()); const networkId = configs.isMainnetEnabled ? constants.MAINNET_NETWORK_ID : constants.TESTNET_NETWORK_ID; provider.addProvider(new RedundantRPCSubprovider( constants.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId], )); provider.start(); } return provider; } private getNameGivenProvider(provider: Web3.Provider): string { if (!_.isUndefined((provider as any).isMetaMask)) { return constants.METAMASK_PROVIDER_NAME; } // HACK: We use the fact that Parity Signer's provider is an instance of their // internal `Web3FrameProvider` class. const isParitySigner = _.startsWith(provider.constructor.toString(), 'function Web3FrameProvider'); if (isParitySigner) { return constants.PARITY_SIGNER_PROVIDER_NAME; } return constants.GENERIC_PROVIDER_NAME; } 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.defaultTrackedTokenSymbols, 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.defaultTrackedTokenSymbols[0]}), _.find(allTokens, {symbol: configs.defaultTrackedTokenSymbols[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 { const c = await contract(artifact); const providerObj = this.web3Wrapper.getProviderObj(); 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) { utils.consoleLog(`Contract does not exist: ${artifact.contract_name} at ${contractAddress}`); throw new Error(BlockchainCallErrs.CONTRACT_DOES_NOT_EXIST); } } try { const contractInstance = _.isUndefined(address) ? await c.deployed() : await c.at(address); return contractInstance; } catch (err) { const errMsg = `${err}`; utils.consoleLog(`Notice: Error encountered: ${err} ${err.stack}`); if (_.includes(errMsg, 'not been deployed to detected network')) { throw new Error(BlockchainCallErrs.CONTRACT_DOES_NOT_EXIST); } else { await errorReporter.reportAsync(err); throw new Error(BlockchainCallErrs.UNHANDLED_ERROR); } } } private async onPageLoadAsync() { if (document.readyState === 'complete') { return; // Already loaded } return new Promise((resolve, reject) => { window.onload = resolve; }); } }