diff --git a/src/const.ts b/src/const.ts index a9e7c9139..12b656fd3 100644 --- a/src/const.ts +++ b/src/const.ts @@ -142,6 +142,8 @@ export const GP_ORDER_TX_HASHES = { 4: 'GP_ORDER_TX_HASHES_4', } +export const TRADES_LOCAL_STORAGE_KEY = 'TRADES_PER_NETWORK' + const LIQUIDITY_TOKEN_LIST_VALUES = process.env.LIQUIDITY_TOKEN_LIST || 'USDT,TUSD,USDC,PAX,GUSD,DAI,sUSD' export const LIQUIDITY_TOKEN_LIST = new Set(LIQUIDITY_TOKEN_LIST_VALUES.split(',').map(symbol => symbol.trim())) export const INPUT_PRECISION_SIZE = 6 diff --git a/src/hooks/useTrades.ts b/src/hooks/useTrades.ts index f333ef005..1d52b747c 100644 --- a/src/hooks/useTrades.ts +++ b/src/hooks/useTrades.ts @@ -7,7 +7,7 @@ import { web3 } from 'api' import { getTradesAndTradeReversions } from 'services' -import { appendTrades, updateLastCheckedBlock, overwriteTrades } from 'reducers-actions/trades' +import { appendTrades, updateLastCheckedBlock } from 'reducers-actions/trades' import { useWalletConnection } from 'hooks/useWalletConnection' import useGlobalState from 'hooks/useGlobalState' @@ -16,21 +16,17 @@ import { BATCH_TIME_IN_MS } from 'const' import { getTimeRemainingInBatch } from 'utils' -// TODO: (on global state) store trades to local storage export function useTrades(): Trade[] { const [ { - trades: { trades, lastCheckedBlock }, + trades: globalStateTrades, orders: { orders }, }, dispatch, ] = useGlobalState() const { userAddress, networkId } = useWalletConnection() - useEffect(() => { - // Resetting trades on network change - dispatch(overwriteTrades({ trades: [], reverts: [], lastCheckedBlock: undefined })) - }, [dispatch, networkId, userAddress]) + const { lastCheckedBlock = undefined, trades = [] } = networkId ? globalStateTrades[networkId] : {} useEffect(() => { // Flow control. Cancel query/state update on unmount @@ -64,8 +60,8 @@ export function useTrades(): Trade[] { dispatch( newTrades.length > 0 || reverts.length > 0 - ? appendTrades({ trades: newTrades, reverts, lastCheckedBlock: toBlock }) - : updateLastCheckedBlock(toBlock), + ? appendTrades({ trades: newTrades, reverts, lastCheckedBlock: toBlock, networkId }) + : updateLastCheckedBlock(toBlock, networkId), ) } } diff --git a/src/reducers-actions/index.ts b/src/reducers-actions/index.ts index 22e5778b5..22f7fd6fa 100644 --- a/src/reducers-actions/index.ts +++ b/src/reducers-actions/index.ts @@ -12,7 +12,7 @@ import { sideEffect as OrdersSideEffect, } from './orders' import { reducer as TradeReducer, TradeState, INITIAL_TRADE_STATE as trade } from './trade' -import { reducer as TradesReducer, TradesState, INITIAL_TRADES_STATE as trades } from './trades' +import { reducer as TradesReducer, TradesState, initialState as trades, sideEffect as TradesSideEffect } from './trades' import { reducer as LocalTokensReducer, LocalTokensState, @@ -89,6 +89,6 @@ export const rootReducer = combineReducers({ pendingOrders: PendingOrderReducer, orders: addSideEffect(OrdersReducer, OrdersSideEffect), trade: TradeReducer, - trades: TradesReducer, + trades: addSideEffect(TradesReducer, TradesSideEffect), localTokens: addSideEffect(LocalTokensReducer, LocalTokensSideEffect), }) diff --git a/src/reducers-actions/trades.ts b/src/reducers-actions/trades.ts index cf2428d94..0ccfaf088 100644 --- a/src/reducers-actions/trades.ts +++ b/src/reducers-actions/trades.ts @@ -1,10 +1,19 @@ -import { Actions } from 'reducers-actions' +import BigNumber from 'bignumber.js' + import { Trade, TradeReversion, EventWithBlockInfo } from 'api/exchange/ExchangeApi' -import { logDebug, flattenMapOfLists, dateToBatchId } from 'utils' + +import { Actions } from 'reducers-actions' + +import { logDebug, flattenMapOfLists, dateToBatchId, toBN } from 'utils' +import { TRADES_LOCAL_STORAGE_KEY } from 'const' + +// ******** TYPES/INTERFACES export type ActionTypes = 'OVERWRITE_TRADES' | 'APPEND_TRADES' | 'UPDATE_BLOCK' -export interface TradesState { +export type TradesState = Record + +interface TradesStateSingleNetwork { trades: Trade[] pendingTrades: Map lastCheckedBlock?: number @@ -14,39 +23,55 @@ interface WithReverts { reverts: TradeReversion[] } -type OverwriteTradesActionType = Actions<'OVERWRITE_TRADES', Omit & WithReverts> -type AppendTradesActionType = Actions<'APPEND_TRADES', Required> & WithReverts> -type UpdateBlockActionType = Actions<'UPDATE_BLOCK', Required>> -type ReducerActionType = Actions +interface WithNetworkId { + networkId: number +} -interface Params { +// ******** ACTION TYPES + +type OverwriteTradesActionType = Actions< + 'OVERWRITE_TRADES', + Omit & WithReverts & WithNetworkId +> +type AppendTradesActionType = Actions< + 'APPEND_TRADES', + Required> & WithReverts & WithNetworkId +> +type UpdateBlockActionType = Actions< + 'UPDATE_BLOCK', + Required> & WithNetworkId +> +type ReducerActionType = Actions + +interface Params extends WithNetworkId { trades: Trade[] reverts: TradeReversion[] lastCheckedBlock?: number } -export const overwriteTrades = ({ trades, reverts, lastCheckedBlock }: Params): OverwriteTradesActionType => ({ +// ******** REDUCER FUNCTIONS + +export const overwriteTrades = (params: Params): OverwriteTradesActionType => ({ type: 'OVERWRITE_TRADES', - payload: { trades, reverts, lastCheckedBlock }, + payload: params, }) -export const appendTrades = ({ trades, reverts, lastCheckedBlock }: Required): AppendTradesActionType => ({ +export const appendTrades = (params: Required): AppendTradesActionType => ({ type: 'APPEND_TRADES', - payload: { trades, reverts, lastCheckedBlock }, + payload: params, }) -export const updateLastCheckedBlock = (lastCheckedBlock: number): UpdateBlockActionType => ({ +export const updateLastCheckedBlock = (lastCheckedBlock: number, networkId: number): UpdateBlockActionType => ({ type: 'UPDATE_BLOCK', - payload: { lastCheckedBlock }, + payload: { lastCheckedBlock, networkId }, }) -// TODO: store to/load from localStorage -export const INITIAL_TRADES_STATE = { trades: [], pendingTrades: new Map() } - function buildTradeRevertKey(batchId: number, orderId: string): string { return batchId + '|' + orderId } +// ******** HELPERS + function groupByRevertKey(list: T[], initial?: Map): Map { const map = initial || new Map() const seenIds = new Set() @@ -161,28 +186,97 @@ function applyRevertsToTrades( return [flattenMapOfLists(tradesByRevertKey), getPendingTrades(tradesByRevertKey)] } +// ******** REDUCER + export const reducer = (state: TradesState, action: ReducerActionType): TradesState => { switch (action.type) { case 'APPEND_TRADES': { - const { trades: currTrades, pendingTrades: currPendingTrades } = state - const { trades: newTrades, reverts, lastCheckedBlock } = action.payload + const { trades: newTrades, reverts, lastCheckedBlock, networkId } = action.payload + const { trades: currTrades, pendingTrades: currPendingTrades } = state[networkId] const [trades, pendingTrades] = applyRevertsToTrades(newTrades, reverts, currPendingTrades) - return { trades: currTrades.concat(trades), lastCheckedBlock, pendingTrades } + return { ...state, [networkId]: { trades: currTrades.concat(trades), lastCheckedBlock, pendingTrades } } } case 'OVERWRITE_TRADES': { - const { trades: newTrades, reverts, lastCheckedBlock } = action.payload + const { trades: newTrades, reverts, lastCheckedBlock, networkId } = action.payload const [trades, pendingTrades] = applyRevertsToTrades(newTrades, reverts) - return { trades, lastCheckedBlock, pendingTrades } + return { ...state, [networkId]: { trades, lastCheckedBlock, pendingTrades } } } case 'UPDATE_BLOCK': { - return { ...state, ...action.payload } + const { networkId, lastCheckedBlock } = action.payload + + return { ...state, [networkId]: { ...state[networkId], lastCheckedBlock } } } default: { return state } } } + +// TODO: use the one from David once his changes are merged https://github.com/gnosis/dex-react/pull/1091 +function setStorageItem(key: string, data: unknown): void { + // localStorage API accepts only strings + // TODO: consider switching to localForage API (accepts all types) + const formattedData = JSON.stringify(data) + return localStorage.setItem(key, formattedData) +} + +// ******** SIDE EFFECT + +export async function sideEffect(state: TradesState, action: ReducerActionType): Promise { + switch (action.type) { + case 'APPEND_TRADES': + case 'OVERWRITE_TRADES': + case 'UPDATE_BLOCK': + setStorageItem(TRADES_LOCAL_STORAGE_KEY, state) + } +} + +// ******** INITIAL STATE / LOCAL STORAGE + +const INITIAL_TRADES_STATE_SINGLE_NETWORK = { trades: [], pendingTrades: new Map() } + +/** + * Custom json parser for BN and BigNumber values. + * Since there's no context on what is BN/BigNumber, + * we have to keep track of keys and parse accordingly + */ +function reviver(key: string, value: unknown): unknown { + if (value && typeof value === 'string') { + switch (key) { + case 'limitPrice': + case 'fillPrice': + return new BigNumber(value) + case 'buyAmount': + case 'sellAmount': + case 'orderBuyAmount': + case 'orderSellAmount': + case 'remainingAmount': + return toBN(value) + default: + return value + } + } + return value +} + +function loadInitialState(): TradesState { + let state = { 1: INITIAL_TRADES_STATE_SINGLE_NETWORK, 4: INITIAL_TRADES_STATE_SINGLE_NETWORK } + + const localStorageOrders = localStorage.getItem(TRADES_LOCAL_STORAGE_KEY) + + if (localStorageOrders) { + try { + state = JSON.parse(localStorageOrders, reviver) + } catch (e) { + logDebug(`[reducer:trades] Failed to load localStorage`, e.msg) + } + } + + return state +} + +export const initialState = loadInitialState()