feature: set tx's owners pending status

This commit is contained in:
fernandomg 2020-05-23 01:54:37 -03:00
parent 165d0ff0d6
commit 2c41105474
6 changed files with 207 additions and 90 deletions

View File

@ -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>
)

View File

@ -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}

View File

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

View File

@ -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))

View File

@ -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))

View File

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