diff options
Diffstat (limited to 'packages/website/ts/blockchain.ts')
-rw-r--r-- | packages/website/ts/blockchain.ts | 1504 |
1 files changed, 752 insertions, 752 deletions
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 711c3329d..5530701c0 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -1,25 +1,25 @@ import { - BlockParam, - BlockRange, - DecodedLogEvent, - ExchangeContractEventArgs, - ExchangeEvents, - IndexedFilterValues, - LogCancelContractEventArgs, - LogFillContractEventArgs, - LogWithDecodedArgs, - Order, - SignedOrder, - Token as ZeroExToken, - TransactionReceiptWithDecodedLogs, - ZeroEx, + BlockParam, + BlockRange, + DecodedLogEvent, + ExchangeContractEventArgs, + ExchangeEvents, + IndexedFilterValues, + LogCancelContractEventArgs, + LogFillContractEventArgs, + LogWithDecodedArgs, + Order, + SignedOrder, + Token as ZeroExToken, + TransactionReceiptWithDecodedLogs, + ZeroEx, } from '0x.js'; import { - InjectedWeb3Subprovider, - ledgerEthereumBrowserClientFactoryAsync, - LedgerSubprovider, - LedgerWalletSubprovider, - RedundantRPCSubprovider, + InjectedWeb3Subprovider, + ledgerEthereumBrowserClientFactoryAsync, + LedgerSubprovider, + LedgerWalletSubprovider, + RedundantRPCSubprovider, } from '@0xproject/subproviders'; import { BigNumber, intervalUtils, promisify } from '@0xproject/utils'; import * as _ from 'lodash'; @@ -31,16 +31,16 @@ 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, - EtherscanLinkSuffixes, - ProviderType, - Side, - SignatureData, - Token, - TokenByAddress, - TokenStateByAddress, + BlockchainCallErrs, + BlockchainErrs, + ContractInstance, + EtherscanLinkSuffixes, + ProviderType, + Side, + SignatureData, + Token, + TokenByAddress, + TokenStateByAddress, } from 'ts/types'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; @@ -56,727 +56,727 @@ import * as MintableArtifacts from '../contracts/Mintable.json'; 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 _userAddress: string; - private _cachedProvider: Web3.Provider; - private _ledgerSubprovider: LedgerWalletSubprovider; - private _zrxPollIntervalId: NodeJS.Timer; - private static async _onPageLoadAsync(): Promise<void> { - if (document.readyState === 'complete') { - return; // Already loaded - } - return new Promise<void>((resolve, reject) => { - window.onload = () => resolve(); - }); - } - private static _getNameGivenProvider(provider: Web3.Provider): string { - if (!_.isUndefined((provider as any).isMetaMask)) { - return constants.PROVIDER_NAME_METAMASK; - } - - // 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.PROVIDER_NAME_PARITY_SIGNER; - } - - return constants.PROVIDER_NAME_GENERIC; - } - private static async _getProviderAsync(injectedWeb3: Web3, networkIdIfExists: number) { - const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3); - const publicNodeUrlsIfExistsForNetworkId = configs.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.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(); - } - - return provider; - } - constructor(dispatcher: Dispatcher, isSalePage: boolean = false) { - this._dispatcher = dispatcher; - this._userAddress = ''; - // tslint:disable-next-line:no-floating-promises - this._onPageLoadInitFireAndForgetAsync(); - } - public async networkIdUpdatedFireAndForgetAsync(newNetworkId: number) { - 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._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<boolean> { - utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); - // HACK: temporarily whitelist the new WETH token address `as if` they were - // already in the tokenRegistry. - // TODO: Remove this hack once we've updated the TokenRegistries - // Airtable task: https://airtable.com/tblFe0Q9JuKJPYbTn/viwsOG2Y97qdIeCIO/recv3VGmIorFzHBVz - if (configs.SHOULD_DEPRECATE_OLD_WETH_TOKEN && tokenAddress === configs.NEW_WRAPPED_ETHERS[this.networkId]) { - return true; - } - 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(); - 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); - } - - await this._fetchTokenInformationAsync(); - } - public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> { - utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid); - utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); - 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<void> { - 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(); - const takerOrNullAddress = _.isEmpty(taker) ? constants.NULL_ADDRESS : taker; - const signedOrder = { - ecSignature, - exchangeContractAddress, - expirationUnixTimestampSec, - feeRecipient, - maker, - makerFee, - makerTokenAddress, - makerTokenAmount, - salt, - taker: takerOrNullAddress, - takerFee, - takerTokenAddress, - takerTokenAmount, - }; - return signedOrder; - } - public async fillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber): Promise<BigNumber> { - utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); - - const shouldThrowOnInsufficientBalanceOrAllowance = true; - - const txHash = await this._zeroEx.exchange.fillOrderAsync( - signedOrder, - fillTakerTokenAmount, - shouldThrowOnInsufficientBalanceOrAllowance, - this._userAddress, - ); - const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); - const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = 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<BigNumber> { - const txHash = await this._zeroEx.exchange.cancelOrderAsync(signedOrder, cancelTakerTokenAmount); - const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); - const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = 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<BigNumber> { - 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 async validateFillOrderThrowIfInvalidAsync( - signedOrder: SignedOrder, - fillTakerTokenAmount: BigNumber, - takerAddress: string, - ): Promise<void> { - await this._zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( - signedOrder, - fillTakerTokenAmount, - takerAddress, - ); - } - public async validateCancelOrderThrowIfInvalidAsync( - order: Order, - cancelTakerTokenAmount: BigNumber, - ): Promise<void> { - 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.UserHasNoAssociatedAddresses); - - const [currBalance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address); - - this._zrxPollIntervalId = intervalUtils.setAsyncExcludingInterval( - async () => { - const [balance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address); - if (!balance.eq(currBalance)) { - this._dispatcher.replaceTokenBalanceByAddress(token.address, balance); - intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId); - delete this._zrxPollIntervalId; - } - }, - 5000, - (err: Error) => { - utils.consoleLog(`Polling tokenBalance failed: ${err}`); - intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId); - delete this._zrxPollIntervalId; - }, - ); - } - public async signOrderHashAsync(orderHash: string): Promise<SignatureData> { - 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.UserHasNoAssociatedAddresses); - - 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<BigNumber> { - const balance = await this._web3Wrapper.getBalanceInEthAsync(owner); - return balance; - } - public async convertEthToWrappedEthTokensAsync(etherTokenAddress: string, amount: BigNumber): Promise<void> { - utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); - utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); - - const txHash = await this._zeroEx.etherToken.depositAsync(etherTokenAddress, amount, this._userAddress); - await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); - } - public async convertWrappedEthTokensToEthAsync(etherTokenAddress: string, amount: BigNumber): Promise<void> { - utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); - utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); - - const txHash = await this._zeroEx.etherToken.withdrawAsync(etherTokenAddress, 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<BigNumber[]> { - const tokenBalanceAndAllowance = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, tokenAddress); - return tokenBalanceAndAllowance; - } - public async getTokenBalanceAndAllowanceAsync(ownerAddress: string, tokenAddress: string): Promise<BigNumber[]> { - 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() { - intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId); - this._web3Wrapper.destroy(); - this._stopWatchingExchangeLogFillEvents(); - } - private async _showEtherScanLinkAndAwaitTransactionMinedAsync( - txHash: string, - ): Promise<TransactionReceiptWithDecodedLogs> { - 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 - this._stopWatchingExchangeLogFillEvents(); - - 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<void> { - utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); - utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); - - // Fetch historical logs - await this._fetchHistoricalExchangeLogFillEventsAsync(indexFilterValues); - - // Start a subscription for new logs - this._zeroEx.exchange.subscribe( - ExchangeEvents.LogFill, - indexFilterValues, - async (err: Error, decodedLogEvent: DecodedLogEvent<LogFillContractEventArgs>) => { - 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._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 blockRange: BlockRange = { - fromBlock, - toBlock: 'latest' as BlockParam, - }; - const decodedLogs = await this._zeroEx.exchange.getLogsAsync<LogFillContractEventArgs>( - 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._userAddress, this.networkId, fill); - } - } - private async _convertDecodedLogToFillAsync(decodedLog: LogWithDecodedArgs<LogFillContractEventArgs>) { - 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<LogFillContractEventArgs>) { - const args = decodedLog.args; - 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 _stopWatchingExchangeLogFillEvents(): void { - this._zeroEx.exchange.unsubscribeAll(); - } - private async _getTokenRegistryTokensByAddressAsync(): Promise<TokenByAddress> { - 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 = configs.ICON_URL_BY_SYMBOL[t.symbol]; - // HACK: Temporarily we hijack the WETH addresses fetched from the tokenRegistry - // so that we can take our time with actually updating it. This ensures that when - // we deploy the new WETH page, everyone will re-fill their trackedTokens with the - // new canonical WETH. - // TODO: Remove this hack once we've updated the TokenRegistries - // Airtable task: https://airtable.com/tblFe0Q9JuKJPYbTn/viwsOG2Y97qdIeCIO/recv3VGmIorFzHBVz - let address = t.address; - if (configs.SHOULD_DEPRECATE_OLD_WETH_TOKEN && t.symbol === 'WETH') { - const newEtherTokenAddressIfExists = configs.NEW_WRAPPED_ETHERS[this.networkId]; - if (!_.isUndefined(newEtherTokenAddressIfExists)) { - address = newEtherTokenAddressIfExists; - } - } - const token: Token = { - iconUrl, - address, - name: t.name, - symbol: t.symbol, - decimals: t.decimals, - isTracked: false, - isRegistered: true, - }; - tokenByAddress[token.address] = token; - }); - return tokenByAddress; - } - private async _onPageLoadInitFireAndForgetAsync() { - await Blockchain._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 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); - const networkId = !_.isUndefined(networkIdIfExists) - ? networkIdIfExists - : configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_TESTNET; - const zeroExConfigs = { - networkId, - }; - this._zeroEx = new ZeroEx(provider, zeroExConfigs); - 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 = this._zeroEx.exchange.getContractAddress(); - } - private _updateProviderName(injectedWeb3: Web3) { - const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3); - const providerName = doesInjectedWeb3Exist - ? Blockchain._getNameGivenProvider(injectedWeb3.currentProvider) - : 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(); - 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.ContractDoesNotExist); - } - } - - 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.ContractDoesNotExist); - } else { - await errorReporter.reportAsync(err); - throw new Error(BlockchainCallErrs.UnhandledError); - } - } - } + public networkId: number; + public nodeVersion: string; + private _zeroEx: ZeroEx; + private _dispatcher: Dispatcher; + private _web3Wrapper?: Web3Wrapper; + private _exchangeAddress: string; + private _userAddress: string; + private _cachedProvider: Web3.Provider; + private _ledgerSubprovider: LedgerWalletSubprovider; + private _zrxPollIntervalId: NodeJS.Timer; + private static async _onPageLoadAsync(): Promise<void> { + if (document.readyState === 'complete') { + return; // Already loaded + } + return new Promise<void>((resolve, reject) => { + window.onload = () => resolve(); + }); + } + private static _getNameGivenProvider(provider: Web3.Provider): string { + if (!_.isUndefined((provider as any).isMetaMask)) { + return constants.PROVIDER_NAME_METAMASK; + } + + // 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.PROVIDER_NAME_PARITY_SIGNER; + } + + return constants.PROVIDER_NAME_GENERIC; + } + private static async _getProviderAsync(injectedWeb3: Web3, networkIdIfExists: number) { + const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3); + const publicNodeUrlsIfExistsForNetworkId = configs.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.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(); + } + + return provider; + } + constructor(dispatcher: Dispatcher, isSalePage: boolean = false) { + this._dispatcher = dispatcher; + this._userAddress = ''; + // tslint:disable-next-line:no-floating-promises + this._onPageLoadInitFireAndForgetAsync(); + } + public async networkIdUpdatedFireAndForgetAsync(newNetworkId: number) { + 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._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<boolean> { + utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); + // HACK: temporarily whitelist the new WETH token address `as if` they were + // already in the tokenRegistry. + // TODO: Remove this hack once we've updated the TokenRegistries + // Airtable task: https://airtable.com/tblFe0Q9JuKJPYbTn/viwsOG2Y97qdIeCIO/recv3VGmIorFzHBVz + if (configs.SHOULD_DEPRECATE_OLD_WETH_TOKEN && tokenAddress === configs.NEW_WRAPPED_ETHERS[this.networkId]) { + return true; + } + 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(); + 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); + } + + await this._fetchTokenInformationAsync(); + } + public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> { + utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid); + utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); + 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<void> { + 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(); + const takerOrNullAddress = _.isEmpty(taker) ? constants.NULL_ADDRESS : taker; + const signedOrder = { + ecSignature, + exchangeContractAddress, + expirationUnixTimestampSec, + feeRecipient, + maker, + makerFee, + makerTokenAddress, + makerTokenAmount, + salt, + taker: takerOrNullAddress, + takerFee, + takerTokenAddress, + takerTokenAmount, + }; + return signedOrder; + } + public async fillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber): Promise<BigNumber> { + utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); + + const shouldThrowOnInsufficientBalanceOrAllowance = true; + + const txHash = await this._zeroEx.exchange.fillOrderAsync( + signedOrder, + fillTakerTokenAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + this._userAddress, + ); + const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); + const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = 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<BigNumber> { + const txHash = await this._zeroEx.exchange.cancelOrderAsync(signedOrder, cancelTakerTokenAmount); + const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); + const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = 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<BigNumber> { + 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 async validateFillOrderThrowIfInvalidAsync( + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string, + ): Promise<void> { + await this._zeroEx.exchange.validateFillOrderThrowIfInvalidAsync( + signedOrder, + fillTakerTokenAmount, + takerAddress, + ); + } + public async validateCancelOrderThrowIfInvalidAsync( + order: Order, + cancelTakerTokenAmount: BigNumber, + ): Promise<void> { + 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.UserHasNoAssociatedAddresses); + + const [currBalance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address); + + this._zrxPollIntervalId = intervalUtils.setAsyncExcludingInterval( + async () => { + const [balance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address); + if (!balance.eq(currBalance)) { + this._dispatcher.replaceTokenBalanceByAddress(token.address, balance); + intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId); + delete this._zrxPollIntervalId; + } + }, + 5000, + (err: Error) => { + utils.consoleLog(`Polling tokenBalance failed: ${err}`); + intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId); + delete this._zrxPollIntervalId; + }, + ); + } + public async signOrderHashAsync(orderHash: string): Promise<SignatureData> { + 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.UserHasNoAssociatedAddresses); + + 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<BigNumber> { + const balance = await this._web3Wrapper.getBalanceInEthAsync(owner); + return balance; + } + public async convertEthToWrappedEthTokensAsync(etherTokenAddress: string, amount: BigNumber): Promise<void> { + utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); + utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); + + const txHash = await this._zeroEx.etherToken.depositAsync(etherTokenAddress, amount, this._userAddress); + await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); + } + public async convertWrappedEthTokensToEthAsync(etherTokenAddress: string, amount: BigNumber): Promise<void> { + utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); + utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); + + const txHash = await this._zeroEx.etherToken.withdrawAsync(etherTokenAddress, 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<BigNumber[]> { + const tokenBalanceAndAllowance = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, tokenAddress); + return tokenBalanceAndAllowance; + } + public async getTokenBalanceAndAllowanceAsync(ownerAddress: string, tokenAddress: string): Promise<BigNumber[]> { + 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() { + intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId); + this._web3Wrapper.destroy(); + this._stopWatchingExchangeLogFillEvents(); + } + private async _showEtherScanLinkAndAwaitTransactionMinedAsync( + txHash: string, + ): Promise<TransactionReceiptWithDecodedLogs> { + 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 + this._stopWatchingExchangeLogFillEvents(); + + 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<void> { + utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); + utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); + + // Fetch historical logs + await this._fetchHistoricalExchangeLogFillEventsAsync(indexFilterValues); + + // Start a subscription for new logs + this._zeroEx.exchange.subscribe( + ExchangeEvents.LogFill, + indexFilterValues, + async (err: Error, decodedLogEvent: DecodedLogEvent<LogFillContractEventArgs>) => { + 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._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 blockRange: BlockRange = { + fromBlock, + toBlock: 'latest' as BlockParam, + }; + const decodedLogs = await this._zeroEx.exchange.getLogsAsync<LogFillContractEventArgs>( + 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._userAddress, this.networkId, fill); + } + } + private async _convertDecodedLogToFillAsync(decodedLog: LogWithDecodedArgs<LogFillContractEventArgs>) { + 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<LogFillContractEventArgs>) { + const args = decodedLog.args; + 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 _stopWatchingExchangeLogFillEvents(): void { + this._zeroEx.exchange.unsubscribeAll(); + } + private async _getTokenRegistryTokensByAddressAsync(): Promise<TokenByAddress> { + 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 = configs.ICON_URL_BY_SYMBOL[t.symbol]; + // HACK: Temporarily we hijack the WETH addresses fetched from the tokenRegistry + // so that we can take our time with actually updating it. This ensures that when + // we deploy the new WETH page, everyone will re-fill their trackedTokens with the + // new canonical WETH. + // TODO: Remove this hack once we've updated the TokenRegistries + // Airtable task: https://airtable.com/tblFe0Q9JuKJPYbTn/viwsOG2Y97qdIeCIO/recv3VGmIorFzHBVz + let address = t.address; + if (configs.SHOULD_DEPRECATE_OLD_WETH_TOKEN && t.symbol === 'WETH') { + const newEtherTokenAddressIfExists = configs.NEW_WRAPPED_ETHERS[this.networkId]; + if (!_.isUndefined(newEtherTokenAddressIfExists)) { + address = newEtherTokenAddressIfExists; + } + } + const token: Token = { + iconUrl, + address, + name: t.name, + symbol: t.symbol, + decimals: t.decimals, + isTracked: false, + isRegistered: true, + }; + tokenByAddress[token.address] = token; + }); + return tokenByAddress; + } + private async _onPageLoadInitFireAndForgetAsync() { + await Blockchain._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 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); + const networkId = !_.isUndefined(networkIdIfExists) + ? networkIdIfExists + : configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_TESTNET; + const zeroExConfigs = { + networkId, + }; + this._zeroEx = new ZeroEx(provider, zeroExConfigs); + 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 = this._zeroEx.exchange.getContractAddress(); + } + private _updateProviderName(injectedWeb3: Web3) { + const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3); + const providerName = doesInjectedWeb3Exist + ? Blockchain._getNameGivenProvider(injectedWeb3.currentProvider) + : 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(); + 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.ContractDoesNotExist); + } + } + + 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.ContractDoesNotExist); + } else { + await errorReporter.reportAsync(err); + throw new Error(BlockchainCallErrs.UnhandledError); + } + } + } } // tslint:disable:max-file-line-count |