From ce4b4885455f4f2397e6eef210cda94ce0414b1e Mon Sep 17 00:00:00 2001 From: Daniel Dimitrov Date: Fri, 3 May 2024 12:21:05 +0200 Subject: [PATCH] feat: better notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Original issue: we had a listener for the different order statuses on the swap widget. That meant that if the user would leave the widget page he wouldn’t receive any notifications about the order. What is even worse - the user would be on the transaction page, would see the order updating and later when navigating to the widget he would now see a notification for that order. Solution: I’ve changed the way we deal with events. Instead of directly dispatching a notification whenever we receive a new event from the widget, we now store those order updates in the redux state. We update those orders based on our txHistory and on the widget updates we receive. The middleware that listens on those updates has access to the state prior to the state update and can decide if a notification needs to be shown to the user or not. fix: rename swap to order You can have swap or limit orders. Unfortunately it is a bit complicated to know whether the current order is a swap or limit order, so we are going to use the generic order. fix: type in notification title feat: delete swap order We are persisting the order slice and if the users are making a lot of orders it can grow quite a lot. Since right now we are mainly interested in the state in order to show correct notifications, we can optimise and delete the order if the state is fulfilled, cancelled or expired. Those are final states - the next time we see them from the txHistory we can ignore them, if our redux state doesn’t have a created, open or presignaturePending state. If the previous state is undefined - this means that we are not interested in those final states and can ignore them. . fix: failing swap slice test feat: change notification variant for success swap fix: state was not always cleaned up --- src/features/swap/index.tsx | 44 +- src/store/__tests__/swapOrderSlice.test.ts | 533 +++++++++++++++++++++ src/store/index.ts | 12 +- src/store/slices.ts | 1 + src/store/swapOrderSlice.ts | 206 ++++++++ 5 files changed, 767 insertions(+), 29 deletions(-) create mode 100644 src/store/__tests__/swapOrderSlice.test.ts create mode 100644 src/store/swapOrderSlice.ts diff --git a/src/features/swap/index.tsx b/src/features/swap/index.tsx index 4189747fae..0ed5714b14 100644 --- a/src/features/swap/index.tsx +++ b/src/features/swap/index.tsx @@ -14,7 +14,6 @@ import { import { useCurrentChain, useHasFeature } from '@/hooks/useChains' import { useDarkMode } from '@/hooks/useDarkMode' import { useCustomAppCommunicator } from '@/hooks/safe-apps/useCustomAppCommunicator' -import { showNotification } from '@/store/notificationsSlice' import { useAppDispatch, useAppSelector } from '@/store' import css from './styles.module.css' @@ -26,6 +25,7 @@ import useSwapConsent from './useSwapConsent' import Disclaimer from '@/components/common/Disclaimer' import LegalDisclaimerContent from '@/components/common/LegalDisclaimerContent' import { selectSwapParams, setSwapParams } from './store/swapParamsSlice' +import { setSwapOrder } from '@/store/swapOrderSlice' const BASE_URL = typeof window !== 'undefined' && window.location.origin ? window.location.origin : '' @@ -61,7 +61,6 @@ const SwapWidget = ({ sell }: Params) => { const wallet = useWallet() const { isConsentAccepted, onAccept } = useSwapConsent() - const groupKey = 'swap-order-status' const listeners = useMemo(() => { return [ { @@ -69,54 +68,45 @@ const SwapWidget = ({ sell }: Params) => { handler: (event) => { console.info('🍞 New toast message:', event) const { messageType } = event + switch (messageType) { case 'ORDER_CREATED': dispatch( - showNotification({ - title: 'Swap transaction created', - message: 'Waiting for confirmation from signers of your Safe', - groupKey, - variant: 'info', + setSwapOrder({ + orderUid: event.data.orderUid, + status: 'created', }), ) break case 'ORDER_PRESIGNED': dispatch( - showNotification({ - title: 'Swap transaction confirmed', - message: 'Waiting for swap execution by the CoW Protocol', - groupKey, - variant: 'info', + setSwapOrder({ + orderUid: event.data.orderUid, + status: 'open', }), ) break case 'ORDER_FULFILLED': dispatch( - showNotification({ - title: 'Swap executed', - message: 'Your swap has been successful', - groupKey, - variant: 'info', + setSwapOrder({ + orderUid: event.data.orderUid, + status: 'fulfilled', }), ) break case 'ORDER_EXPIRED': dispatch( - showNotification({ - title: 'Swap expired', - message: 'Your swap has reached the expiry time and has become invalid', - groupKey, - variant: 'warning', + setSwapOrder({ + orderUid: event.data.orderUid, + status: 'expired', }), ) break case 'ORDER_CANCELLED': dispatch( - showNotification({ - title: 'Swap cancelled', - message: 'Your swap has been cancelled', - groupKey, - variant: 'warning', + setSwapOrder({ + orderUid: event.data.orderUid, + status: 'cancelled', }), ) break diff --git a/src/store/__tests__/swapOrderSlice.test.ts b/src/store/__tests__/swapOrderSlice.test.ts new file mode 100644 index 0000000000..10b1c71283 --- /dev/null +++ b/src/store/__tests__/swapOrderSlice.test.ts @@ -0,0 +1,533 @@ +import { listenerMiddlewareInstance } from '@/store' +import { txHistorySlice } from '@/store/txHistorySlice' +import { swapOrderListener, swapOrderStatusListener, setSwapOrder, deleteSwapOrder } from '@/store/swapOrderSlice' +import { + TransactionListItemType, + type TransactionListItem, + TransactionInfoType, +} from '@safe-global/safe-gateway-typescript-sdk' +import * as notificationsSlice from '@/store/notificationsSlice' + +describe('swapOrderSlice', () => { + describe('swapOrderListener', () => { + const listenerMiddleware = listenerMiddlewareInstance + const mockDispatch = jest.fn() + const startListeningMock = jest.fn() + + beforeEach(() => { + jest.clearAllMocks() + listenerMiddleware.startListening = startListeningMock + swapOrderListener(listenerMiddleware) + }) + + it('should not dispatch an event if the transaction is not a swapTx', () => { + const nonSwapTransaction = { + type: TransactionListItemType.TRANSACTION, + transaction: { + id: '0x123', + txInfo: { + type: TransactionInfoType.TRANSFER, + }, + }, + } as TransactionListItem + + const action = txHistorySlice.actions.set({ + loading: false, + data: { + results: [nonSwapTransaction], + }, + }) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { dispatch: mockDispatch }) + + expect(mockDispatch).not.toHaveBeenCalled() + }) + + it('should not dispatch an event if the swapOrder status did not change', () => { + const swapTransaction = { + type: TransactionListItemType.TRANSACTION, + transaction: { + id: '0x123', + txInfo: { + type: TransactionInfoType.SWAP_ORDER, + uid: 'order1', + status: 'open', + }, + }, + } as unknown as TransactionListItem + + const action = txHistorySlice.actions.set({ + loading: false, + data: { + results: [swapTransaction], + }, + }) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getOriginalState: () => ({ + swapOrders: { + order1: { + orderUid: 'order1', + status: 'open', + }, + }, + }), + }) + + expect(mockDispatch).not.toHaveBeenCalled() + }) + + it('should dispatch setSwapOrder if the swapOrder status changed', () => { + const swapTransaction = { + type: TransactionListItemType.TRANSACTION, + transaction: { + id: '0x123', + txInfo: { + type: TransactionInfoType.SWAP_ORDER, + uid: 'order1', + status: 'fulfilled', + }, + }, + } as unknown as TransactionListItem + + const action = txHistorySlice.actions.set({ + loading: false, + data: { + results: [swapTransaction], + }, + }) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getOriginalState: () => ({ + swapOrders: { + order1: { + orderUid: 'order1', + status: 'open', + }, + }, + }), + }) + + expect(mockDispatch).toHaveBeenCalledWith( + setSwapOrder({ + orderUid: 'order1', + status: 'fulfilled', + txId: '0x123', + }), + ) + }) + + it('should not dispatch setSwapOrder if the old status is undefined and new status is fulfilled, expired, or cancelled', () => { + const swapTransaction = { + type: TransactionListItemType.TRANSACTION, + transaction: { + id: '0x123', + txInfo: { + type: TransactionInfoType.SWAP_ORDER, + uid: 'order1', + status: 'fulfilled', // Test with 'expired' and 'cancelled' as well + }, + }, + } as unknown as TransactionListItem + + const action = txHistorySlice.actions.set({ + loading: false, + data: { + results: [swapTransaction], + }, + }) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getOriginalState: () => ({ + swapOrders: {}, // Old status is undefined + }), + }) + + expect(mockDispatch).not.toHaveBeenCalled() + }) + }) + + describe('swapOrderStatusListener', () => { + const listenerMiddleware = listenerMiddlewareInstance + const mockDispatch = jest.fn() + const showNotificationSpy = jest.spyOn(notificationsSlice, 'showNotification') + const startListeningMock = jest.fn() + + beforeEach(() => { + jest.clearAllMocks() + listenerMiddleware.startListening = startListeningMock + swapOrderStatusListener(listenerMiddleware) + }) + + it('should dispatch a notification if the swapOrder status is created and threshold is 1', () => { + const swapOrder = { + orderUid: 'order1', + status: 'created' as const, + txId: '0x123', + } + + const action = setSwapOrder(swapOrder) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getState: () => ({ + safeInfo: { + data: { + threshold: 1, + }, + }, + }), + getOriginalState: () => ({ + swapOrders: {}, + }), + }) + + expect(showNotificationSpy).toHaveBeenCalledWith({ + title: 'Order created', + message: 'Waiting for the transaction to be executed', + groupKey: 'swap-order-status', + variant: 'info', + }) + }) + + it('should dispatch a notification if the swapOrder status is created and there is nothing about this swap in the state and threshold is more than 1', () => { + const swapOrder = { + orderUid: 'order1', + status: 'created' as const, + txId: '0x123', + } + + const action = setSwapOrder(swapOrder) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getState: () => ({ + safeInfo: { + data: { + threshold: 2, + }, + }, + }), + getOriginalState: () => ({ + swapOrders: {}, + }), + }) + + expect(showNotificationSpy).toHaveBeenCalledWith({ + title: 'Order created', + message: 'Waiting for confirmation from signers of your Safe', + groupKey: 'swap-order-status', + variant: 'info', + }) + }) + + it('should dispatch a notification if the swapOrder status is open and we have old status and threshold is 1', () => { + const swapOrder = { + orderUid: 'order1', + status: 'open' as const, + txId: '0x123', + } + + const action = setSwapOrder(swapOrder) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getState: () => ({ + safeInfo: { + data: { + threshold: 1, + }, + }, + }), + getOriginalState: () => ({ + swapOrders: { + order1: { + orderUid: 'order1', + status: 'created', // Old status is not undefined + }, + }, + }), + }) + + expect(showNotificationSpy).toHaveBeenCalledWith({ + title: 'Order transaction confirmed', + message: 'Waiting for order execution by the CoW Protocol', + groupKey: 'swap-order-status', + variant: 'info', + }) + }) + + it('should dispatch a notification if the swapOrder status is presignaturePending', () => { + const swapOrder = { + orderUid: 'order1', + status: 'presignaturePending' as const, + txId: '0x123', + } + + const action = setSwapOrder(swapOrder) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getState: () => ({ + safeInfo: { + data: { + threshold: 1, + }, + }, + }), + getOriginalState: () => ({ + swapOrders: {}, + }), + }) + + expect(showNotificationSpy).toHaveBeenCalledWith({ + title: 'Order waiting for signature', + message: 'Waiting for confirmation from signers of your Safe', + groupKey: 'swap-order-status', + variant: 'info', + }) + }) + + it('should dispatch a notification if the swapOrder status is open', () => { + const swapOrder = { + orderUid: 'order1', + status: 'open' as const, + txId: '0x123', + } + + const action = setSwapOrder(swapOrder) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getState: () => ({ + safeInfo: { + data: { + threshold: 1, + }, + }, + }), + getOriginalState: () => ({ + swapOrders: {}, + }), + }) + + expect(showNotificationSpy).toHaveBeenCalledWith({ + title: 'Order transaction confirmed', + message: 'Waiting for order execution by the CoW Protocol', + groupKey: 'swap-order-status', + variant: 'info', + }) + }) + + it('should not dispatch a notification if the swapOrder status is fulfilled and old status is undefined', () => { + const swapOrder = { + orderUid: 'order1', + status: 'fulfilled' as const, + txId: '0x123', + } + + const action = setSwapOrder(swapOrder) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getState: () => ({ + safeInfo: { + data: { + threshold: 1, + }, + }, + }), + getOriginalState: () => ({ + swapOrders: {}, + }), + }) + + expect(showNotificationSpy).not.toHaveBeenCalled() + }) + + it('should dispatch a notification if the swapOrder status is fulfilled and old status is not undefined', () => { + const swapOrder = { + orderUid: 'order1', + status: 'fulfilled' as const, + txId: '0x123', + } + + const action = setSwapOrder(swapOrder) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getState: () => ({ + safeInfo: { + data: { + threshold: 1, + }, + }, + }), + getOriginalState: () => ({ + swapOrders: { + order1: { + orderUid: 'order1', + status: 'open', + }, + }, + }), + }) + + expect(showNotificationSpy).toHaveBeenCalledWith({ + title: 'Order executed', + message: 'Your order has been successful', + groupKey: 'swap-order-status', + variant: 'success', + }) + + expect(mockDispatch).toHaveBeenCalledWith(deleteSwapOrder('order1')) + }) + + it('should not dispatch a notification if the swapOrder status is expired and old status is undefined', () => { + const swapOrder = { + orderUid: 'order1', + status: 'expired' as const, + txId: '0x123', + } + + const action = setSwapOrder(swapOrder) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getState: () => ({ + safeInfo: { + data: { + threshold: 1, + }, + }, + }), + getOriginalState: () => ({ + swapOrders: {}, + }), + }) + + expect(showNotificationSpy).not.toHaveBeenCalled() + expect(mockDispatch).not.toHaveBeenCalledWith(deleteSwapOrder('order1')) + }) + + it('should dispatch a notification if the swapOrder status is expired and old status is not undefined', () => { + const swapOrder = { + orderUid: 'order1', + status: 'expired' as const, + txId: '0x123', + } + + const action = setSwapOrder(swapOrder) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getState: () => ({ + safeInfo: { + data: { + threshold: 1, + }, + }, + }), + getOriginalState: () => ({ + swapOrders: { + order1: { + orderUid: 'order1', + status: 'open', + }, + }, + }), + }) + + expect(showNotificationSpy).toHaveBeenCalledWith({ + title: 'Order expired', + message: 'Your order has reached the expiry time and has become invalid', + groupKey: 'swap-order-status', + variant: 'warning', + }) + + expect(mockDispatch).toHaveBeenCalledWith(deleteSwapOrder('order1')) + }) + + it('should not dispatch a notification if the swapOrder status is cancelled and old status is undefined', () => { + const swapOrder = { + orderUid: 'order1', + status: 'cancelled' as const, + txId: '0x123', + } + + const action = setSwapOrder(swapOrder) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getState: () => ({ + safeInfo: { + data: { + threshold: 1, + }, + }, + }), + getOriginalState: () => ({ + swapOrders: {}, + }), + }) + + expect(showNotificationSpy).not.toHaveBeenCalled() + expect(mockDispatch).not.toHaveBeenCalledWith(deleteSwapOrder('order1')) + }) + + it('should dispatch a notification if the swapOrder status is cancelled and old status is not undefined', () => { + const swapOrder = { + orderUid: 'order1', + status: 'cancelled' as const, + txId: '0x123', + } + + const action = setSwapOrder(swapOrder) + + const effect = startListeningMock.mock.calls[0][0].effect + effect(action, { + dispatch: mockDispatch, + getState: () => ({ + safeInfo: { + data: { + threshold: 1, + }, + }, + }), + getOriginalState: () => ({ + swapOrders: { + order1: { + orderUid: 'order1', + status: 'open', + }, + }, + }), + }) + + expect(showNotificationSpy).toHaveBeenCalledWith({ + title: 'Order cancelled', + message: 'Your order has been cancelled', + groupKey: 'swap-order-status', + variant: 'warning', + }) + expect(mockDispatch).toHaveBeenCalledWith(deleteSwapOrder('order1')) + }) + }) +}) diff --git a/src/store/index.ts b/src/store/index.ts index b9f8665a5c..de091020b3 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -11,7 +11,13 @@ import merge from 'lodash/merge' import { IS_PRODUCTION } from '@/config/constants' import { getPreloadedState, persistState } from './persistStore' import { broadcastState, listenToBroadcast } from './broadcast' -import { safeMessagesListener, txHistoryListener, txQueueListener } from './slices' +import { + safeMessagesListener, + swapOrderListener, + swapOrderStatusListener, + txHistoryListener, + txQueueListener, +} from './slices' import * as slices from './slices' import * as hydrate from './useHydrateStore' @@ -22,6 +28,7 @@ const rootReducer = combineReducers({ [slices.sessionSlice.name]: slices.sessionSlice.reducer, [slices.txHistorySlice.name]: slices.txHistorySlice.reducer, [slices.txQueueSlice.name]: slices.txQueueSlice.reducer, + [slices.swapOrderSlice.name]: slices.swapOrderSlice.reducer, [slices.addressBookSlice.name]: slices.addressBookSlice.reducer, [slices.notificationsSlice.name]: slices.notificationsSlice.reducer, [slices.pendingTxsSlice.name]: slices.pendingTxsSlice.reducer, @@ -50,6 +57,7 @@ const persistedSlices: (keyof PreloadedState)[] = [ slices.batchSlice.name, slices.undeployedSafesSlice.name, slices.swapParamsSlice.name, + slices.swapOrderSlice.name, ] export const getPersistedState = () => { @@ -63,7 +71,7 @@ const middleware = [ broadcastState(persistedSlices), listenerMiddlewareInstance.middleware, ] -const listeners = [safeMessagesListener, txHistoryListener, txQueueListener] +const listeners = [safeMessagesListener, txHistoryListener, txQueueListener, swapOrderListener, swapOrderStatusListener] export const _hydrationReducer: typeof rootReducer = (state, action) => { if (action.type === hydrate.HYDRATE_ACTION) { diff --git a/src/store/slices.ts b/src/store/slices.ts index 6110473145..b186894502 100644 --- a/src/store/slices.ts +++ b/src/store/slices.ts @@ -18,3 +18,4 @@ export * from './pendingSafeMessagesSlice' export * from './batchSlice' export * from '@/features/counterfactual/store/undeployedSafesSlice' export * from '@/features/swap/store/swapParamsSlice' +export * from './swapOrderSlice' diff --git a/src/store/swapOrderSlice.ts b/src/store/swapOrderSlice.ts new file mode 100644 index 0000000000..2fd4afcf2b --- /dev/null +++ b/src/store/swapOrderSlice.ts @@ -0,0 +1,206 @@ +import type { listenerMiddlewareInstance } from '@/store' +import { createSelector, createSlice } from '@reduxjs/toolkit' +import type { OrderStatuses } from '@safe-global/safe-gateway-typescript-sdk' +import type { RootState } from '@/store' +import { isSwapTxInfo, isTransactionListItem } from '@/utils/transaction-guards' +import { txHistorySlice } from '@/store/txHistorySlice' +import { showNotification } from '@/store/notificationsSlice' +import { selectSafeInfo } from '@/store/safeInfoSlice' + +type AllStatuses = OrderStatuses | 'created' +type Order = { + orderUid: string + status: AllStatuses + txId?: string +} + +type SwapOrderState = { + [orderUid: string]: Order +} + +const initialState: SwapOrderState = {} + +const slice = createSlice({ + name: 'swapOrders', + initialState, + reducers: { + setSwapOrder: (state, { payload }: { payload: Order }): SwapOrderState => { + return { + ...state, + [payload.orderUid]: { + ...state[payload.orderUid], + ...payload, + }, + } + }, + deleteSwapOrder: (state, { payload }: { payload: string }): SwapOrderState => { + const newState = { ...state } + delete newState[payload] + return newState + }, + }, +}) + +export const { setSwapOrder, deleteSwapOrder } = slice.actions +const selector = (state: RootState) => state[slice.name] +export const swapOrderSlice = slice +export const selectAllSwapOrderStatuses = selector + +export const selectSwapOrderStatus = createSelector( + [selectAllSwapOrderStatuses, (_, uid: string) => uid], + (allOrders, uid): undefined | AllStatuses => { + return allOrders ? allOrders[uid]?.status : undefined + }, +) + +const groupKey = 'swap-order-status' +/** + * Listen for changes in the swap order status and determines if a notification should be shown + * + * Some gotchas: + * If the status of an order is created, presignaturePending, open - we always display a notification. + * Here it doesn't matter if the order was started through the UI or the gateway returned that order on a new browser instance. + * + * For fulfilled, expired, cancelled - we only display a notification if the old status is not undefined. + * Why? Because if the status is undefined, it means that the order was just fetched from the gateway, and + * it was already processed and there is no need to show a notification. If the status is != undefined, it means + * that the user has started the swap through the UI (or has continued it from a previous state), and we should show a notification. + * + * @param listenerMiddleware + */ +export const swapOrderStatusListener = (listenerMiddleware: typeof listenerMiddlewareInstance) => { + listenerMiddleware.startListening({ + actionCreator: slice.actions.setSwapOrder, + effect: (action, listenerApi) => { + const { dispatch } = listenerApi + const swapOrder = action.payload + const oldStatus = selectSwapOrderStatus(listenerApi.getOriginalState(), swapOrder.orderUid) + const newStatus = swapOrder.status + + if (oldStatus === newStatus || newStatus === undefined) { + return + } + + switch (newStatus) { + case 'created': + const safeInfo = selectSafeInfo(listenerApi.getState()) + + dispatch( + showNotification({ + title: 'Order created', + message: + safeInfo.data?.threshold === 1 + ? 'Waiting for the transaction to be executed' + : 'Waiting for confirmation from signers of your Safe', + groupKey, + variant: 'info', + }), + ) + + break + case 'presignaturePending': + dispatch( + showNotification({ + title: 'Order waiting for signature', + message: 'Waiting for confirmation from signers of your Safe', + groupKey, + variant: 'info', + }), + ) + break + case 'open': + dispatch( + showNotification({ + title: 'Order transaction confirmed', + message: 'Waiting for order execution by the CoW Protocol', + groupKey, + variant: 'info', + }), + ) + break + case 'fulfilled': + dispatch(slice.actions.deleteSwapOrder(swapOrder.orderUid)) + if (oldStatus === undefined) { + return + } + dispatch( + showNotification({ + title: 'Order executed', + message: 'Your order has been successful', + groupKey, + variant: 'success', + }), + ) + break + case 'expired': + dispatch(slice.actions.deleteSwapOrder(swapOrder.orderUid)) + if (oldStatus === undefined) { + return + } + dispatch( + showNotification({ + title: 'Order expired', + message: 'Your order has reached the expiry time and has become invalid', + groupKey, + variant: 'warning', + }), + ) + break + case 'cancelled': + dispatch(slice.actions.deleteSwapOrder(swapOrder.orderUid)) + if (oldStatus === undefined) { + return + } + dispatch( + showNotification({ + title: 'Order cancelled', + message: 'Your order has been cancelled', + groupKey, + variant: 'warning', + }), + ) + break + } + }, + }) +} + +/** + * Listen for changes in the tx history, check if the transaction is a swap order and update the status of the order + * @param listenerMiddleware + */ +export const swapOrderListener = (listenerMiddleware: typeof listenerMiddlewareInstance) => { + listenerMiddleware.startListening({ + actionCreator: txHistorySlice.actions.set, + effect: (action, listenerApi) => { + if (!action.payload.data) { + return + } + + for (const result of action.payload.data.results) { + if (!isTransactionListItem(result)) { + continue + } + + if (isSwapTxInfo(result.transaction.txInfo)) { + const swapOrder = result.transaction.txInfo + const oldStatus = selectSwapOrderStatus(listenerApi.getOriginalState(), swapOrder.uid) + + const finalStatuses: AllStatuses[] = ['fulfilled', 'expired', 'cancelled'] + if (oldStatus === swapOrder.status || (oldStatus === undefined && finalStatuses.includes(swapOrder.status))) { + continue + } + + listenerApi.dispatch({ + type: slice.actions.setSwapOrder.type, + payload: { + orderUid: swapOrder.uid, + status: swapOrder.status, + txId: result.transaction.id, + }, + }) + } + } + }, + }) +}