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.ts75
-rw-r--r--packages/instant/src/redux/async_data.ts104
-rw-r--r--packages/instant/src/redux/reducer.ts288
-rw-r--r--packages/instant/src/redux/store.ts14
4 files changed, 481 insertions, 0 deletions
diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts
new file mode 100644
index 000000000..77e3dec12
--- /dev/null
+++ b/packages/instant/src/redux/actions.ts
@@ -0,0 +1,75 @@
+import { BuyQuote } from '@0x/asset-buyer';
+import { BigNumber } from '@0x/utils';
+import * as _ from 'lodash';
+
+import { ActionsUnion, AddressAndEthBalanceInWei, Asset, StandardSlidingPanelContent } 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_UNIT_AMOUNT = 'UPDATE_SELECTED_ASSET_UNIT_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',
+ OPEN_STANDARD_SLIDING_PANEL = 'OPEN_STANDARD_SLIDING_PANEL',
+ CLOSE_STANDARD_SLIDING_PANEL = 'CLOSE_STANDARD_SLIDING_PANEL',
+}
+
+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_UNIT_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),
+ openStandardSlidingPanel: (content: StandardSlidingPanelContent) =>
+ createAction(ActionTypes.OPEN_STANDARD_SLIDING_PANEL, content),
+ closeStandardSlidingPanel: () => createAction(ActionTypes.CLOSE_STANDARD_SLIDING_PANEL),
+};
diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts
new file mode 100644
index 000000000..5d30388b8
--- /dev/null
+++ b/packages/instant/src/redux/async_data.ts
@@ -0,0 +1,104 @@
+import { AssetProxyId } from '@0x/types';
+import { Web3Wrapper } from '@0x/web3-wrapper';
+import * as _ from 'lodash';
+import { Dispatch } from 'redux';
+
+import { BIG_NUMBER_ZERO } from '../constants';
+import { AccountState, ERC20Asset, OrderProcessState, ProviderState } 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 { State } from './reducer';
+
+export const asyncData = {
+ fetchEthPriceAndDispatchToStore: async (dispatch: Dispatch) => {
+ try {
+ const ethUsdPrice = await coinbaseApi.getEthUsdPrice();
+ dispatch(actions.updateEthUsdPrice(ethUsdPrice));
+ } catch (e) {
+ const errorMessage = 'Error fetching ETH/USD price';
+ errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
+ dispatch(actions.updateEthUsdPrice(BIG_NUMBER_ZERO));
+ }
+ },
+ fetchAvailableAssetDatasAndDispatchToStore: async (state: State, dispatch: Dispatch) => {
+ const { providerState, assetMetaDataMap, network } = state;
+ const assetBuyer = providerState.assetBuyer;
+ try {
+ const assetDatas = await assetBuyer.getAvailableAssetDatasAsync();
+ const assets = assetUtils.createAssetsFromAssetDatas(assetDatas, assetMetaDataMap, network);
+ dispatch(actions.setAvailableAssets(assets));
+ } catch (e) {
+ const errorMessage = 'Could not find any assets';
+ errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
+ // On error, just specify that none are available
+ dispatch(actions.setAvailableAssets([]));
+ }
+ },
+ fetchAccountInfoAndDispatchToStore: async (
+ providerState: ProviderState,
+ dispatch: Dispatch,
+ shouldAttemptUnlock: boolean = false,
+ shouldSetToLoading: boolean = false,
+ ) => {
+ const web3Wrapper = providerState.web3Wrapper;
+ const provider = providerState.provider;
+ if (shouldSetToLoading && providerState.account.state !== AccountState.Loading) {
+ 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 && shouldAttemptUnlock
+ ? await (provider as any).enable()
+ : await web3Wrapper.getAvailableAddressesAsync();
+ } catch (e) {
+ dispatch(actions.setAccountStateLocked());
+ return;
+ }
+ if (!_.isEmpty(availableAddresses)) {
+ const activeAddress = availableAddresses[0];
+ dispatch(actions.setAccountStateReady(activeAddress));
+ // tslint:disable-next-line:no-floating-promises
+ asyncData.fetchAccountBalanceAndDispatchToStore(activeAddress, providerState.web3Wrapper, dispatch);
+ } else {
+ dispatch(actions.setAccountStateLocked());
+ }
+ },
+ fetchAccountBalanceAndDispatchToStore: async (address: string, web3Wrapper: Web3Wrapper, dispatch: Dispatch) => {
+ try {
+ const ethBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(address);
+ dispatch(actions.updateAccountEthBalance({ address, ethBalanceInWei }));
+ } catch (e) {
+ // leave balance as is
+ return;
+ }
+ },
+ fetchCurrentBuyQuoteAndDispatchToStore: async (
+ state: State,
+ dispatch: Dispatch,
+ options: { updateSilently: boolean },
+ ) => {
+ const { buyOrderState, providerState, selectedAsset, selectedAssetUnitAmount, affiliateInfo } = state;
+ const assetBuyer = providerState.assetBuyer;
+ if (
+ !_.isUndefined(selectedAssetUnitAmount) &&
+ !_.isUndefined(selectedAsset) &&
+ buyOrderState.processState === OrderProcessState.None &&
+ selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20
+ ) {
+ await buyQuoteUpdater.updateBuyQuoteAsync(
+ assetBuyer,
+ dispatch,
+ selectedAsset as ERC20Asset,
+ selectedAssetUnitAmount,
+ { setPending: !options.updateSilently, dispatchErrors: !options.updateSilently, affiliateInfo },
+ );
+ }
+ },
+};
diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts
new file mode 100644
index 000000000..dfc2b89f3
--- /dev/null
+++ b/packages/instant/src/redux/reducer.ts
@@ -0,0 +1,288 @@
+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 { 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,
+ StandardSlidingPanelContent,
+ StandardSlidingPanelSettings,
+} from '../types';
+
+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;
+ standardSlidingPanelSettings: StandardSlidingPanelSettings;
+}
+
+// State that is required but needs to be derived from the props
+interface PropsDerivedState {
+ providerState: ProviderState;
+}
+
+// State that is optional
+interface OptionalState {
+ selectedAsset: Asset;
+ availableAssets: Asset[];
+ selectedAssetUnitAmount: 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,
+ standardSlidingPanelSettings: {
+ animationState: 'none',
+ content: StandardSlidingPanelContent.None,
+ },
+};
+
+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 address = action.data;
+ let newAccount: AccountReady = {
+ state: AccountState.Ready,
+ address,
+ };
+ const currentAccount = state.providerState.account;
+ if (currentAccount.state === AccountState.Ready && currentAccount.address === address) {
+ newAccount = {
+ ...newAccount,
+ ethBalanceInWei: currentAccount.ethBalanceInWei,
+ };
+ }
+ return reduceStateWithAccount(state, newAccount);
+ }
+ 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_UNIT_AMOUNT:
+ return {
+ ...state,
+ selectedAssetUnitAmount: 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 },
+ selectedAssetUnitAmount: undefined,
+ };
+ case ActionTypes.SET_AVAILABLE_ASSETS:
+ return {
+ ...state,
+ availableAssets: action.data,
+ };
+ case ActionTypes.OPEN_STANDARD_SLIDING_PANEL:
+ return {
+ ...state,
+ standardSlidingPanelSettings: {
+ content: action.data,
+ animationState: 'slidIn',
+ },
+ };
+ case ActionTypes.CLOSE_STANDARD_SLIDING_PANEL:
+ return {
+ ...state,
+ standardSlidingPanelSettings: {
+ content: state.standardSlidingPanelSettings.content,
+ animationState: 'slidOut',
+ },
+ };
+ 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 selectedAssetUnitAmountIfExists = state.selectedAssetUnitAmount;
+ // if no selectedAsset or selectedAssetAmount exists on the current state, return false
+ if (_.isUndefined(selectedAssetIfExists) || _.isUndefined(selectedAssetUnitAmountIfExists)) {
+ 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(
+ selectedAssetUnitAmountIfExists,
+ 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
new file mode 100644
index 000000000..20710765d
--- /dev/null
+++ b/packages/instant/src/redux/store.ts
@@ -0,0 +1,14 @@
+import * as _ from 'lodash';
+import { createStore, Store as ReduxStore } from 'redux';
+import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly';
+
+import { createReducer, State } from './reducer';
+
+export type Store = ReduxStore<State>;
+
+export const store = {
+ create: (initialState: State): Store => {
+ const reducer = createReducer(initialState);
+ return createStore(reducer, initialState, devToolsEnhancer({}));
+ },
+};