diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnerComponent.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnerComponent.tsx
index c4ca0a60..bd381a32 100644
--- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnerComponent.tsx
+++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnerComponent.tsx
@@ -8,6 +8,7 @@ import ConfirmSmallFilledCircle from './assets/confirm-small-filled.svg'
import ConfirmSmallGreenCircle from './assets/confirm-small-green.svg'
import ConfirmSmallGreyCircle from './assets/confirm-small-grey.svg'
import ConfirmSmallRedCircle from './assets/confirm-small-red.svg'
+import PendingSmallYellowCircle from './assets/confirm-small-yellow.svg'
import { styles } from './style'
import EtherscanLink from 'src/components/EtherscanLink'
@@ -32,6 +33,8 @@ const OwnerComponent = ({
onTxExecute,
onTxReject,
owner,
+ pendingAcceptAction,
+ pendingRejectAction,
showConfirmBtn,
showExecuteBtn,
showExecuteRejectBtn,
@@ -43,18 +46,110 @@ const OwnerComponent = ({
const [imgCircle, setImgCircle] = React.useState(ConfirmSmallGreyCircle)
React.useMemo(() => {
+ if (pendingAcceptAction || pendingRejectAction) {
+ setImgCircle(PendingSmallYellowCircle)
+ return
+ }
if (confirmed) {
setImgCircle(isCancelTx ? CancelSmallFilledCircle : ConfirmSmallFilledCircle)
- } else if (thresholdReached || executor) {
- setImgCircle(isCancelTx ? ConfirmSmallRedCircle : ConfirmSmallGreenCircle)
+ return
}
- }, [confirmed, thresholdReached, executor, isCancelTx])
+ if (thresholdReached || executor) {
+ setImgCircle(isCancelTx ? ConfirmSmallRedCircle : ConfirmSmallGreenCircle)
+ return
+ }
+ setImgCircle(ConfirmSmallGreyCircle)
+ }, [confirmed, thresholdReached, executor, isCancelTx, pendingAcceptAction, pendingRejectAction])
- const getTimelineLine = () => (isCancelTx ? classes.verticalLineCancel : classes.verticalLineDone)
+ const getTimelineLine = () => {
+ if (pendingAcceptAction || pendingRejectAction) {
+ return classes.verticalPendingAction
+ }
+ if (isCancelTx) {
+ return classes.verticalLineCancel
+ }
+ return classes.verticalLineDone
+ }
+
+ const confirmButton = () => {
+ if (pendingRejectAction) {
+ return null
+ }
+ if (pendingAcceptAction) {
+ return Pending
+ }
+ return (
+ <>
+ {showConfirmBtn && (
+
+ )}
+ {showExecuteBtn && (
+
+ )}
+ >
+ )
+ }
+
+ const rejectButton = () => {
+ if (pendingRejectAction) {
+ return Pending
+ }
+ if (pendingAcceptAction) {
+ return null
+ }
+ return (
+ <>
+ {showRejectBtn && (
+
+ )}
+ {showExecuteRejectBtn && (
+
+ )}
+ >
+ )
+ }
return (
-
+
@@ -66,61 +161,7 @@ const OwnerComponent = ({
- {owner === userAddress && (
-
- {isCancelTx ? (
- <>
- {showRejectBtn && (
-
- )}
- {showExecuteRejectBtn && (
-
- )}
- >
- ) : (
- <>
- {showConfirmBtn && (
-
- )}
- {showExecuteBtn && (
-
- )}
- >
- )}
-
- )}
+ {owner === userAddress && {isCancelTx ? rejectButton() : confirmButton()}}
{owner === executor && Executor}
)
diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnersList.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnersList.tsx
index 385da37e..8cd107f9 100644
--- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnersList.tsx
+++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnersList.tsx
@@ -38,7 +38,7 @@ const OwnersList = ({
userAddress={userAddress}
/>
))}
- {ownersUnconfirmed.map((owner) => (
+ {ownersUnconfirmed.map(({ hasPendingAcceptActions, hasPendingRejectActions, owner }) => (
{
const confirmationsEntry = tx.confirmations.find((conf) => conf.owner === owner.address)
if (!confirmationsEntry) {
- ownersNotConfirmed.push(owner.address)
+ ownersWithNoConfirmations.push(owner.address)
}
if (confirmationsEntry && confirmationsEntry.owner === userAddress) {
currentUserNotConfirmed = false
}
})
- return [ownersNotConfirmed, currentUserNotConfirmed]
+ const confirmationPendingActions = tx.ownersWithPendingActions.get('confirm')
+ const confirmationRejectActions = tx.ownersWithPendingActions.get('reject')
+
+ const ownersWithNoConfirmationsSorted = ownersWithNoConfirmations
+ .map((owner) => ({
+ hasPendingAcceptActions: confirmationPendingActions.includes(owner),
+ hasPendingRejectActions: confirmationRejectActions.includes(owner),
+ owner,
+ }))
+ // Reorders the list of unconfirmed owners, owners with pendingActions should be first
+ .sort((ownerA, ownerB) => {
+ // If the first owner has pending actions, A should be before B
+ if (ownerA.hasPendingRejectActions || ownerA.hasPendingAcceptActions) {
+ return -1
+ }
+ // The first owner has not pending actions but the second yes, B should be before A
+ if (ownerB.hasPendingRejectActions || ownerB.hasPendingAcceptActions) {
+ return 1
+ }
+ // Otherwise do not change order
+ return 0
+ })
+
+ return [ownersWithNoConfirmationsSorted, currentUserNotConfirmed]
}
const OwnersColumn = ({
diff --git a/src/routes/safe/store/actions/createTransaction.ts b/src/routes/safe/store/actions/createTransaction.ts
index 42de4219..01c6046a 100644
--- a/src/routes/safe/store/actions/createTransaction.ts
+++ b/src/routes/safe/store/actions/createTransaction.ts
@@ -1,5 +1,6 @@
import { push } from 'connected-react-router'
-import { List, Map, fromJS } from 'immutable'
+import { List, Map } from 'immutable'
+import { batch } from 'react-redux'
import semverSatisfies from 'semver/functions/satisfies'
import { onboardUser } from 'src/components/ConnectButton'
@@ -22,30 +23,41 @@ import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/routes/s
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import { makeConfirmation } from '../models/confirmation'
import fetchTransactions from './transactions/fetchTransactions'
+import { safeTransactionsSelector } from 'src/routes/safe/store/selectors'
-export const removeTxFromStore = (tx, safeAddress, dispatch) => {
+export const removeTxFromStore = (tx, safeAddress, dispatch, state) => {
if (tx.isCancellationTx) {
- dispatch(removeCancellationTransaction({ safeAddress, transaction: tx }))
+ const newTxStatus = 'awaiting_confirmations'
+ const transactions = safeTransactionsSelector(state)
+ const txsToUpdate = transactions
+ .filter((transaction) => Number(transaction.nonce) === Number(tx.nonce))
+ .withMutations((list) => list.map((tx) => tx.set('status', newTxStatus)))
+
+ batch(() => {
+ dispatch(addOrUpdateTransactions({ safeAddress, transactions: txsToUpdate }))
+ dispatch(removeCancellationTransaction({ safeAddress, transaction: tx }))
+ })
} else {
dispatch(removeTransaction({ safeAddress, transaction: tx }))
}
}
-export const storeTx = async (tx, safeAddress, dispatch) => {
+export const storeTx = async (tx, safeAddress, dispatch, state) => {
if (tx.isCancellationTx) {
- dispatch(
- addOrUpdateCancellationTransactions({
- safeAddress,
- transactions: Map({ [`${tx.nonce}`]: tx }),
- }),
- )
+ const newTxStatus = tx.isExecuted ? 'cancelled' : tx.status === 'pending' ? 'pending' : 'awaiting_confirmations'
+ const transactions = safeTransactionsSelector(state)
+ const txsToUpdate = transactions
+ .filter((transaction) => Number(transaction.nonce) === Number(tx.nonce))
+ .withMutations((list) =>
+ list.map((tx) => tx.set('status', newTxStatus).set('cancelled', newTxStatus === 'cancelled')),
+ )
+
+ batch(() => {
+ dispatch(addOrUpdateCancellationTransactions({ safeAddress, transactions: Map({ [`${tx.nonce}`]: tx }) }))
+ dispatch(addOrUpdateTransactions({ safeAddress, transactions: txsToUpdate }))
+ })
} else {
- dispatch(
- addOrUpdateTransactions({
- safeAddress,
- transactions: List([tx]),
- }),
- )
+ dispatch(addOrUpdateTransactions({ safeAddress, transactions: List([tx]) }))
}
}
@@ -151,15 +163,26 @@ const createTransaction = ({
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar)
- await Promise.all([saveTxToHistory({ ...txArgs, txHash, origin }), storeTx(mockedTx, safeAddress, dispatch)])
+ await Promise.all([
+ saveTxToHistory({ ...txArgs, txHash, origin }),
+ storeTx(
+ mockedTx.updateIn(
+ ['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
+ (previous) => previous.push(from),
+ ),
+ safeAddress,
+ dispatch,
+ state,
+ ),
+ ])
dispatch(fetchTransactions(safeAddress))
} catch (e) {
- removeTxFromStore(mockedTx, safeAddress, dispatch)
+ removeTxFromStore(mockedTx, safeAddress, dispatch, state)
}
})
.on('error', (error) => {
closeSnackbar(pendingExecutionKey)
- removeTxFromStore(mockedTx, safeAddress, dispatch)
+ removeTxFromStore(mockedTx, safeAddress, dispatch, state)
console.error('Tx error: ', error)
})
.then(async (receipt) => {
@@ -188,9 +211,16 @@ const createTransaction = ({
: mockedTx.set('status', 'awaiting_confirmations')
await storeTx(
- toStoreTx.set('confirmations', fromJS([makeConfirmation({ owner: from })])),
+ toStoreTx.withMutations((record) => {
+ record
+ .set('confirmations', List([makeConfirmation({ owner: from })]))
+ .updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
+ previous.pop(from),
+ )
+ }),
safeAddress,
dispatch,
+ state,
)
dispatch(fetchTransactions(safeAddress))
diff --git a/src/routes/safe/store/actions/processTransaction.ts b/src/routes/safe/store/actions/processTransaction.ts
index fb7b7ab8..cf708ff8 100644
--- a/src/routes/safe/store/actions/processTransaction.ts
+++ b/src/routes/safe/store/actions/processTransaction.ts
@@ -117,18 +117,28 @@ const processTransaction = ({
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar)
try {
- await saveTxToHistory({ ...txArgs, txHash })
- await storeTx(mockedTx, safeAddress, dispatch)
+ await Promise.all([
+ saveTxToHistory({ ...txArgs, txHash }),
+ storeTx(
+ mockedTx.updateIn(
+ ['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
+ (previous) => previous.push(from),
+ ),
+ safeAddress,
+ dispatch,
+ state,
+ ),
+ ])
dispatch(fetchTransactions(safeAddress))
} catch (e) {
closeSnackbar(pendingExecutionKey)
- await storeTx(tx, safeAddress, dispatch)
+ await storeTx(tx, safeAddress, dispatch, state)
console.error(e)
}
})
.on('error', (error) => {
closeSnackbar(pendingExecutionKey)
- storeTx(mockedTx.set('isSuccessful', false), safeAddress, dispatch)
+ storeTx(tx, safeAddress, dispatch, state)
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
@@ -159,9 +169,16 @@ const processTransaction = ({
: mockedTx.set('status', 'awaiting_confirmations')
await storeTx(
- toStoreTx.set('confirmations', fromJS([...tx.confirmations, makeConfirmation({ owner: from })])),
+ toStoreTx.withMutations((record) => {
+ record
+ .set('confirmations', fromJS([...tx.confirmations, makeConfirmation({ owner: from })]))
+ .updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
+ previous.pop(from),
+ )
+ }),
safeAddress,
dispatch,
+ state,
)
dispatch(fetchTransactions(safeAddress))
diff --git a/src/routes/safe/store/models/transaction.ts b/src/routes/safe/store/models/transaction.ts
index da1fb3ad..0968937d 100644
--- a/src/routes/safe/store/models/transaction.ts
+++ b/src/routes/safe/store/models/transaction.ts
@@ -1,4 +1,4 @@
-import { List, Record } from 'immutable'
+import { List, Map, Record } from 'immutable'
import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
@@ -22,6 +22,8 @@ export type TransactionStatus =
| 'awaiting_execution'
| 'pending'
+export type PendingActionType = 'Confirm' | 'Reject'
+
export type TransactionProps = {
baseGas: number
blockNumber?: number | null
@@ -48,6 +50,7 @@ export type TransactionProps = {
nonce?: number | null
operation: number
origin: string | null
+ ownersWithPendingActions: Map>
recipient: string
refundParams: any
refundReceiver: string
@@ -86,6 +89,7 @@ export const makeTransaction = Record({
nonce: 0,
operation: 0,
origin: null,
+ ownersWithPendingActions: Map({ confirm: List([]), reject: List([]) }),
recipient: '',
refundParams: null,
refundReceiver: ZERO_ADDRESS,