From f0b3172abe50911a4e11fbc1784e83b3920cb933 Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Wed, 11 Dec 2019 07:06:38 -0300 Subject: [PATCH] Feature #159: Pending transaction that requires user confirmation (#330) * Creates a new notification: waitingConfirmation Adds key as optional parameter for notification Implemented getAwaitingTransactions to get the transactions that needs to be confirmed by the current user Not fetchTransactions action also dispatch a notification for awaiting transactions Improved performance of routes/safe/container/index to avoid re-rendering * Removes notification logic on fetchTransactions Adds notificationsMiddleware * Moves fetchTransaction to container * Removes unused param on fetchTransactions * Fixs null safe check * Fixs middleware declaration * Removes lodash * Changes cancelled transaction detection logic --- .../notifications/notificationBuilder.js | 18 ++++++- src/logic/notifications/notificationTypes.js | 9 ++++ .../store/actions/enqueueSnackbar.js | 3 +- .../safe/transactions/awaitingTransactions.js | 36 +++++++++++++ .../safe/transactions/notifiedTransactions.js | 2 + .../safe/components/Transactions/index.jsx | 22 +------- src/routes/safe/container/index.jsx | 5 +- .../middleware/notificationsMiddleware.js | 50 +++++++++++++++++++ src/store/index.js | 3 +- 9 files changed, 123 insertions(+), 25 deletions(-) create mode 100644 src/logic/safe/transactions/awaitingTransactions.js create mode 100644 src/routes/safe/store/middleware/notificationsMiddleware.js diff --git a/src/logic/notifications/notificationBuilder.js b/src/logic/notifications/notificationBuilder.js index 006df334..d2755a25 100644 --- a/src/logic/notifications/notificationBuilder.js +++ b/src/logic/notifications/notificationBuilder.js @@ -10,6 +10,7 @@ import { type Notification, NOTIFICATIONS } from './notificationTypes' export type NotificationsQueue = { beforeExecution: Notification | null, pendingExecution: Notification | null, + waitingConfirmation: Notification | null, afterExecution: { noMoreConfirmationsNeeded: Notification | null, moreConfirmationsNeeded: Notification | null, @@ -29,6 +30,15 @@ const standardTxNotificationsQueue: NotificationsQueue = { afterExecutionError: NOTIFICATIONS.TX_FAILED_MSG, } +const waitingTransactionNotificationsQueue: NotificationsQueue = { + beforeExecution: null, + pendingExecution: null, + afterRejection: null, + waitingConfirmation: NOTIFICATIONS.TX_WAITING_MSG, + afterExecution: null, + afterExecutionError: null, +} + const confirmationTxNotificationsQueue: NotificationsQueue = { beforeExecution: NOTIFICATIONS.SIGN_TX_MSG, pendingExecution: NOTIFICATIONS.TX_CONFIRMATION_PENDING_MSG, @@ -123,6 +133,10 @@ export const getNotificationsFromTxType = (txType: string) => { notificationsQueue = ownerNameChangeNotificationsQueue break } + case TX_NOTIFICATION_TYPES.WAITING_TX: { + notificationsQueue = waitingTransactionNotificationsQueue + break + } default: { notificationsQueue = defaultNotificationsQueue break @@ -132,10 +146,12 @@ export const getNotificationsFromTxType = (txType: string) => { return notificationsQueue } -export const enhanceSnackbarForAction = (notification: Notification) => ({ +export const enhanceSnackbarForAction = (notification: Notification, key?: string, onClick?: Function) => ({ ...notification, + key, options: { ...notification.options, + onClick, action: (key: number) => ( store.dispatch(closeSnackbarAction({ key }))}> diff --git a/src/logic/notifications/notificationTypes.js b/src/logic/notifications/notificationTypes.js index 34b12c33..e3592953 100644 --- a/src/logic/notifications/notificationTypes.js +++ b/src/logic/notifications/notificationTypes.js @@ -14,6 +14,7 @@ export type Variant = 'success' | 'error' | 'warning' | 'info' export type Notification = { message: string, + key?: string, options: { variant: Variant, persist: boolean, @@ -38,6 +39,7 @@ export type Notifications = { TX_EXECUTED_MSG: Notification, TX_EXECUTED_MORE_CONFIRMATIONS_MSG: Notification, TX_FAILED_MSG: Notification, + TX_WAITING_MSG: Notification, // Approval Transactions TX_CONFIRMATION_PENDING_MSG: Notification, @@ -122,6 +124,13 @@ export const NOTIFICATIONS: Notifications = { message: 'Transaction failed', options: { variant: ERROR, persist: false, autoHideDuration: longDuration }, }, + TX_WAITING_MSG: { + message: 'A pending transaction requires your confirmation!', + key: 'TX_WAITING_MSG', + options: { + variant: WARNING, persist: true, preventDuplicate: true, + }, + }, // Approval Transactions TX_CONFIRMATION_PENDING_MSG: { diff --git a/src/logic/notifications/store/actions/enqueueSnackbar.js b/src/logic/notifications/store/actions/enqueueSnackbar.js index dae418f4..28dfc89f 100644 --- a/src/logic/notifications/store/actions/enqueueSnackbar.js +++ b/src/logic/notifications/store/actions/enqueueSnackbar.js @@ -11,9 +11,8 @@ const addSnackbar = createAction(ENQUEUE_SNACKBAR) const enqueueSnackbar = (notification: NotificationProps) => (dispatch: ReduxDispatch) => { const newNotification = { ...notification, - key: new Date().getTime(), + key: notification.key || new Date().getTime(), } - dispatch(addSnackbar(newNotification)) } diff --git a/src/logic/safe/transactions/awaitingTransactions.js b/src/logic/safe/transactions/awaitingTransactions.js new file mode 100644 index 00000000..9be23b87 --- /dev/null +++ b/src/logic/safe/transactions/awaitingTransactions.js @@ -0,0 +1,36 @@ +// @flow +import { List } from 'immutable' +import type { Transaction } from '~/routes/safe/store/models/transaction' + +export const getAwaitingTransactions = (allTransactions: List, userAccount: string): List => { + if (!allTransactions) { + return List([]) + } + + const allAwaitingTransactions = allTransactions.map((safeTransactions) => { + const nonCancelledTransactions = safeTransactions.filter((transaction: Transaction) => { + // If transactions are not executed, but there's a transaction with the same nonce EXECUTED later + // it means that the transaction was cancelled (Replaced) and shouldn't get executed + if (!transaction.isExecuted) { + const replacementTransaction = safeTransactions.findLast( + (tx) => tx.isExecuted && tx.nonce === transaction.nonce, + ) + if (replacementTransaction) { + // eslint-disable-next-line no-param-reassign + transaction = transaction.set('cancelled', true) + } + } + // The transaction is not executed and is not cancelled, so it's still waiting confirmations + if (!transaction.executionTxHash && !transaction.cancelled) { + // Then we check if the waiting confirmations are not from the current user, otherwise, filters this transaction + const transactionWaitingUser = transaction.confirmations.filter((confirmation) => confirmation.owner && confirmation.owner.address !== userAccount) + + return transactionWaitingUser.size > 0 + } + return false + }) + return nonCancelledTransactions + }) + + return allAwaitingTransactions +} diff --git a/src/logic/safe/transactions/notifiedTransactions.js b/src/logic/safe/transactions/notifiedTransactions.js index d8c0871a..368be3b7 100644 --- a/src/logic/safe/transactions/notifiedTransactions.js +++ b/src/logic/safe/transactions/notifiedTransactions.js @@ -4,6 +4,7 @@ export type NotifiedTransaction = { STANDARD_TX: string, CONFIRMATION_TX: string, CANCELLATION_TX: string, + WAITING_TX: string, SETTINGS_CHANGE_TX: string, SAFE_NAME_CHANGE_TX: string, OWNER_NAME_CHANGE_TX: string, @@ -13,6 +14,7 @@ export const TX_NOTIFICATION_TYPES: NotifiedTransaction = { STANDARD_TX: 'STANDARD_TX', CONFIRMATION_TX: 'CONFIRMATION_TX', CANCELLATION_TX: 'CANCELLATION_TX', + WAITING_TX: 'WAITING_TX', SETTINGS_CHANGE_TX: 'SETTINGS_CHANGE_TX', SAFE_NAME_CHANGE_TX: 'SAFE_NAME_CHANGE_TX', OWNER_NAME_CHANGE_TX: 'OWNER_NAME_CHANGE_TX', diff --git a/src/routes/safe/components/Transactions/index.jsx b/src/routes/safe/components/Transactions/index.jsx index 2f7d2e5e..7c21b115 100644 --- a/src/routes/safe/components/Transactions/index.jsx +++ b/src/routes/safe/components/Transactions/index.jsx @@ -1,5 +1,5 @@ // @flow -import React, { useEffect } from 'react' +import React from 'react' import { List } from 'immutable' import TxsTable from '~/routes/safe/components/Transactions/TxsTable' import { type Transaction } from '~/routes/safe/store/models/transaction' @@ -8,7 +8,6 @@ import { type Owner } from '~/routes/safe/store/models/owner' type Props = { safeAddress: string, threshold: number, - fetchTransactions: Function, transactions: List, owners: List, userAddress: string, @@ -18,8 +17,6 @@ type Props = { currentNetwork: string, } -const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000 - const Transactions = ({ transactions = List(), owners, @@ -29,22 +26,8 @@ const Transactions = ({ safeAddress, createTransaction, processTransaction, - fetchTransactions, currentNetwork, -}: Props) => { - let intervalId: IntervalID - - useEffect(() => { - fetchTransactions(safeAddress) - - intervalId = setInterval(() => { - fetchTransactions(safeAddress) - }, TIMEOUT) - - return () => clearInterval(intervalId) - }, [safeAddress]) - - return ( +}: Props) => ( ) -} export default Transactions diff --git a/src/routes/safe/container/index.jsx b/src/routes/safe/container/index.jsx index c194da21..0a8a81ad 100644 --- a/src/routes/safe/container/index.jsx +++ b/src/routes/safe/container/index.jsx @@ -34,11 +34,14 @@ class SafeView extends React.Component { componentDidMount() { const { - fetchSafe, activeTokens, safeUrl, fetchTokenBalances, fetchTokens, + fetchSafe, activeTokens, safeUrl, fetchTokenBalances, fetchTokens, fetchTransactions, safe, } = this.props fetchSafe(safeUrl) fetchTokenBalances(safeUrl, activeTokens) + if (safe && safe.address) { + fetchTransactions(safe.address) + } // fetch tokens there to get symbols for tokens in TXs list fetchTokens() diff --git a/src/routes/safe/store/middleware/notificationsMiddleware.js b/src/routes/safe/store/middleware/notificationsMiddleware.js new file mode 100644 index 00000000..c00792dc --- /dev/null +++ b/src/routes/safe/store/middleware/notificationsMiddleware.js @@ -0,0 +1,50 @@ +// @flow +import type { AnyAction, Store } from 'redux' +import { push } from 'connected-react-router' +import { type GlobalState } from '~/store/' +import { ADD_TRANSACTIONS } from '~/routes/safe/store/actions/addTransactions' +import { getAwaitingTransactions } from '~/logic/safe/transactions/awaitingTransactions' +import { userAccountSelector } from '~/logic/wallets/store/selectors' +import enqueueSnackbar from '~/logic/notifications/store/actions/enqueueSnackbar' +import { enhanceSnackbarForAction, NOTIFICATIONS } from '~/logic/notifications' +import closeSnackbarAction from '~/logic/notifications/store/actions/closeSnackbar' + +const watchedActions = [ + ADD_TRANSACTIONS, +] + +const notificationsMiddleware = (store: Store) => (next: Function) => async (action: AnyAction) => { + const handledAction = next(action) + const { dispatch } = store + + if (watchedActions.includes(action.type)) { + const state: GlobalState = store.getState() + switch (action.type) { + case ADD_TRANSACTIONS: { + const transactionsList = action.payload + const userAddress: string = userAccountSelector(state) + const awaitingTransactions = getAwaitingTransactions(transactionsList, userAddress) + + + awaitingTransactions.map((awaitingTransactionsList, safeAddress) => { + const convertedList = awaitingTransactionsList.toJS() + const notificationKey = `${safeAddress}-${userAddress}` + const onNotificationClicked = () => { + dispatch(closeSnackbarAction({ key: notificationKey })) + dispatch(push(`/safes/${safeAddress}/transactions`)) + } + if (convertedList.length > 0) { + dispatch(enqueueSnackbar(enhanceSnackbarForAction(NOTIFICATIONS.TX_WAITING_MSG, notificationKey, onNotificationClicked))) + } + }) + break + } + default: + break + } + } + + return handledAction +} + +export default notificationsMiddleware diff --git a/src/store/index.js b/src/store/index.js index f59be684..3ddeb590 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -19,6 +19,7 @@ import notifications, { type NotificationReducerState as NotificationsState, } from '~/logic/notifications/store/reducer/notifications' import cookies, { COOKIES_REDUCER_ID } from '~/logic/cookies/store/reducer/cookies' +import notificationsMiddleware from '~/routes/safe/store/middleware/notificationsMiddleware' export const history = createBrowserHistory() @@ -26,7 +27,7 @@ export const history = createBrowserHistory() // eslint-disable-next-line const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose const finalCreateStore = composeEnhancers( - applyMiddleware(thunk, routerMiddleware(history), safeStorage, providerWatcher), + applyMiddleware(thunk, routerMiddleware(history), safeStorage, providerWatcher, notificationsMiddleware), ) export type GlobalState = {