aboutsummaryrefslogtreecommitdiffstats
path: root/packages/instant/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'packages/instant/src/util')
-rw-r--r--packages/instant/src/util/address.ts6
-rw-r--r--packages/instant/src/util/assert.ts55
-rw-r--r--packages/instant/src/util/asset.ts70
-rw-r--r--packages/instant/src/util/asset_buyer_factory.ts17
-rw-r--r--packages/instant/src/util/buy_quote_updater.ts59
-rw-r--r--packages/instant/src/util/coinbase_api.ts7
-rw-r--r--packages/instant/src/util/error.ts68
-rw-r--r--packages/instant/src/util/error_flasher.ts26
-rw-r--r--packages/instant/src/util/etherscan.ts7
-rw-r--r--packages/instant/src/util/format.ts11
-rw-r--r--packages/instant/src/util/gas_price_estimator.ts62
-rw-r--r--packages/instant/src/util/heartbeater.ts35
-rw-r--r--packages/instant/src/util/heartbeater_factory.ts22
-rw-r--r--packages/instant/src/util/maybe_big_number.ts25
-rw-r--r--packages/instant/src/util/provider.ts12
-rw-r--r--packages/instant/src/util/provider_factory.ts34
-rw-r--r--packages/instant/src/util/provider_state_factory.ts63
-rw-r--r--packages/instant/src/util/time.ts39
-rw-r--r--packages/instant/src/util/web3_wrapper.ts5
19 files changed, 525 insertions, 98 deletions
diff --git a/packages/instant/src/util/address.ts b/packages/instant/src/util/address.ts
new file mode 100644
index 000000000..b21863a8e
--- /dev/null
+++ b/packages/instant/src/util/address.ts
@@ -0,0 +1,6 @@
+import { Web3Wrapper } from '@0x/web3-wrapper';
+
+export const getBestAddress = async (web3Wrapper: Web3Wrapper): Promise<string | undefined> => {
+ const addresses = await web3Wrapper.getAvailableAddressesAsync();
+ return addresses[0];
+};
diff --git a/packages/instant/src/util/assert.ts b/packages/instant/src/util/assert.ts
new file mode 100644
index 000000000..971c1eb96
--- /dev/null
+++ b/packages/instant/src/util/assert.ts
@@ -0,0 +1,55 @@
+import { assert as sharedAssert } from '@0x/assert';
+import { schemas } from '@0x/json-schemas';
+import { assetDataUtils } from '@0x/order-utils';
+import { AssetProxyId, ObjectMap, SignedOrder } from '@0x/types';
+import * as _ from 'lodash';
+
+import { AffiliateInfo, AssetMetaData } from '../types';
+
+export const assert = {
+ ...sharedAssert,
+ isValidOrderSource(variableName: string, orderSource: string | SignedOrder[]): void {
+ if (_.isString(orderSource)) {
+ sharedAssert.isUri(variableName, orderSource);
+ return;
+ }
+ sharedAssert.doesConformToSchema(variableName, orderSource, schemas.signedOrdersSchema);
+ },
+ areValidAssetDatas(variableName: string, assetDatas: string[]): void {
+ _.forEach(assetDatas, (assetData, index) => assert.isHexString(`${variableName}[${index}]`, assetData));
+ },
+ isValidAssetMetaDataMap(variableName: string, metaDataMap: ObjectMap<AssetMetaData>): void {
+ _.forEach(metaDataMap, (metaData, assetData) => {
+ assert.isHexString(`key ${assetData} of ${variableName}`, assetData);
+ assert.isValidAssetMetaData(`${variableName}.${assetData}`, metaData);
+ const assetDataProxyId = assetDataUtils.decodeAssetProxyId(assetData);
+ assert.assert(
+ metaData.assetProxyId === assetDataProxyId,
+ `Expected meta data for assetData ${assetData} to have asset proxy id of ${assetDataProxyId}, but instead got ${
+ metaData.assetProxyId
+ }`,
+ );
+ });
+ },
+ isValidAssetMetaData(variableName: string, metaData: AssetMetaData): void {
+ assert.isHexString(`${variableName}.assetProxyId`, metaData.assetProxyId);
+ if (!_.isUndefined(metaData.primaryColor)) {
+ assert.isString(`${variableName}.primaryColor`, metaData.primaryColor);
+ }
+ if (metaData.assetProxyId === AssetProxyId.ERC20) {
+ assert.isNumber(`${variableName}.decimals`, metaData.decimals);
+ assert.isString(`${variableName}.symbol`, metaData.symbol);
+ } else if (metaData.assetProxyId === AssetProxyId.ERC721) {
+ assert.isString(`${variableName}.name`, metaData.name);
+ assert.isUri(`${variableName}.imageUrl`, metaData.imageUrl);
+ }
+ },
+ isValidAffiliateInfo(variableName: string, affiliateInfo: AffiliateInfo): void {
+ assert.isETHAddressHex(`${variableName}.recipientAddress`, affiliateInfo.feeRecipient);
+ assert.isNumber(`${variableName}.percentage`, affiliateInfo.feePercentage);
+ assert.assert(
+ affiliateInfo.feePercentage >= 0 && affiliateInfo.feePercentage <= 0.05,
+ `Expected ${variableName}.percentage to be between 0 and 0.05, but is ${affiliateInfo.feePercentage}`,
+ );
+ },
+};
diff --git a/packages/instant/src/util/asset.ts b/packages/instant/src/util/asset.ts
index 4e3b2b946..fbfbb19f3 100644
--- a/packages/instant/src/util/asset.ts
+++ b/packages/instant/src/util/asset.ts
@@ -2,10 +2,34 @@ import { AssetProxyId, ObjectMap } from '@0x/types';
import * as _ from 'lodash';
import { assetDataNetworkMapping } from '../data/asset_data_network_mapping';
-import { Asset, AssetMetaData, Network, ZeroExInstantError } from '../types';
+import { Asset, AssetMetaData, ERC20Asset, Network, ZeroExInstantError } from '../types';
export const assetUtils = {
- createAssetFromAssetData: (
+ createAssetsFromAssetDatas: (
+ assetDatas: string[],
+ assetMetaDataMap: ObjectMap<AssetMetaData>,
+ network: Network,
+ ): Asset[] => {
+ const arrayOfAssetOrUndefined = _.map(assetDatas, assetData =>
+ assetUtils.createAssetFromAssetDataIfExists(assetData, assetMetaDataMap, network),
+ );
+ return _.compact(arrayOfAssetOrUndefined);
+ },
+ createAssetFromAssetDataIfExists: (
+ assetData: string,
+ assetMetaDataMap: ObjectMap<AssetMetaData>,
+ network: Network,
+ ): Asset | undefined => {
+ const metaData = assetUtils.getMetaDataIfExists(assetData, assetMetaDataMap, network);
+ if (_.isUndefined(metaData)) {
+ return;
+ }
+ return {
+ assetData,
+ metaData,
+ };
+ },
+ createAssetFromAssetDataOrThrow: (
assetData: string,
assetMetaDataMap: ObjectMap<AssetMetaData>,
network: Network,
@@ -16,16 +40,33 @@ export const assetUtils = {
};
},
getMetaDataOrThrow: (assetData: string, metaDataMap: ObjectMap<AssetMetaData>, network: Network): AssetMetaData => {
+ const metaDataIfExists = assetUtils.getMetaDataIfExists(assetData, metaDataMap, network);
+ if (_.isUndefined(metaDataIfExists)) {
+ throw new Error(ZeroExInstantError.AssetMetaDataNotAvailable);
+ }
+ return metaDataIfExists;
+ },
+ getMetaDataIfExists: (
+ assetData: string,
+ metaDataMap: ObjectMap<AssetMetaData>,
+ network: Network,
+ ): AssetMetaData | undefined => {
let mainnetAssetData: string | undefined = assetData;
if (network !== Network.Mainnet) {
- mainnetAssetData = assetUtils.getAssociatedAssetDataIfExists(assetData, network);
+ const mainnetAssetDataIfExists = assetUtils.getAssociatedAssetDataIfExists(
+ assetData.toLowerCase(),
+ network,
+ );
+ // Just so we don't fail in the case where we are on a non-mainnet network,
+ // but pass in a valid mainnet assetData.
+ mainnetAssetData = mainnetAssetDataIfExists || assetData;
}
if (_.isUndefined(mainnetAssetData)) {
- throw new Error(ZeroExInstantError.AssetMetaDataNotAvailable);
+ return;
}
- const metaData = metaDataMap[mainnetAssetData];
+ const metaData = metaDataMap[mainnetAssetData.toLowerCase()];
if (_.isUndefined(metaData)) {
- throw new Error(ZeroExInstantError.AssetMetaDataNotAvailable);
+ return;
}
return metaData;
},
@@ -43,6 +84,16 @@ export const assetUtils = {
return defaultName;
}
},
+ formattedSymbolForAsset: (asset?: ERC20Asset, defaultName: string = '???'): string => {
+ if (_.isUndefined(asset)) {
+ return defaultName;
+ }
+ const symbol = asset.metaData.symbol;
+ if (symbol.length <= 5) {
+ return symbol;
+ }
+ return `${symbol.slice(0, 3)}…`;
+ },
getAssociatedAssetDataIfExists: (assetData: string, network: Network): string | undefined => {
const assetDataGroupIfExists = _.find(assetDataNetworkMapping, value => value[network] === assetData);
if (_.isUndefined(assetDataGroupIfExists)) {
@@ -50,4 +101,11 @@ export const assetUtils = {
}
return assetDataGroupIfExists[Network.Mainnet];
},
+ getERC20AssetsFromAssets: (assets: Asset[]): ERC20Asset[] => {
+ const erc20sOrUndefined = _.map(
+ assets,
+ asset => (asset.metaData.assetProxyId === AssetProxyId.ERC20 ? (asset as ERC20Asset) : undefined),
+ );
+ return _.compact(erc20sOrUndefined);
+ },
};
diff --git a/packages/instant/src/util/asset_buyer_factory.ts b/packages/instant/src/util/asset_buyer_factory.ts
new file mode 100644
index 000000000..5ba46223c
--- /dev/null
+++ b/packages/instant/src/util/asset_buyer_factory.ts
@@ -0,0 +1,17 @@
+import { AssetBuyer, AssetBuyerOpts } from '@0x/asset-buyer';
+import { Provider } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { Network, OrderSource } from '../types';
+
+export const assetBuyerFactory = {
+ getAssetBuyer: (provider: Provider, orderSource: OrderSource, network: Network): AssetBuyer => {
+ const assetBuyerOptions: Partial<AssetBuyerOpts> = {
+ networkId: network,
+ };
+ const assetBuyer = _.isString(orderSource)
+ ? AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl(provider, orderSource, assetBuyerOptions)
+ : AssetBuyer.getAssetBuyerForProvidedOrders(provider, orderSource, assetBuyerOptions);
+ return assetBuyer;
+ },
+};
diff --git a/packages/instant/src/util/buy_quote_updater.ts b/packages/instant/src/util/buy_quote_updater.ts
new file mode 100644
index 000000000..c33e28f1c
--- /dev/null
+++ b/packages/instant/src/util/buy_quote_updater.ts
@@ -0,0 +1,59 @@
+import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as _ from 'lodash';
+import { Dispatch } from 'redux';
+import { oc } from 'ts-optchain';
+
+import { Action, actions } from '../redux/actions';
+import { AffiliateInfo, ERC20Asset } from '../types';
+import { assetUtils } from '../util/asset';
+import { errorFlasher } from '../util/error_flasher';
+
+export const buyQuoteUpdater = {
+ updateBuyQuoteAsync: async (
+ assetBuyer: AssetBuyer,
+ dispatch: Dispatch<Action>,
+ asset: ERC20Asset,
+ assetAmount: BigNumber,
+ setPending = true,
+ affiliateInfo?: AffiliateInfo,
+ ): Promise<void> => {
+ // get a new buy quote.
+ const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals);
+ if (setPending) {
+ // mark quote as pending
+ dispatch(actions.setQuoteRequestStatePending());
+ }
+ const feePercentage = oc(affiliateInfo).feePercentage();
+ let newBuyQuote: BuyQuote | undefined;
+ try {
+ newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage });
+ } catch (error) {
+ dispatch(actions.setQuoteRequestStateFailure());
+ let errorMessage;
+ if (error.message === AssetBuyerError.InsufficientAssetLiquidity) {
+ const assetName = assetUtils.bestNameForAsset(asset, 'of this asset');
+ errorMessage = `Not enough ${assetName} available`;
+ } else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) {
+ errorMessage = 'Not enough ZRX available';
+ } else if (
+ error.message === AssetBuyerError.StandardRelayerApiError ||
+ error.message.startsWith(AssetBuyerError.AssetUnavailable)
+ ) {
+ const assetName = assetUtils.bestNameForAsset(asset, 'This asset');
+ errorMessage = `${assetName} is currently unavailable`;
+ }
+ if (!_.isUndefined(errorMessage)) {
+ errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
+ } else {
+ throw error;
+ }
+ return;
+ }
+ // We have a successful new buy quote
+ errorFlasher.clearError(dispatch);
+ // invalidate the last buy quote.
+ dispatch(actions.updateLatestBuyQuote(newBuyQuote));
+ },
+};
diff --git a/packages/instant/src/util/coinbase_api.ts b/packages/instant/src/util/coinbase_api.ts
index 080421f98..faac8d82d 100644
--- a/packages/instant/src/util/coinbase_api.ts
+++ b/packages/instant/src/util/coinbase_api.ts
@@ -1,9 +1,10 @@
-import { BigNumber } from '@0x/utils';
+import { BigNumber, fetchAsync } from '@0x/utils';
+
+import { COINBASE_API_BASE_URL } from '../constants';
-const baseEndpoint = 'https://api.coinbase.com/v2';
export const coinbaseApi = {
getEthUsdPrice: async (): Promise<BigNumber> => {
- const res = await fetch(`${baseEndpoint}/prices/ETH-USD/buy`);
+ const res = await fetchAsync(`${COINBASE_API_BASE_URL}/prices/ETH-USD/buy`);
const resJson = await res.json();
return new BigNumber(resJson.data.amount);
},
diff --git a/packages/instant/src/util/error.ts b/packages/instant/src/util/error.ts
deleted file mode 100644
index 64c1f4885..000000000
--- a/packages/instant/src/util/error.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { AssetBuyerError } from '@0x/asset-buyer';
-import { Dispatch } from 'redux';
-
-import { Action, actions } from '../redux/actions';
-import { Asset } from '../types';
-
-import { assetUtils } from './asset';
-
-class ErrorFlasher {
- private _timeoutId?: number;
- public flashNewError(dispatch: Dispatch<Action>, error: any, delayMs: number = 7000): void {
- this._clearTimeout();
-
- // dispatch new message
- dispatch(actions.setError(error));
-
- this._timeoutId = window.setTimeout(() => {
- dispatch(actions.hideError());
- }, delayMs);
- }
- public clearError(dispatch: Dispatch<Action>): void {
- this._clearTimeout();
- dispatch(actions.hideError());
- }
- private _clearTimeout(): void {
- if (this._timeoutId) {
- window.clearTimeout(this._timeoutId);
- }
- }
-}
-
-const humanReadableMessageForError = (error: Error, asset?: Asset): string | undefined => {
- const hasInsufficientLiquidity =
- error.message === AssetBuyerError.InsufficientAssetLiquidity ||
- error.message === AssetBuyerError.InsufficientZrxLiquidity;
- if (hasInsufficientLiquidity) {
- const assetName = assetUtils.bestNameForAsset(asset, 'of this asset');
- return `Not enough ${assetName} available`;
- }
-
- if (
- error.message === AssetBuyerError.StandardRelayerApiError ||
- error.message.startsWith(AssetBuyerError.AssetUnavailable)
- ) {
- const assetName = assetUtils.bestNameForAsset(asset, 'This asset');
- return `${assetName} is currently unavailable`;
- }
-
- if (error.message === AssetBuyerError.SignatureRequestDenied) {
- return 'You denied this transaction';
- }
-
- return undefined;
-};
-
-export const errorUtil = {
- errorFlasher: new ErrorFlasher(),
- errorDescription: (error?: any, asset?: Asset): { icon: string; message: string } => {
- let bestMessage: string | undefined;
- if (error instanceof Error) {
- bestMessage = humanReadableMessageForError(error, asset);
- }
- return {
- icon: '😢',
- message: bestMessage || 'Something went wrong...',
- };
- },
-};
diff --git a/packages/instant/src/util/error_flasher.ts b/packages/instant/src/util/error_flasher.ts
new file mode 100644
index 000000000..068c12fe2
--- /dev/null
+++ b/packages/instant/src/util/error_flasher.ts
@@ -0,0 +1,26 @@
+import { Dispatch } from 'redux';
+
+import { Action, actions } from '../redux/actions';
+
+class ErrorFlasher {
+ private _timeoutId?: number;
+ public flashNewErrorMessage(dispatch: Dispatch<Action>, errorMessage?: string, delayMs: number = 7000): void {
+ this._clearTimeout();
+ // dispatch new message
+ dispatch(actions.setErrorMessage(errorMessage || 'Something went wrong...'));
+ this._timeoutId = window.setTimeout(() => {
+ dispatch(actions.hideError());
+ }, delayMs);
+ }
+ public clearError(dispatch: Dispatch<Action>): void {
+ this._clearTimeout();
+ dispatch(actions.hideError());
+ }
+ private _clearTimeout(): void {
+ if (this._timeoutId) {
+ window.clearTimeout(this._timeoutId);
+ }
+ }
+}
+
+export const errorFlasher = new ErrorFlasher();
diff --git a/packages/instant/src/util/etherscan.ts b/packages/instant/src/util/etherscan.ts
index cfc2578a3..4d62c4d9f 100644
--- a/packages/instant/src/util/etherscan.ts
+++ b/packages/instant/src/util/etherscan.ts
@@ -21,4 +21,11 @@ export const etherscanUtil = {
}
return `https://${prefix}etherscan.io/tx/${txHash}`;
},
+ getEtherScanEthAddressIfExists: (ethAddress: string, networkId: number) => {
+ const prefix = etherscanPrefix(networkId);
+ if (_.isUndefined(prefix)) {
+ return;
+ }
+ return `https://${prefix}etherscan.io/address/${ethAddress}`;
+ },
};
diff --git a/packages/instant/src/util/format.ts b/packages/instant/src/util/format.ts
index 8482b1526..44661d697 100644
--- a/packages/instant/src/util/format.ts
+++ b/packages/instant/src/util/format.ts
@@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';
-import { ethDecimals } from '../constants';
+import { ETH_DECIMALS } from '../constants';
export const format = {
ethBaseAmount: (
@@ -13,7 +13,7 @@ export const format = {
if (_.isUndefined(ethBaseAmount)) {
return defaultText;
}
- const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseAmount, ethDecimals);
+ const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseAmount, ETH_DECIMALS);
return format.ethUnitAmount(ethUnitAmount, decimalPlaces);
},
ethUnitAmount: (
@@ -24,7 +24,7 @@ export const format = {
if (_.isUndefined(ethUnitAmount)) {
return defaultText;
}
- const roundedAmount = ethUnitAmount.round(decimalPlaces);
+ const roundedAmount = ethUnitAmount.round(decimalPlaces).toDigits(decimalPlaces);
return `${roundedAmount} ETH`;
},
ethBaseAmountInUsd: (
@@ -36,7 +36,7 @@ export const format = {
if (_.isUndefined(ethBaseAmount) || _.isUndefined(ethUsdPrice)) {
return defaultText;
}
- const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseAmount, ethDecimals);
+ const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseAmount, ETH_DECIMALS);
return format.ethUnitAmountInUsd(ethUnitAmount, ethUsdPrice, decimalPlaces);
},
ethUnitAmountInUsd: (
@@ -50,4 +50,7 @@ export const format = {
}
return `$${ethUnitAmount.mul(ethUsdPrice).toFixed(decimalPlaces)}`;
},
+ ethAddress: (address: string): string => {
+ return `0x${address.slice(2, 7)}…${address.slice(-5)}`;
+ },
};
diff --git a/packages/instant/src/util/gas_price_estimator.ts b/packages/instant/src/util/gas_price_estimator.ts
new file mode 100644
index 000000000..6b15809a3
--- /dev/null
+++ b/packages/instant/src/util/gas_price_estimator.ts
@@ -0,0 +1,62 @@
+import { BigNumber, fetchAsync } from '@0x/utils';
+
+import {
+ DEFAULT_ESTIMATED_TRANSACTION_TIME_MS,
+ DEFAULT_GAS_PRICE,
+ ETH_GAS_STATION_API_BASE_URL,
+ GWEI_IN_WEI,
+} from '../constants';
+
+interface EthGasStationResult {
+ average: number;
+ fastestWait: number;
+ fastWait: number;
+ fast: number;
+ safeLowWait: number;
+ blockNum: number;
+ avgWait: number;
+ block_time: number;
+ speed: number;
+ fastest: number;
+ safeLow: number;
+}
+
+interface GasInfo {
+ gasPriceInWei: BigNumber;
+ estimatedTimeMs: number;
+}
+
+const fetchFastAmountInWeiAsync = async (): Promise<GasInfo> => {
+ const res = await fetchAsync(`${ETH_GAS_STATION_API_BASE_URL}/json/ethgasAPI.json`);
+ const gasInfo = (await res.json()) as EthGasStationResult;
+ // Eth Gas Station result is gwei * 10
+ const gasPriceInGwei = new BigNumber(gasInfo.fast / 10);
+ // Time is in minutes
+ const estimatedTimeMs = gasInfo.fastWait * 60 * 1000; // Minutes to MS
+ return { gasPriceInWei: gasPriceInGwei.mul(GWEI_IN_WEI), estimatedTimeMs };
+};
+
+export class GasPriceEstimator {
+ private _lastFetched?: GasInfo;
+ public async getGasInfoAsync(): Promise<GasInfo> {
+ let fetchedAmount: GasInfo | undefined;
+ try {
+ fetchedAmount = await fetchFastAmountInWeiAsync();
+ } catch {
+ fetchedAmount = undefined;
+ }
+
+ if (fetchedAmount) {
+ this._lastFetched = fetchedAmount;
+ }
+
+ return (
+ fetchedAmount ||
+ this._lastFetched || {
+ gasPriceInWei: DEFAULT_GAS_PRICE,
+ estimatedTimeMs: DEFAULT_ESTIMATED_TRANSACTION_TIME_MS,
+ }
+ );
+ }
+}
+export const gasPriceEstimator = new GasPriceEstimator();
diff --git a/packages/instant/src/util/heartbeater.ts b/packages/instant/src/util/heartbeater.ts
new file mode 100644
index 000000000..e700d489e
--- /dev/null
+++ b/packages/instant/src/util/heartbeater.ts
@@ -0,0 +1,35 @@
+import { intervalUtils } from '@0x/utils';
+import * as _ from 'lodash';
+
+type HeartbeatableFunction = () => Promise<void>;
+export class Heartbeater {
+ private _intervalId?: NodeJS.Timer;
+ private readonly _performImmediatelyOnStart: boolean;
+ private readonly _performFunction: HeartbeatableFunction;
+
+ public constructor(performingFunctionAsync: HeartbeatableFunction, performImmediatelyOnStart: boolean) {
+ this._performFunction = performingFunctionAsync;
+ this._performImmediatelyOnStart = performImmediatelyOnStart;
+ }
+
+ public start(intervalTimeMs: number): void {
+ if (!_.isUndefined(this._intervalId)) {
+ throw new Error('Heartbeat is running, please stop before restarting');
+ }
+
+ if (this._performImmediatelyOnStart) {
+ // tslint:disable-next-line:no-floating-promises
+ this._performFunction();
+ }
+
+ // tslint:disable-next-line:no-unbound-method
+ this._intervalId = intervalUtils.setAsyncExcludingInterval(this._performFunction, intervalTimeMs, _.noop);
+ }
+
+ public stop(): void {
+ if (this._intervalId) {
+ intervalUtils.clearInterval(this._intervalId);
+ }
+ this._intervalId = undefined;
+ }
+}
diff --git a/packages/instant/src/util/heartbeater_factory.ts b/packages/instant/src/util/heartbeater_factory.ts
new file mode 100644
index 000000000..96a8ac4e6
--- /dev/null
+++ b/packages/instant/src/util/heartbeater_factory.ts
@@ -0,0 +1,22 @@
+import { asyncData } from '../redux/async_data';
+import { Store } from '../redux/store';
+
+import { Heartbeater } from './heartbeater';
+
+export interface HeartbeatFactoryOptions {
+ store: Store;
+ shouldPerformImmediatelyOnStart: boolean;
+}
+export const generateAccountHeartbeater = (options: HeartbeatFactoryOptions): Heartbeater => {
+ const { store, shouldPerformImmediatelyOnStart } = options;
+ return new Heartbeater(async () => {
+ await asyncData.fetchAccountInfoAndDispatchToStore({ store, shouldSetToLoading: false });
+ }, shouldPerformImmediatelyOnStart);
+};
+
+export const generateBuyQuoteHeartbeater = (options: HeartbeatFactoryOptions): Heartbeater => {
+ const { store, shouldPerformImmediatelyOnStart } = options;
+ return new Heartbeater(async () => {
+ await asyncData.fetchCurrentBuyQuoteAndDispatchToStore({ store, shouldSetPending: false });
+ }, shouldPerformImmediatelyOnStart);
+};
diff --git a/packages/instant/src/util/maybe_big_number.ts b/packages/instant/src/util/maybe_big_number.ts
new file mode 100644
index 000000000..9d3746e10
--- /dev/null
+++ b/packages/instant/src/util/maybe_big_number.ts
@@ -0,0 +1,25 @@
+import { BigNumber } from '@0x/utils';
+import * as _ from 'lodash';
+
+import { Maybe } from '../types';
+
+export const maybeBigNumberUtil = {
+ // converts a string to a Maybe<BigNumber>
+ // if string is a NaN, considered undefined
+ stringToMaybeBigNumber: (stringValue: string): Maybe<BigNumber> => {
+ let validBigNumber: BigNumber;
+ try {
+ validBigNumber = new BigNumber(stringValue);
+ } catch {
+ return undefined;
+ }
+
+ return validBigNumber.isNaN() ? undefined : validBigNumber;
+ },
+ areMaybeBigNumbersEqual: (val1: Maybe<BigNumber>, val2: Maybe<BigNumber>): boolean => {
+ if (!_.isUndefined(val1) && !_.isUndefined(val2)) {
+ return val1.equals(val2);
+ }
+ return _.isUndefined(val1) && _.isUndefined(val2);
+ },
+};
diff --git a/packages/instant/src/util/provider.ts b/packages/instant/src/util/provider.ts
deleted file mode 100644
index 49705fd11..000000000
--- a/packages/instant/src/util/provider.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Provider } from 'ethereum-types';
-
-export const getProvider = (): Provider => {
- const injectedWeb3 = (window as any).web3 || undefined;
- try {
- // Use MetaMask/Mist provider
- return injectedWeb3.currentProvider;
- } catch (err) {
- // Throws when user doesn't have MetaMask/Mist running
- throw new Error(`No injected web3 found: ${err}`);
- }
-};
diff --git a/packages/instant/src/util/provider_factory.ts b/packages/instant/src/util/provider_factory.ts
new file mode 100644
index 000000000..603f7674d
--- /dev/null
+++ b/packages/instant/src/util/provider_factory.ts
@@ -0,0 +1,34 @@
+import { EmptyWalletSubprovider, RPCSubprovider, Web3ProviderEngine } from '@0x/subproviders';
+import { Provider } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { BLOCK_POLLING_INTERVAL_MS, ETHEREUM_NODE_URL_BY_NETWORK } from '../constants';
+import { Maybe, Network } from '../types';
+
+export const providerFactory = {
+ getInjectedProviderIfExists: (): Maybe<Provider> => {
+ const injectedProviderIfExists = (window as any).ethereum;
+ if (!_.isUndefined(injectedProviderIfExists)) {
+ return injectedProviderIfExists;
+ }
+ const injectedWeb3IfExists = (window as any).web3;
+ if (!_.isUndefined(injectedWeb3IfExists) && !_.isUndefined(injectedWeb3IfExists.currentProvider)) {
+ return injectedWeb3IfExists.currentProvider;
+ }
+ return undefined;
+ },
+ getFallbackNoSigningProvider: (network: Network): Provider => {
+ const providerEngine = new Web3ProviderEngine({
+ pollingInterval: BLOCK_POLLING_INTERVAL_MS,
+ });
+ // Intercept calls to `eth_accounts` and always return empty
+ providerEngine.addProvider(new EmptyWalletSubprovider());
+ // Construct an RPC subprovider, all data based requests will be sent via the RPCSubprovider
+ // TODO(bmillman): make this more resilient to infura failures
+ const rpcUrl = ETHEREUM_NODE_URL_BY_NETWORK[network];
+ providerEngine.addProvider(new RPCSubprovider(rpcUrl));
+ // // Start the Provider Engine
+ providerEngine.start();
+ return providerEngine;
+ },
+};
diff --git a/packages/instant/src/util/provider_state_factory.ts b/packages/instant/src/util/provider_state_factory.ts
new file mode 100644
index 000000000..3281f6bfb
--- /dev/null
+++ b/packages/instant/src/util/provider_state_factory.ts
@@ -0,0 +1,63 @@
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import { Provider } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { LOADING_ACCOUNT, NO_ACCOUNT } from '../constants';
+import { Maybe, Network, OrderSource, ProviderState } from '../types';
+
+import { assetBuyerFactory } from './asset_buyer_factory';
+import { providerFactory } from './provider_factory';
+
+export const providerStateFactory = {
+ getInitialProviderState: (orderSource: OrderSource, network: Network, provider?: Provider): ProviderState => {
+ if (!_.isUndefined(provider)) {
+ return providerStateFactory.getInitialProviderStateFromProvider(orderSource, network, provider);
+ }
+ const providerStateFromWindowIfExits = providerStateFactory.getInitialProviderStateFromWindowIfExists(
+ orderSource,
+ network,
+ );
+ if (providerStateFromWindowIfExits) {
+ return providerStateFromWindowIfExits;
+ } else {
+ return providerStateFactory.getInitialProviderStateFallback(orderSource, network);
+ }
+ },
+ getInitialProviderStateFromProvider: (
+ orderSource: OrderSource,
+ network: Network,
+ provider: Provider,
+ ): ProviderState => {
+ const providerState: ProviderState = {
+ provider,
+ web3Wrapper: new Web3Wrapper(provider),
+ assetBuyer: assetBuyerFactory.getAssetBuyer(provider, orderSource, network),
+ account: LOADING_ACCOUNT,
+ };
+ return providerState;
+ },
+ getInitialProviderStateFromWindowIfExists: (orderSource: OrderSource, network: Network): Maybe<ProviderState> => {
+ const injectedProviderIfExists = providerFactory.getInjectedProviderIfExists();
+ if (!_.isUndefined(injectedProviderIfExists)) {
+ const providerState: ProviderState = {
+ provider: injectedProviderIfExists,
+ web3Wrapper: new Web3Wrapper(injectedProviderIfExists),
+ assetBuyer: assetBuyerFactory.getAssetBuyer(injectedProviderIfExists, orderSource, network),
+ account: LOADING_ACCOUNT,
+ };
+ return providerState;
+ } else {
+ return undefined;
+ }
+ },
+ getInitialProviderStateFallback: (orderSource: OrderSource, network: Network): ProviderState => {
+ const provider = providerFactory.getFallbackNoSigningProvider(network);
+ const providerState: ProviderState = {
+ provider,
+ web3Wrapper: new Web3Wrapper(provider),
+ assetBuyer: assetBuyerFactory.getAssetBuyer(provider, orderSource, network),
+ account: NO_ACCOUNT,
+ };
+ return providerState;
+ },
+};
diff --git a/packages/instant/src/util/time.ts b/packages/instant/src/util/time.ts
new file mode 100644
index 000000000..bfe69cad5
--- /dev/null
+++ b/packages/instant/src/util/time.ts
@@ -0,0 +1,39 @@
+const secondsToMinutesAndRemainingSeconds = (seconds: number): { minutes: number; remainingSeconds: number } => {
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = seconds - minutes * 60;
+
+ return {
+ minutes,
+ remainingSeconds,
+ };
+};
+
+const padZero = (aNumber: number): string => {
+ return aNumber < 10 ? `0${aNumber}` : aNumber.toString();
+};
+
+export const timeUtil = {
+ // converts seconds to human readable version of seconds or minutes
+ secondsToHumanDescription: (seconds: number): string => {
+ const { minutes, remainingSeconds } = secondsToMinutesAndRemainingSeconds(seconds);
+
+ if (minutes === 0) {
+ const suffix = seconds > 1 ? 's' : '';
+ return `${seconds} second${suffix}`;
+ }
+
+ const minuteSuffix = minutes > 1 ? 's' : '';
+ const minuteText = `${minutes} minute${minuteSuffix}`;
+
+ const secondsSuffix = remainingSeconds > 1 ? 's' : '';
+ const secondsText = remainingSeconds === 0 ? '' : ` ${remainingSeconds} second${secondsSuffix}`;
+
+ return `${minuteText}${secondsText}`;
+ },
+ // converts seconds to stopwatch time (i.e. 05:30 and 00:30)
+ // only goes up to minutes, not hours
+ secondsToStopwatchTime: (seconds: number): string => {
+ const { minutes, remainingSeconds } = secondsToMinutesAndRemainingSeconds(seconds);
+ return `${padZero(minutes)}:${padZero(remainingSeconds)}`;
+ },
+};
diff --git a/packages/instant/src/util/web3_wrapper.ts b/packages/instant/src/util/web3_wrapper.ts
deleted file mode 100644
index 24dcd9076..000000000
--- a/packages/instant/src/util/web3_wrapper.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Web3Wrapper } from '@0x/web3-wrapper';
-
-import { getProvider } from './provider';
-
-export const web3Wrapper = new Web3Wrapper(getProvider());