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