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 ConfirmSmallGreenCircle from './assets/confirm-small-green.svg'
import ConfirmSmallGreyCircle from './assets/confirm-small-grey.svg' import ConfirmSmallGreyCircle from './assets/confirm-small-grey.svg'
import ConfirmSmallRedCircle from './assets/confirm-small-red.svg' import ConfirmSmallRedCircle from './assets/confirm-small-red.svg'
import PendingSmallYellowCircle from './assets/confirm-small-yellow.svg'
import { styles } from './style' import { styles } from './style'
import EtherscanLink from 'src/components/EtherscanLink' import EtherscanLink from 'src/components/EtherscanLink'
@ -32,6 +33,8 @@ const OwnerComponent = ({
onTxExecute, onTxExecute,
onTxReject, onTxReject,
owner, owner,
pendingAcceptAction,
pendingRejectAction,
showConfirmBtn, showConfirmBtn,
showExecuteBtn, showExecuteBtn,
showExecuteRejectBtn, showExecuteRejectBtn,
@ -43,18 +46,110 @@ const OwnerComponent = ({
const [imgCircle, setImgCircle] = React.useState(ConfirmSmallGreyCircle) const [imgCircle, setImgCircle] = React.useState(ConfirmSmallGreyCircle)
React.useMemo(() => { React.useMemo(() => {
if (pendingAcceptAction || pendingRejectAction) {
setImgCircle(PendingSmallYellowCircle)
return
}
if (confirmed) { if (confirmed) {
setImgCircle(isCancelTx ? CancelSmallFilledCircle : ConfirmSmallFilledCircle) setImgCircle(isCancelTx ? CancelSmallFilledCircle : ConfirmSmallFilledCircle)
} else if (thresholdReached || executor) { return
setImgCircle(isCancelTx ? ConfirmSmallRedCircle : ConfirmSmallGreenCircle)
} }
}, [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 (
<>
{showConfirmBtn && (
<Button
className={classes.button}
color="primary"
onClick={onTxConfirm}
testId={CONFIRM_TX_BTN_TEST_ID}
variant="contained"
>
Confirm
</Button>
)}
{showExecuteBtn && (
<Button
className={classes.button}
color="primary"
onClick={onTxExecute}
testId={EXECUTE_TX_BTN_TEST_ID}
variant="contained"
>
Execute
</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 ( return (
<Block className={classes.container}> <Block className={classes.container}>
<div className={cn(classes.verticalLine, (confirmed || thresholdReached || executor) && getTimelineLine())} /> <div
className={cn(
classes.verticalLine,
(confirmed || thresholdReached || executor || pendingAcceptAction || pendingRejectAction) &&
getTimelineLine(),
)}
/>
<div className={classes.circleState}> <div className={classes.circleState}>
<Img alt="" src={imgCircle} /> <Img alt="" src={imgCircle} />
</div> </div>
@ -66,61 +161,7 @@ const OwnerComponent = ({
<EtherscanLink className={classes.address} cut={4} type="address" value={owner} /> <EtherscanLink className={classes.address} cut={4} type="address" value={owner} />
</Block> </Block>
<Block className={classes.spacer} /> <Block className={classes.spacer} />
{owner === userAddress && ( {owner === userAddress && <Block>{isCancelTx ? rejectButton() : confirmButton()}</Block>}
<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
className={classes.button}
color="primary"
onClick={onTxConfirm}
testId={CONFIRM_TX_BTN_TEST_ID}
variant="contained"
>
Confirm
</Button>
)}
{showExecuteBtn && (
<Button
className={classes.button}
color="primary"
onClick={onTxExecute}
testId={EXECUTE_TX_BTN_TEST_ID}
variant="contained"
>
Execute
</Button>
)}
</>
)}
</Block>
)}
{owner === executor && <Block className={classes.executor}>Executor</Block>} {owner === executor && <Block className={classes.executor}>Executor</Block>}
</Block> </Block>
) )

View File

@ -38,7 +38,7 @@ const OwnersList = ({
userAddress={userAddress} userAddress={userAddress}
/> />
))} ))}
{ownersUnconfirmed.map((owner) => ( {ownersUnconfirmed.map(({ hasPendingAcceptActions, hasPendingRejectActions, owner }) => (
<OwnerComponent <OwnerComponent
classes={classes} classes={classes}
executor={executor} executor={executor}
@ -48,6 +48,8 @@ const OwnersList = ({
onTxExecute={onTxExecute} onTxExecute={onTxExecute}
onTxReject={onTxReject} onTxReject={onTxReject}
owner={owner} owner={owner}
pendingAcceptAction={hasPendingAcceptActions}
pendingRejectAction={hasPendingRejectActions}
showConfirmBtn={showConfirmBtn} showConfirmBtn={showConfirmBtn}
showExecuteBtn={showExecuteBtn} showExecuteBtn={showExecuteBtn}
showExecuteRejectBtn={showExecuteRejectBtn} showExecuteRejectBtn={showExecuteRejectBtn}

View File

@ -34,20 +34,43 @@ function getOwnersConfirmations(tx, userAddress) {
} }
function getPendingOwnersConfirmations(owners, tx, userAddress) { function getPendingOwnersConfirmations(owners, tx, userAddress) {
const ownersNotConfirmed = [] const ownersWithNoConfirmations = []
let currentUserNotConfirmed = true let currentUserNotConfirmed = true
owners.forEach((owner) => { owners.forEach((owner) => {
const confirmationsEntry = tx.confirmations.find((conf) => conf.owner === owner.address) const confirmationsEntry = tx.confirmations.find((conf) => conf.owner === owner.address)
if (!confirmationsEntry) { if (!confirmationsEntry) {
ownersNotConfirmed.push(owner.address) ownersWithNoConfirmations.push(owner.address)
} }
if (confirmationsEntry && confirmationsEntry.owner === userAddress) { if (confirmationsEntry && confirmationsEntry.owner === userAddress) {
currentUserNotConfirmed = false 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 = ({ const OwnersColumn = ({

View File

@ -1,5 +1,6 @@
import { push } from 'connected-react-router' 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 semverSatisfies from 'semver/functions/satisfies'
import { onboardUser } from 'src/components/ConnectButton' 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 { getErrorMessage } from 'src/test/utils/ethereumErrors'
import { makeConfirmation } from '../models/confirmation' import { makeConfirmation } from '../models/confirmation'
import fetchTransactions from './transactions/fetchTransactions' 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) { 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 { } else {
dispatch(removeTransaction({ safeAddress, transaction: tx })) dispatch(removeTransaction({ safeAddress, transaction: tx }))
} }
} }
export const storeTx = async (tx, safeAddress, dispatch) => { export const storeTx = async (tx, safeAddress, dispatch, state) => {
if (tx.isCancellationTx) { if (tx.isCancellationTx) {
dispatch( const newTxStatus = tx.isExecuted ? 'cancelled' : tx.status === 'pending' ? 'pending' : 'awaiting_confirmations'
addOrUpdateCancellationTransactions({ const transactions = safeTransactionsSelector(state)
safeAddress, const txsToUpdate = transactions
transactions: Map({ [`${tx.nonce}`]: tx }), .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 { } else {
dispatch( dispatch(addOrUpdateTransactions({ safeAddress, transactions: List([tx]) }))
addOrUpdateTransactions({
safeAddress,
transactions: List([tx]),
}),
)
} }
} }
@ -151,15 +163,26 @@ const createTransaction = ({
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar) 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)) dispatch(fetchTransactions(safeAddress))
} catch (e) { } catch (e) {
removeTxFromStore(mockedTx, safeAddress, dispatch) removeTxFromStore(mockedTx, safeAddress, dispatch, state)
} }
}) })
.on('error', (error) => { .on('error', (error) => {
closeSnackbar(pendingExecutionKey) closeSnackbar(pendingExecutionKey)
removeTxFromStore(mockedTx, safeAddress, dispatch) removeTxFromStore(mockedTx, safeAddress, dispatch, state)
console.error('Tx error: ', error) console.error('Tx error: ', error)
}) })
.then(async (receipt) => { .then(async (receipt) => {
@ -188,9 +211,16 @@ const createTransaction = ({
: mockedTx.set('status', 'awaiting_confirmations') : mockedTx.set('status', 'awaiting_confirmations')
await storeTx( 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, safeAddress,
dispatch, dispatch,
state,
) )
dispatch(fetchTransactions(safeAddress)) dispatch(fetchTransactions(safeAddress))

View File

@ -117,18 +117,28 @@ const processTransaction = ({
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar) pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar)
try { try {
await saveTxToHistory({ ...txArgs, txHash }) await Promise.all([
await storeTx(mockedTx, safeAddress, dispatch) saveTxToHistory({ ...txArgs, txHash }),
storeTx(
mockedTx.updateIn(
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
(previous) => previous.push(from),
),
safeAddress,
dispatch,
state,
),
])
dispatch(fetchTransactions(safeAddress)) dispatch(fetchTransactions(safeAddress))
} catch (e) { } catch (e) {
closeSnackbar(pendingExecutionKey) closeSnackbar(pendingExecutionKey)
await storeTx(tx, safeAddress, dispatch) await storeTx(tx, safeAddress, dispatch, state)
console.error(e) console.error(e)
} }
}) })
.on('error', (error) => { .on('error', (error) => {
closeSnackbar(pendingExecutionKey) closeSnackbar(pendingExecutionKey)
storeTx(mockedTx.set('isSuccessful', false), safeAddress, dispatch) storeTx(tx, safeAddress, dispatch, state)
console.error('Processing transaction error: ', error) console.error('Processing transaction error: ', error)
}) })
.then(async (receipt) => { .then(async (receipt) => {
@ -159,9 +169,16 @@ const processTransaction = ({
: mockedTx.set('status', 'awaiting_confirmations') : mockedTx.set('status', 'awaiting_confirmations')
await storeTx( 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, safeAddress,
dispatch, dispatch,
state,
) )
dispatch(fetchTransactions(safeAddress)) 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' import { ZERO_ADDRESS } from 'src/logic/wallets/ethAddresses'
@ -22,6 +22,8 @@ export type TransactionStatus =
| 'awaiting_execution' | 'awaiting_execution'
| 'pending' | 'pending'
export type PendingActionType = 'Confirm' | 'Reject'
export type TransactionProps = { export type TransactionProps = {
baseGas: number baseGas: number
blockNumber?: number | null blockNumber?: number | null
@ -48,6 +50,7 @@ export type TransactionProps = {
nonce?: number | null nonce?: number | null
operation: number operation: number
origin: string | null origin: string | null
ownersWithPendingActions: Map<PendingActionType, List<any>>
recipient: string recipient: string
refundParams: any refundParams: any
refundReceiver: string refundReceiver: string
@ -86,6 +89,7 @@ export const makeTransaction = Record({
nonce: 0, nonce: 0,
operation: 0, operation: 0,
origin: null, origin: null,
ownersWithPendingActions: Map({ confirm: List([]), reject: List([]) }),
recipient: '', recipient: '',
refundParams: null, refundParams: null,
refundReceiver: ZERO_ADDRESS, refundReceiver: ZERO_ADDRESS,