aboutsummaryrefslogtreecommitdiffstats
path: root/packages/instant/src/redux
diff options
context:
space:
mode:
Diffstat (limited to 'packages/instant/src/redux')
-rw-r--r--packages/instant/src/redux/actions.ts69
-rw-r--r--packages/instant/src/redux/async_data.ts103
-rw-r--r--packages/instant/src/redux/reducer.ts270
-rw-r--r--packages/instant/src/redux/store.ts12
4 files changed, 430 insertions, 24 deletions
diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts
new file mode 100644
index 000000000..8947c6c97
--- /dev/null
+++ b/packages/instant/src/redux/actions.ts
@@ -0,0 +1,69 @@
+import { BuyQuote } from '@0x/asset-buyer';
+import { BigNumber } from '@0x/utils';
+import * as _ from 'lodash';
+
+import { ActionsUnion, AddressAndEthBalanceInWei, Asset } from '../types';
+
+export interface PlainAction<T extends string> {
+ type: T;
+}
+
+export interface ActionWithPayload<T extends string, P> extends PlainAction<T> {
+ data: P;
+}
+
+export type Action = ActionsUnion<typeof actions>;
+
+function createAction<T extends string>(type: T): PlainAction<T>;
+function createAction<T extends string, P>(type: T, data: P): ActionWithPayload<T, P>;
+function createAction<T extends string, P>(type: T, data?: P): PlainAction<T> | ActionWithPayload<T, P> {
+ return _.isUndefined(data) ? { type } : { type, data };
+}
+
+export enum ActionTypes {
+ SET_ACCOUNT_STATE_LOADING = 'SET_ACCOUNT_STATE_LOADING',
+ SET_ACCOUNT_STATE_LOCKED = 'SET_ACCOUNT_STATE_LOCKED',
+ SET_ACCOUNT_STATE_READY = 'SET_ACCOUNT_STATE_READY',
+ UPDATE_ACCOUNT_ETH_BALANCE = 'UPDATE_ACCOUNT_ETH_BALANCE',
+ UPDATE_ETH_USD_PRICE = 'UPDATE_ETH_USD_PRICE',
+ UPDATE_SELECTED_ASSET_AMOUNT = 'UPDATE_SELECTED_ASSET_AMOUNT',
+ SET_BUY_ORDER_STATE_NONE = 'SET_BUY_ORDER_STATE_NONE',
+ SET_BUY_ORDER_STATE_VALIDATING = 'SET_BUY_ORDER_STATE_VALIDATING',
+ SET_BUY_ORDER_STATE_PROCESSING = 'SET_BUY_ORDER_STATE_PROCESSING',
+ SET_BUY_ORDER_STATE_FAILURE = 'SET_BUY_ORDER_STATE_FAILURE',
+ SET_BUY_ORDER_STATE_SUCCESS = 'SET_BUY_ORDER_STATE_SUCCESS',
+ UPDATE_LATEST_BUY_QUOTE = 'UPDATE_LATEST_BUY_QUOTE',
+ UPDATE_SELECTED_ASSET = 'UPDATE_SELECTED_ASSET',
+ SET_AVAILABLE_ASSETS = 'SET_AVAILABLE_ASSETS',
+ SET_QUOTE_REQUEST_STATE_PENDING = 'SET_QUOTE_REQUEST_STATE_PENDING',
+ SET_QUOTE_REQUEST_STATE_FAILURE = 'SET_QUOTE_REQUEST_STATE_FAILURE',
+ SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE',
+ HIDE_ERROR = 'HIDE_ERROR',
+ CLEAR_ERROR = 'CLEAR_ERROR',
+ RESET_AMOUNT = 'RESET_AMOUNT',
+}
+
+export const actions = {
+ setAccountStateLoading: () => createAction(ActionTypes.SET_ACCOUNT_STATE_LOADING),
+ setAccountStateLocked: () => createAction(ActionTypes.SET_ACCOUNT_STATE_LOCKED),
+ setAccountStateReady: (address: string) => createAction(ActionTypes.SET_ACCOUNT_STATE_READY, address),
+ updateAccountEthBalance: (addressAndBalance: AddressAndEthBalanceInWei) =>
+ createAction(ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE, addressAndBalance),
+ updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price),
+ updateSelectedAssetAmount: (amount?: BigNumber) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount),
+ setBuyOrderStateNone: () => createAction(ActionTypes.SET_BUY_ORDER_STATE_NONE),
+ setBuyOrderStateValidating: () => createAction(ActionTypes.SET_BUY_ORDER_STATE_VALIDATING),
+ setBuyOrderStateProcessing: (txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) =>
+ createAction(ActionTypes.SET_BUY_ORDER_STATE_PROCESSING, { txHash, startTimeUnix, expectedEndTimeUnix }),
+ setBuyOrderStateFailure: (txHash: string) => createAction(ActionTypes.SET_BUY_ORDER_STATE_FAILURE, txHash),
+ setBuyOrderStateSuccess: (txHash: string) => createAction(ActionTypes.SET_BUY_ORDER_STATE_SUCCESS, txHash),
+ updateLatestBuyQuote: (buyQuote?: BuyQuote) => createAction(ActionTypes.UPDATE_LATEST_BUY_QUOTE, buyQuote),
+ updateSelectedAsset: (asset: Asset) => createAction(ActionTypes.UPDATE_SELECTED_ASSET, asset),
+ setAvailableAssets: (availableAssets: Asset[]) => createAction(ActionTypes.SET_AVAILABLE_ASSETS, availableAssets),
+ setQuoteRequestStatePending: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING),
+ setQuoteRequestStateFailure: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE),
+ setErrorMessage: (errorMessage: string) => createAction(ActionTypes.SET_ERROR_MESSAGE, errorMessage),
+ hideError: () => createAction(ActionTypes.HIDE_ERROR),
+ clearError: () => createAction(ActionTypes.CLEAR_ERROR),
+ resetAmount: () => createAction(ActionTypes.RESET_AMOUNT),
+};
diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts
new file mode 100644
index 000000000..b920ac914
--- /dev/null
+++ b/packages/instant/src/redux/async_data.ts
@@ -0,0 +1,103 @@
+import { AssetProxyId } from '@0x/types';
+import * as _ from 'lodash';
+
+import { BIG_NUMBER_ZERO } from '../constants';
+import { AccountState, ERC20Asset, OrderProcessState } from '../types';
+import { assetUtils } from '../util/asset';
+import { buyQuoteUpdater } from '../util/buy_quote_updater';
+import { coinbaseApi } from '../util/coinbase_api';
+import { errorFlasher } from '../util/error_flasher';
+
+import { actions } from './actions';
+import { Store } from './store';
+
+export const asyncData = {
+ fetchEthPriceAndDispatchToStore: async (store: Store) => {
+ try {
+ const ethUsdPrice = await coinbaseApi.getEthUsdPrice();
+ store.dispatch(actions.updateEthUsdPrice(ethUsdPrice));
+ } catch (e) {
+ const errorMessage = 'Error fetching ETH/USD price';
+ errorFlasher.flashNewErrorMessage(store.dispatch, errorMessage);
+ store.dispatch(actions.updateEthUsdPrice(BIG_NUMBER_ZERO));
+ }
+ },
+ fetchAvailableAssetDatasAndDispatchToStore: async (store: Store) => {
+ const { providerState, assetMetaDataMap, network } = store.getState();
+ const assetBuyer = providerState.assetBuyer;
+ try {
+ const assetDatas = await assetBuyer.getAvailableAssetDatasAsync();
+ const assets = assetUtils.createAssetsFromAssetDatas(assetDatas, assetMetaDataMap, network);
+ store.dispatch(actions.setAvailableAssets(assets));
+ } catch (e) {
+ const errorMessage = 'Could not find any assets';
+ errorFlasher.flashNewErrorMessage(store.dispatch, errorMessage);
+ // On error, just specify that none are available
+ store.dispatch(actions.setAvailableAssets([]));
+ }
+ },
+ fetchAccountInfoAndDispatchToStore: async (options: { store: Store; shouldSetToLoading: boolean }) => {
+ const { store, shouldSetToLoading } = options;
+ const { providerState } = store.getState();
+ const web3Wrapper = providerState.web3Wrapper;
+ const provider = providerState.provider;
+ if (shouldSetToLoading && providerState.account.state !== AccountState.Loading) {
+ store.dispatch(actions.setAccountStateLoading());
+ }
+ let availableAddresses: string[];
+ try {
+ // TODO(bmillman): Add support at the web3Wrapper level for calling `eth_requestAccounts` instead of calling enable here
+ const isPrivacyModeEnabled = !_.isUndefined((provider as any).enable);
+ availableAddresses = isPrivacyModeEnabled
+ ? await (provider as any).enable()
+ : await web3Wrapper.getAvailableAddressesAsync();
+ } catch (e) {
+ store.dispatch(actions.setAccountStateLocked());
+ return;
+ }
+ if (!_.isEmpty(availableAddresses)) {
+ const activeAddress = availableAddresses[0];
+ store.dispatch(actions.setAccountStateReady(activeAddress));
+ // tslint:disable-next-line:no-floating-promises
+ asyncData.fetchAccountBalanceAndDispatchToStore(store);
+ } else {
+ store.dispatch(actions.setAccountStateLocked());
+ }
+ },
+ fetchAccountBalanceAndDispatchToStore: async (store: Store) => {
+ const { providerState } = store.getState();
+ const web3Wrapper = providerState.web3Wrapper;
+ const account = providerState.account;
+ if (account.state !== AccountState.Ready) {
+ return;
+ }
+ try {
+ const address = account.address;
+ const ethBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(address);
+ store.dispatch(actions.updateAccountEthBalance({ address, ethBalanceInWei }));
+ } catch (e) {
+ // leave balance as is
+ return;
+ }
+ },
+ fetchCurrentBuyQuoteAndDispatchToStore: async (options: { store: Store; shouldSetPending: boolean }) => {
+ const { store, shouldSetPending } = options;
+ const { buyOrderState, providerState, selectedAsset, selectedAssetAmount, affiliateInfo } = store.getState();
+ const assetBuyer = providerState.assetBuyer;
+ if (
+ !_.isUndefined(selectedAssetAmount) &&
+ !_.isUndefined(selectedAsset) &&
+ buyOrderState.processState === OrderProcessState.None &&
+ selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20
+ ) {
+ await buyQuoteUpdater.updateBuyQuoteAsync(
+ assetBuyer,
+ store.dispatch,
+ selectedAsset as ERC20Asset,
+ selectedAssetAmount,
+ shouldSetPending,
+ affiliateInfo,
+ );
+ }
+ },
+};
diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts
index 5026895ae..ef46fdd9d 100644
--- a/packages/instant/src/redux/reducer.ts
+++ b/packages/instant/src/redux/reducer.ts
@@ -1,31 +1,257 @@
-import { BigNumber } from '@0xproject/utils';
+import { BuyQuote } from '@0x/asset-buyer';
+import { AssetProxyId, ObjectMap } from '@0x/types';
+import { BigNumber } from '@0x/utils';
+import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';
-import { Action, ActionTypes } from '../types';
+import { LOADING_ACCOUNT, LOCKED_ACCOUNT } from '../constants';
+import { assetMetaDataMap } from '../data/asset_meta_data_map';
+import {
+ Account,
+ AccountReady,
+ AccountState,
+ AffiliateInfo,
+ Asset,
+ AssetMetaData,
+ AsyncProcessState,
+ DisplayStatus,
+ Network,
+ OrderProcessState,
+ OrderState,
+ ProviderState,
+} from '../types';
-export interface State {
- ethUsdPrice?: string;
- selectedAssetAmount?: BigNumber;
+import { Action, ActionTypes } from './actions';
+
+// State that is required and we have defaults for, before props are passed in
+export interface DefaultState {
+ network: Network;
+ assetMetaDataMap: ObjectMap<AssetMetaData>;
+ buyOrderState: OrderState;
+ latestErrorDisplayStatus: DisplayStatus;
+ quoteRequestState: AsyncProcessState;
+}
+
+// State that is required but needs to be derived from the props
+interface PropsDerivedState {
+ providerState: ProviderState;
}
-export const INITIAL_STATE: State = {
- ethUsdPrice: undefined,
- selectedAssetAmount: undefined,
+// State that is optional
+interface OptionalState {
+ selectedAsset: Asset;
+ availableAssets: Asset[];
+ selectedAssetAmount: BigNumber;
+ ethUsdPrice: BigNumber;
+ latestBuyQuote: BuyQuote;
+ latestErrorMessage: string;
+ affiliateInfo: AffiliateInfo;
+}
+
+export type State = DefaultState & PropsDerivedState & Partial<OptionalState>;
+
+export const DEFAULT_STATE: DefaultState = {
+ network: Network.Mainnet,
+ assetMetaDataMap,
+ buyOrderState: { processState: OrderProcessState.None },
+ latestErrorDisplayStatus: DisplayStatus.Hidden,
+ quoteRequestState: AsyncProcessState.None,
};
-export const reducer = (state: State = INITIAL_STATE, action: Action): State => {
- switch (action.type) {
- case ActionTypes.UPDATE_ETH_USD_PRICE:
- return {
- ...state,
- ethUsdPrice: action.data,
- };
- case ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT:
- return {
- ...state,
- selectedAssetAmount: action.data,
- };
- default:
- return state;
+export const createReducer = (initialState: State) => {
+ const reducer = (state: State = initialState, action: Action): State => {
+ switch (action.type) {
+ case ActionTypes.SET_ACCOUNT_STATE_LOADING:
+ return reduceStateWithAccount(state, LOADING_ACCOUNT);
+ case ActionTypes.SET_ACCOUNT_STATE_LOCKED:
+ return reduceStateWithAccount(state, LOCKED_ACCOUNT);
+ case ActionTypes.SET_ACCOUNT_STATE_READY: {
+ const account: AccountReady = {
+ state: AccountState.Ready,
+ address: action.data,
+ };
+ return reduceStateWithAccount(state, account);
+ }
+ case ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE: {
+ const { address, ethBalanceInWei } = action.data;
+ const currentAccount = state.providerState.account;
+ if (currentAccount.state !== AccountState.Ready || currentAccount.address !== address) {
+ return state;
+ } else {
+ const newAccount: AccountReady = {
+ ...currentAccount,
+ ethBalanceInWei,
+ };
+ return reduceStateWithAccount(state, newAccount);
+ }
+ }
+ case ActionTypes.UPDATE_ETH_USD_PRICE:
+ return {
+ ...state,
+ ethUsdPrice: action.data,
+ };
+ case ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT:
+ return {
+ ...state,
+ selectedAssetAmount: action.data,
+ };
+ case ActionTypes.UPDATE_LATEST_BUY_QUOTE:
+ const newBuyQuoteIfExists = action.data;
+ const shouldUpdate =
+ _.isUndefined(newBuyQuoteIfExists) || doesBuyQuoteMatchState(newBuyQuoteIfExists, state);
+ if (shouldUpdate) {
+ return {
+ ...state,
+ latestBuyQuote: newBuyQuoteIfExists,
+ quoteRequestState: AsyncProcessState.Success,
+ };
+ } else {
+ return state;
+ }
+ case ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING:
+ return {
+ ...state,
+ latestBuyQuote: undefined,
+ quoteRequestState: AsyncProcessState.Pending,
+ };
+ case ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE:
+ return {
+ ...state,
+ latestBuyQuote: undefined,
+ quoteRequestState: AsyncProcessState.Failure,
+ };
+ case ActionTypes.SET_BUY_ORDER_STATE_NONE:
+ return {
+ ...state,
+ buyOrderState: { processState: OrderProcessState.None },
+ };
+ case ActionTypes.SET_BUY_ORDER_STATE_VALIDATING:
+ return {
+ ...state,
+ buyOrderState: { processState: OrderProcessState.Validating },
+ };
+ case ActionTypes.SET_BUY_ORDER_STATE_PROCESSING:
+ const processingData = action.data;
+ const { startTimeUnix, expectedEndTimeUnix } = processingData;
+ return {
+ ...state,
+ buyOrderState: {
+ processState: OrderProcessState.Processing,
+ txHash: processingData.txHash,
+ progress: {
+ startTimeUnix,
+ expectedEndTimeUnix,
+ },
+ },
+ };
+ case ActionTypes.SET_BUY_ORDER_STATE_FAILURE:
+ const failureTxHash = action.data;
+ if ('txHash' in state.buyOrderState) {
+ if (state.buyOrderState.txHash === failureTxHash) {
+ const { txHash, progress } = state.buyOrderState;
+ return {
+ ...state,
+ buyOrderState: {
+ processState: OrderProcessState.Failure,
+ txHash,
+ progress,
+ },
+ };
+ }
+ }
+ return state;
+ case ActionTypes.SET_BUY_ORDER_STATE_SUCCESS:
+ const successTxHash = action.data;
+ if ('txHash' in state.buyOrderState) {
+ if (state.buyOrderState.txHash === successTxHash) {
+ const { txHash, progress } = state.buyOrderState;
+ return {
+ ...state,
+ buyOrderState: {
+ processState: OrderProcessState.Success,
+ txHash,
+ progress,
+ },
+ };
+ }
+ }
+ return state;
+ case ActionTypes.SET_ERROR_MESSAGE:
+ return {
+ ...state,
+ latestErrorMessage: action.data,
+ latestErrorDisplayStatus: DisplayStatus.Present,
+ };
+ case ActionTypes.HIDE_ERROR:
+ return {
+ ...state,
+ latestErrorDisplayStatus: DisplayStatus.Hidden,
+ };
+ case ActionTypes.CLEAR_ERROR:
+ return {
+ ...state,
+ latestErrorMessage: undefined,
+ latestErrorDisplayStatus: DisplayStatus.Hidden,
+ };
+ case ActionTypes.UPDATE_SELECTED_ASSET:
+ return {
+ ...state,
+ selectedAsset: action.data,
+ };
+ case ActionTypes.RESET_AMOUNT:
+ return {
+ ...state,
+ latestBuyQuote: undefined,
+ quoteRequestState: AsyncProcessState.None,
+ buyOrderState: { processState: OrderProcessState.None },
+ selectedAssetAmount: undefined,
+ };
+ case ActionTypes.SET_AVAILABLE_ASSETS:
+ return {
+ ...state,
+ availableAssets: action.data,
+ };
+ default:
+ return state;
+ }
+ };
+ return reducer;
+};
+
+const reduceStateWithAccount = (state: State, account: Account) => {
+ const oldProviderState = state.providerState;
+ const newProviderState: ProviderState = {
+ ...oldProviderState,
+ account,
+ };
+ return {
+ ...state,
+ providerState: newProviderState,
+ };
+};
+
+const doesBuyQuoteMatchState = (buyQuote: BuyQuote, state: State): boolean => {
+ const selectedAssetIfExists = state.selectedAsset;
+ const selectedAssetAmountIfExists = state.selectedAssetAmount;
+ // if no selectedAsset or selectedAssetAmount exists on the current state, return false
+ if (_.isUndefined(selectedAssetIfExists) || _.isUndefined(selectedAssetAmountIfExists)) {
+ return false;
+ }
+ // if buyQuote's assetData does not match that of the current selected asset, return false
+ if (selectedAssetIfExists.assetData !== buyQuote.assetData) {
+ return false;
+ }
+ // if ERC20 and buyQuote's assetBuyAmount does not match selectedAssetAmount, return false
+ // if ERC721, return true
+ const selectedAssetMetaData = selectedAssetIfExists.metaData;
+ if (selectedAssetMetaData.assetProxyId === AssetProxyId.ERC20) {
+ const selectedAssetAmountBaseUnits = Web3Wrapper.toBaseUnitAmount(
+ selectedAssetAmountIfExists,
+ selectedAssetMetaData.decimals,
+ );
+ const doesAssetAmountMatch = selectedAssetAmountBaseUnits.eq(buyQuote.assetBuyAmount);
+ return doesAssetAmountMatch;
+ } else {
+ return true;
}
};
diff --git a/packages/instant/src/redux/store.ts b/packages/instant/src/redux/store.ts
index fcd19f9a8..20710765d 100644
--- a/packages/instant/src/redux/store.ts
+++ b/packages/instant/src/redux/store.ts
@@ -1,6 +1,14 @@
import * as _ from 'lodash';
import { createStore, Store as ReduxStore } from 'redux';
+import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly';
-import { reducer, State } from './reducer';
+import { createReducer, State } from './reducer';
-export const store: ReduxStore<State> = createStore(reducer);
+export type Store = ReduxStore<State>;
+
+export const store = {
+ create: (initialState: State): Store => {
+ const reducer = createReducer(initialState);
+ return createStore(reducer, initialState, devToolsEnhancer({}));
+ },
+};