aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts/blockchain.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/website/ts/blockchain.ts')
-rw-r--r--packages/website/ts/blockchain.ts787
1 files changed, 787 insertions, 0 deletions
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts
new file mode 100644
index 000000000..b13c48a65
--- /dev/null
+++ b/packages/website/ts/blockchain.ts
@@ -0,0 +1,787 @@
+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<boolean> {
+ 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<void> {
+ 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<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();
+ 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<BigNumber> {
+ 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<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 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<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.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<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.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<BigNumber> {
+ const balance = await this.web3Wrapper.getBalanceInEthAsync(owner);
+ return balance;
+ }
+ public async convertEthToWrappedEthTokensAsync(amount: BigNumber): Promise<void> {
+ 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<void> {
+ 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<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() {
+ clearInterval(this.zrxPollIntervalId);
+ this.web3Wrapper.destroy();
+ this.stopWatchingExchangeLogFillEventsAsync(); // fire and forget
+ }
+ 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
+ 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<void> {
+ 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<LogFillContractEventArgs>) => {
+ const decodedLog = decodedLogEvent.log;
+ 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(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 subscriptionOpts: SubscriptionOpts = {
+ fromBlock,
+ toBlock: 'latest' as BlockParam,
+ };
+ const decodedLogs = await this.zeroEx.exchange.getLogsAsync<LogFillContractEventArgs>(
+ 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<LogFillContractEventArgs>) {
+ 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<LogFillContractEventArgs>) {
+ 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<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 = 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<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.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;
+ });
+ }
+}