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.ts1504
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