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
This commit is contained in:
Agustin Pane 2019-12-11 07:06:38 -03:00 committed by Mikhail Mikheev
parent 56a6e16158
commit f0b3172abe
9 changed files with 123 additions and 25 deletions

View File

@ -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) => (
<IconButton onClick={() => store.dispatch(closeSnackbarAction({ key }))}>
<IconClose />

View File

@ -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: {

View File

@ -11,9 +11,8 @@ const addSnackbar = createAction<string, *>(ENQUEUE_SNACKBAR)
const enqueueSnackbar = (notification: NotificationProps) => (dispatch: ReduxDispatch<GlobalState>) => {
const newNotification = {
...notification,
key: new Date().getTime(),
key: notification.key || new Date().getTime(),
}
dispatch(addSnackbar(newNotification))
}

View File

@ -0,0 +1,36 @@
// @flow
import { List } from 'immutable'
import type { Transaction } from '~/routes/safe/store/models/transaction'
export const getAwaitingTransactions = (allTransactions: List<Transaction>, userAccount: string): List<Transaction> => {
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
}

View File

@ -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',

View File

@ -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<Transaction>,
owners: List<Owner>,
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) => (
<TxsTable
transactions={transactions}
threshold={threshold}
@ -57,6 +40,5 @@ const Transactions = ({
processTransaction={processTransaction}
/>
)
}
export default Transactions

View File

@ -34,11 +34,14 @@ class SafeView extends React.Component<Props, State> {
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()

View File

@ -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<GlobalState>) => (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

View File

@ -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 = {