diff --git a/src/components/TradesWidget/index.tsx b/src/components/TradesWidget/index.tsx index 3edd71d5a..efe05b926 100644 --- a/src/components/TradesWidget/index.tsx +++ b/src/components/TradesWidget/index.tsx @@ -88,9 +88,10 @@ const Trades: React.FC = () => { const { networkId, userAddress, isConnected } = useWalletConnection() const trades = useTrades() - const filteredTrades = useMemo(() => trades.filter(trade => isTradeSettled(trade) && !isTradeReverted(trade)), [ - trades, - ]) + const filteredTrades = useMemo( + () => trades.filter(trade => trade && isTradeSettled(trade) && !isTradeReverted(trade)), + [trades], + ) const generateCsv = useCallback( () => diff --git a/src/hooks/useTrades.ts b/src/hooks/useTrades.ts index be37dd354..e4e6563b1 100644 --- a/src/hooks/useTrades.ts +++ b/src/hooks/useTrades.ts @@ -37,35 +37,39 @@ export function useTrades(): Trade[] { async function updateTrades(): Promise { if (userAddress && networkId) { - // Don't want to update on every block - // So instead, we get the latest block when the time comes - const toBlock = await web3.eth.getBlockNumber() - const params = { - userAddress, - networkId, - // fromBlock is inclusive. If set, add 1 to avoid duplicates, otherwise return undefined - fromBlock: !lastCheckedBlock ? lastCheckedBlock : lastCheckedBlock + 1, - toBlock, - orders, + try { + // Don't want to update on every block + // So instead, we get the latest block when the time comes + const toBlock = await web3.eth.getBlockNumber() + const params = { + userAddress, + networkId, + // fromBlock is inclusive. If set, add 1 to avoid duplicates, otherwise return undefined + fromBlock: !lastCheckedBlock ? lastCheckedBlock : lastCheckedBlock + 1, + toBlock, + orders, + } + + // Check before expensive operation + if (cancelled) { + return + } + + const { trades: newTrades, reverts } = await getTradesAndTradeReversions(params) + + // Check before updating state + if (cancelled) { + return + } + + dispatch( + newTrades.length > 0 || reverts.length > 0 + ? appendTrades({ lastCheckedBlock: toBlock, networkId, userAddress, trades: newTrades, reverts }) + : updateLastCheckedBlock({ lastCheckedBlock: toBlock, networkId, userAddress }), + ) + } catch (e) { + console.error(`Failed to update trades`, e) } - - // Check before expensive operation - if (cancelled) { - return - } - - const { trades: newTrades, reverts } = await getTradesAndTradeReversions(params) - - // Check before updating state - if (cancelled) { - return - } - - dispatch( - newTrades.length > 0 || reverts.length > 0 - ? appendTrades({ lastCheckedBlock: toBlock, networkId, userAddress, trades: newTrades, reverts }) - : updateLastCheckedBlock({ lastCheckedBlock: toBlock, networkId, userAddress }), - ) } } diff --git a/src/reducers-actions/trades.ts b/src/reducers-actions/trades.ts index 5952f556f..10a74c866 100644 --- a/src/reducers-actions/trades.ts +++ b/src/reducers-actions/trades.ts @@ -4,7 +4,7 @@ import { Trade, TradeReversion, EventWithBlockInfo } from 'api/exchange/Exchange import { Actions } from 'reducers-actions' -import { logDebug, flattenMapOfLists, dateToBatchId, toBN, setStorageItem } from 'utils' +import { logDebug, dateToBatchId, toBN, setStorageItem, flattenMapOfLists } from 'utils' import { TRADES_LOCAL_STORAGE_KEY } from 'const' // ******** TYPES/INTERFACES @@ -12,13 +12,20 @@ import { TRADES_LOCAL_STORAGE_KEY } from 'const' export type ActionTypes = 'OVERWRITE_TRADES' | 'APPEND_TRADES' | 'UPDATE_BLOCK' export type TradesState = Record +export type SerializableTradesState = Record interface TradesStatePerAccount { trades: Trade[] + tradeIds: Set pendingTrades: Map lastCheckedBlock?: number } +interface SerializableTradesStatePerAccount extends Omit { + tradeIds: string[] + pendingTrades: [string, Trade[]][] +} + interface WithReverts { reverts: TradeReversion[] } @@ -32,11 +39,11 @@ interface WithAccountInfo { type OverwriteTradesActionType = Actions< 'OVERWRITE_TRADES', - Omit & WithReverts & WithAccountInfo + Pick & WithReverts & WithAccountInfo > type AppendTradesActionType = Actions< 'APPEND_TRADES', - Required> & WithReverts & WithAccountInfo + Required> & WithReverts & WithAccountInfo > type UpdateBlockActionType = Actions< 'UPDATE_BLOCK', @@ -79,29 +86,34 @@ function buildTradeRevertKey(batchId: number, orderId: string): string { // ******** HELPERS -function groupByRevertKey(list: T[], initial?: Map): Map { - const map = initial || new Map() - const seenIds = new Set() +function groupByRevertKey( + list: T[], + initial?: Map, + seenTradeIds?: Set, +): { group: Map; seenIds: Set } { + const group = initial || new Map() + const seenIds = seenTradeIds || new Set() + const newIds = new Set() list.forEach(item => { // Avoid duplicate entries - if (seenIds.has(item.id)) { + if (seenIds.has(item.id) || newIds.has(item.id)) { return } - seenIds.add(item.id) + newIds.add(item.id) const revertKey = buildTradeRevertKey(item.batchId, item.orderId) - if (map.has(revertKey)) { - const subList = map.get(revertKey) as T[] + if (group.has(revertKey)) { + const tradesList = group.get(revertKey) as T[] - subList.push(item) + tradesList.push(item) } else { - map.set(revertKey, [item]) + group.set(revertKey, [item]) } }) - return map + return { group, seenIds: newIds } } function sortByTimeAndPosition(a: EventWithBlockInfo, b: EventWithBlockInfo): number { @@ -121,7 +133,7 @@ function getPendingTrades(tradesByRevertKey: Map): Map() @@ -138,10 +150,11 @@ function applyRevertsToTrades( trades: Trade[], reverts: TradeReversion[], pendingTrades?: Map, -): [Trade[], Map] { + seenIds?: Set, +): { trades: Trade[]; pendingTrades: Map; seenIds: Set } { // Group trades by revertKey - const tradesByRevertKey = groupByRevertKey(trades, pendingTrades) - const revertsByRevertKey = groupByRevertKey(reverts) + const { group: tradesByRevertKey, seenIds: newTradeIds } = groupByRevertKey(trades, pendingTrades, seenIds) + const { group: revertsByRevertKey } = groupByRevertKey(reverts) // Assumptions: // 1. There can be more than one trade per batch for a given order (even if there are no reverts) @@ -150,8 +163,8 @@ function applyRevertsToTrades( // 3. Every revert matches 1 trade // 4. Reverts match Trades by order or appearance (first Revert matches first Trade and so on) - revertsByRevertKey.forEach((reverts, revertKey) => { - reverts.sort(sortByTimeAndPosition) + revertsByRevertKey.forEach((revertsList, revertKey) => { + const reverts = revertsList.sort(sortByTimeAndPosition) const trades = tradesByRevertKey.get(revertKey)?.sort(sortByTimeAndPosition) if (trades) { @@ -190,12 +203,28 @@ function applyRevertsToTrades( } }) - return [flattenMapOfLists(tradesByRevertKey), getPendingTrades(tradesByRevertKey)] + // Transform groups into a single list + const newTrades = flattenMapOfLists(tradesByRevertKey) + // Remove old trades that were used only to help matching possible reverts to new trades + .filter(trade => trade && newTradeIds.has(trade.id)) + + // Merge existing and new set of ids + seenIds?.forEach(id => id && newTradeIds.add(id)) + + return { + trades: newTrades, + pendingTrades: getPendingTrades(tradesByRevertKey), + seenIds: newTradeIds, + } } // ******** INITIAL STATE / LOCAL STORAGE -const INITIAL_TRADES_STATE_SINGLE_NETWORK = { trades: [], pendingTrades: new Map() } +const INITIAL_TRADES_STATE_SINGLE_ACCOUNT = { + trades: [], + pendingTrades: new Map(), + tradeIds: new Set(), +} /** * Custom json parser for BN and BigNumber values. @@ -221,6 +250,34 @@ function reviver(key: string, value: unknown): unknown { return value } +function serialize(state: TradesState): SerializableTradesState { + const serialized = {} + + Object.keys(state).forEach(accountKey => { + serialized[accountKey] = { ...state[accountKey], tradeIds: [], pendingTrades: [] } + state[accountKey].tradeIds?.forEach(id => serialized[accountKey].tradeIds.push(id)) + state[accountKey].pendingTrades?.forEach((value, key) => serialized[accountKey].pendingTrades.push([key, value])) + }) + + return serialized +} + +function deserialize(state: SerializableTradesState): TradesState { + const parsedState = {} + + Object.keys(state).forEach(accountKey => { + parsedState[accountKey] = { + ...state[accountKey], + tradeIds: new Set(), + pendingTrades: new Map(), + } + state[accountKey].tradeIds?.forEach(id => parsedState[accountKey].tradeIds.add(id)) + state[accountKey].pendingTrades?.forEach(tuple => parsedState[accountKey].pendingTrades.set(...tuple)) + }) + + return parsedState +} + function loadInitialState(): TradesState { let state = {} @@ -228,7 +285,7 @@ function loadInitialState(): TradesState { if (localStorageOrders) { try { - state = JSON.parse(localStorageOrders, reviver) + state = deserialize(JSON.parse(localStorageOrders, reviver)) } catch (e) { logDebug(`[reducer:trades] Failed to load localStorage`, e.msg) } @@ -248,21 +305,25 @@ export const reducer = (state: TradesState, action: ReducerActionType): TradesSt const accountKey = buildAccountKey({ networkId, userAddress }) - const { trades: currTrades, pendingTrades: currPendingTrades } = - state[accountKey] || INITIAL_TRADES_STATE_SINGLE_NETWORK + const { trades: currTrades, pendingTrades: currPendingTrades, tradeIds } = + state[accountKey] || INITIAL_TRADES_STATE_SINGLE_ACCOUNT - const [trades, pendingTrades] = applyRevertsToTrades(newTrades, reverts, currPendingTrades) + const { trades, pendingTrades, seenIds } = applyRevertsToTrades(newTrades, reverts, currPendingTrades, tradeIds) - return { ...state, [accountKey]: { trades: currTrades.concat(trades), lastCheckedBlock, pendingTrades } } + return { + ...state, + [accountKey]: { trades: currTrades.concat(trades), lastCheckedBlock, pendingTrades, tradeIds: seenIds }, + } } case 'OVERWRITE_TRADES': { const { trades: newTrades, reverts, lastCheckedBlock, networkId, userAddress } = action.payload const accountKey = buildAccountKey({ networkId, userAddress }) + const tradeIds = new Set() - const [trades, pendingTrades] = applyRevertsToTrades(newTrades, reverts) + const { trades, pendingTrades, seenIds } = applyRevertsToTrades(newTrades, reverts, undefined, tradeIds) - return { ...state, [accountKey]: { trades, lastCheckedBlock, pendingTrades } } + return { ...state, [accountKey]: { trades, lastCheckedBlock, pendingTrades, tradeIds: seenIds } } } case 'UPDATE_BLOCK': { const { networkId, lastCheckedBlock } = action.payload @@ -282,6 +343,6 @@ export async function sideEffect(state: TradesState, action: ReducerActionType): case 'APPEND_TRADES': case 'OVERWRITE_TRADES': case 'UPDATE_BLOCK': - setStorageItem(TRADES_LOCAL_STORAGE_KEY, state) + setStorageItem(TRADES_LOCAL_STORAGE_KEY, serialize(state)) } } diff --git a/src/utils/miscellaneous.ts b/src/utils/miscellaneous.ts index 3ef127710..7720bf559 100644 --- a/src/utils/miscellaneous.ts +++ b/src/utils/miscellaneous.ts @@ -183,3 +183,7 @@ export async function retry any>(fn: T, options?: RetryOptions): export function flattenMapOfLists(map: Map): T[] { return Array.from(map.values()).reduce((acc, list) => acc.concat(list), []) } + +export function flattenMapOfSets(map: Map>): T[] { + return Array.from(map.values()).reduce((acc, set) => acc.concat(Array.from(set)), []) +}