feature: set tx's owners pending status
This commit is contained in:
parent
165d0ff0d6
commit
2c41105474
|
@ -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,57 +46,39 @@ 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 <Block className={classes.executor}>Pending</Block>
|
||||
}
|
||||
return (
|
||||
<Block className={classes.container}>
|
||||
<div className={cn(classes.verticalLine, (confirmed || thresholdReached || executor) && getTimelineLine())} />
|
||||
<div className={classes.circleState}>
|
||||
<Img alt="" src={imgCircle} />
|
||||
</div>
|
||||
<Identicon address={owner} className={classes.icon} diameter={32} />
|
||||
<Block>
|
||||
<Paragraph className={classes.name} noMargin>
|
||||
{nameInAdbk}
|
||||
</Paragraph>
|
||||
<EtherscanLink className={classes.address} cut={4} type="address" value={owner} />
|
||||
</Block>
|
||||
<Block className={classes.spacer} />
|
||||
{owner === userAddress && (
|
||||
<Block>
|
||||
{isCancelTx ? (
|
||||
<>
|
||||
{showRejectBtn && (
|
||||
<Button
|
||||
className={cn(classes.button, classes.lastButton)}
|
||||
color="secondary"
|
||||
onClick={onTxReject}
|
||||
testId={REJECT_TX_BTN_TEST_ID}
|
||||
variant="contained"
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
)}
|
||||
{showExecuteRejectBtn && (
|
||||
<Button
|
||||
className={cn(classes.button, classes.lastButton)}
|
||||
color="secondary"
|
||||
onClick={onTxReject}
|
||||
testId={EXECUTE_REJECT_TX_BTN_TEST_ID}
|
||||
variant="contained"
|
||||
>
|
||||
Execute
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{showConfirmBtn && (
|
||||
<Button
|
||||
|
@ -118,9 +103,65 @@ const OwnerComponent = ({
|
|||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const rejectButton = () => {
|
||||
if (pendingRejectAction) {
|
||||
return <Block className={classes.executor}>Pending</Block>
|
||||
}
|
||||
if (pendingAcceptAction) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{showRejectBtn && (
|
||||
<Button
|
||||
className={cn(classes.button, classes.lastButton)}
|
||||
color="secondary"
|
||||
onClick={onTxReject}
|
||||
testId={REJECT_TX_BTN_TEST_ID}
|
||||
variant="contained"
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
)}
|
||||
{showExecuteRejectBtn && (
|
||||
<Button
|
||||
className={cn(classes.button, classes.lastButton)}
|
||||
color="secondary"
|
||||
onClick={onTxReject}
|
||||
testId={EXECUTE_REJECT_TX_BTN_TEST_ID}
|
||||
variant="contained"
|
||||
>
|
||||
Execute
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Block className={classes.container}>
|
||||
<div
|
||||
className={cn(
|
||||
classes.verticalLine,
|
||||
(confirmed || thresholdReached || executor || pendingAcceptAction || pendingRejectAction) &&
|
||||
getTimelineLine(),
|
||||
)}
|
||||
/>
|
||||
<div className={classes.circleState}>
|
||||
<Img alt="" src={imgCircle} />
|
||||
</div>
|
||||
<Identicon address={owner} className={classes.icon} diameter={32} />
|
||||
<Block>
|
||||
<Paragraph className={classes.name} noMargin>
|
||||
{nameInAdbk}
|
||||
</Paragraph>
|
||||
<EtherscanLink className={classes.address} cut={4} type="address" value={owner} />
|
||||
</Block>
|
||||
)}
|
||||
<Block className={classes.spacer} />
|
||||
{owner === userAddress && <Block>{isCancelTx ? rejectButton() : confirmButton()}</Block>}
|
||||
{owner === executor && <Block className={classes.executor}>Executor</Block>}
|
||||
</Block>
|
||||
)
|
||||
|
|
|
@ -38,7 +38,7 @@ const OwnersList = ({
|
|||
userAddress={userAddress}
|
||||
/>
|
||||
))}
|
||||
{ownersUnconfirmed.map((owner) => (
|
||||
{ownersUnconfirmed.map(({ hasPendingAcceptActions, hasPendingRejectActions, owner }) => (
|
||||
<OwnerComponent
|
||||
classes={classes}
|
||||
executor={executor}
|
||||
|
@ -48,6 +48,8 @@ const OwnersList = ({
|
|||
onTxExecute={onTxExecute}
|
||||
onTxReject={onTxReject}
|
||||
owner={owner}
|
||||
pendingAcceptAction={hasPendingAcceptActions}
|
||||
pendingRejectAction={hasPendingRejectActions}
|
||||
showConfirmBtn={showConfirmBtn}
|
||||
showExecuteBtn={showExecuteBtn}
|
||||
showExecuteRejectBtn={showExecuteRejectBtn}
|
||||
|
|
|
@ -34,20 +34,43 @@ function getOwnersConfirmations(tx, userAddress) {
|
|||
}
|
||||
|
||||
function getPendingOwnersConfirmations(owners, tx, userAddress) {
|
||||
const ownersNotConfirmed = []
|
||||
const ownersWithNoConfirmations = []
|
||||
let currentUserNotConfirmed = true
|
||||
|
||||
owners.forEach((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 = ({
|
||||
|
|
|
@ -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) {
|
||||
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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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<PendingActionType, List<any>>
|
||||
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,
|
||||
|
|
Loading…
Reference in New Issue