(Fix) Executed transactions status (#1552)

Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
Fernando 2020-11-04 19:40:59 -03:00 committed by GitHub
parent c9fb7fcc10
commit 7a881537e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 283 additions and 223 deletions

View File

@ -35,7 +35,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null =>
return {
method: METHOD_TO_ID[methodId],
parameters: [
{ name: 'oldOwner', type: 'address', value: decodedParameters[1] },
{ name: 'owner', type: 'address', value: decodedParameters[1] },
{ name: '_threshold', type: 'uint', value: decodedParameters[2] },
],
}

View File

@ -1,6 +1,4 @@
import { push } from 'connected-react-router'
import { List, Map } from 'immutable'
import { batch } from 'react-redux'
import semverSatisfies from 'semver/functions/satisfies'
import { ThunkAction } from 'redux-thunk'
@ -24,10 +22,11 @@ import { providerSelector } from 'src/logic/wallets/store/selectors'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import { addOrUpdateCancellationTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import { removeCancellationTransaction } from 'src/logic/safe/store/actions/transactions/removeCancellationTransaction'
import { removeTransaction } from 'src/logic/safe/store/actions/transactions/removeTransaction'
import {
removeTxFromStore,
storeSignedTx,
storeExecutedTx,
} from 'src/logic/safe/store/actions/transactions/pendingTransactions'
import {
generateSafeTxHash,
mockTransaction,
@ -35,68 +34,13 @@ import {
} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import { makeConfirmation } from '../models/confirmation'
import fetchTransactions from './transactions/fetchTransactions'
import { safeTransactionsSelector } from 'src/logic/safe/store/selectors'
import { Transaction, TransactionStatus, TxArgs } from 'src/logic/safe/store/models/types/transaction'
import { TxArgs } from 'src/logic/safe/store/models/types/transaction'
import { AnyAction } from 'redux'
import { PayableTx } from 'src/types/contracts/types.d'
import { AppReduxState } from 'src/store'
import { Dispatch, DispatchReturn } from './types'
export const removeTxFromStore = (
tx: Transaction,
safeAddress: string,
dispatch: Dispatch,
state: AppReduxState,
): void => {
if (tx.isCancellationTx) {
const newTxStatus = TransactionStatus.AWAITING_YOUR_CONFIRMATION
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: Transaction,
safeAddress: string,
dispatch: Dispatch,
state: AppReduxState,
): Promise<void> => {
if (tx.isCancellationTx) {
let newTxStatus: TransactionStatus = TransactionStatus.AWAITING_YOUR_CONFIRMATION
if (tx.isExecuted) {
newTxStatus = TransactionStatus.CANCELLED
} else if (tx.status === TransactionStatus.PENDING) {
newTxStatus = tx.status
}
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 === TransactionStatus.CANCELLED)),
)
batch(() => {
dispatch(addOrUpdateCancellationTransactions({ safeAddress, transactions: Map({ [`${tx.nonce}`]: tx }) }))
dispatch(addOrUpdateTransactions({ safeAddress, transactions: txsToUpdate }))
})
} else {
dispatch(addOrUpdateTransactions({ safeAddress, transactions: List([tx]) }))
}
}
interface CreateTransactionArgs {
navigateToTransactionsTab?: boolean
notifiedTransaction: string
@ -228,15 +172,7 @@ const createTransaction = (
await Promise.all([
saveTxToHistory({ ...txArgs, txHash, origin }),
storeTx(
mockedTx.updateIn(
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
(previous) => previous.push(from),
),
safeAddress,
dispatch,
state,
),
storeSignedTx({ transaction: mockedTx, from, isExecution, safeAddress, dispatch, state }),
])
dispatch(fetchTransactions(safeAddress))
} catch (e) {
@ -263,29 +199,8 @@ const createTransaction = (
),
)
const toStoreTx = isExecution
? mockedTx.withMutations((record) => {
record
.set('executionTxHash', receipt.transactionHash)
.set('executor', from)
.set('isExecuted', true)
.set('isSuccessful', receipt.status)
.set('status', receipt.status ? TransactionStatus.SUCCESS : TransactionStatus.FAILED)
})
: mockedTx.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
await storeExecutedTx({ transaction: mockedTx, from, safeAddress, isExecution, receipt, dispatch, state })
await storeTx(
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))
return receipt.transactionHash

View File

@ -1,3 +1,5 @@
import { AnyAction } from 'redux'
import { ThunkAction } from 'redux-thunk'
import semverSatisfies from 'semver/functions/satisfies'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
@ -6,27 +8,41 @@ import { generateSignaturesFromTxConfirmations } from 'src/logic/safe/safeTxSign
import { getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions'
import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES, tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner'
import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { providerSelector } from 'src/logic/wallets/store/selectors'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import closeSnackbarAction from 'src/logic/notifications/store/actions/closeSnackbar'
import fetchSafe from 'src/logic/safe/store/actions/fetchSafe'
import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions'
import {
isCancelTransaction,
mockTransaction,
TxToMock,
} from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { mockTransaction, TxToMock } from 'src/logic/safe/store/actions/transactions/utils/transactionHelpers'
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/logic/safe/store/actions/utils'
import { AppReduxState } from 'src/store'
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
import { storeTx } from './createTransaction'
import { TransactionStatus } from 'src/logic/safe/store/models/types/transaction'
import { makeConfirmation } from 'src/logic/safe/store/models/confirmation'
import { storeExecutedTx, storeSignedTx, storeTx } from 'src/logic/safe/store/actions/transactions/pendingTransactions'
import { Transaction } from 'src/logic/safe/store/models/types/transaction'
const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddress, tx, userAddress }) => async (
dispatch,
getState,
) => {
import { Dispatch, DispatchReturn } from './types'
interface ProcessTransactionArgs {
approveAndExecute: boolean
notifiedTransaction: string
safeAddress: string
tx: Transaction
userAddress: string
}
type ProcessTransactionAction = ThunkAction<Promise<void | string>, AppReduxState, DispatchReturn, AnyAction>
const processTransaction = ({
approveAndExecute,
notifiedTransaction,
safeAddress,
tx,
userAddress,
}: ProcessTransactionArgs): ProcessTransactionAction => async (
dispatch: Dispatch,
getState: () => AppReduxState,
): Promise<DispatchReturn> => {
const state = getState()
const { account: from, hardwareWallet, smartContractWallet } = providerSelector(state)
@ -57,7 +73,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
safeInstance,
to: tx.recipient,
valueInWei: tx.value,
data: tx.data,
data: tx.data ?? EMPTY_DATA,
operation: tx.operation,
nonce: tx.nonce,
safeTxGas: tx.safeTxGas,
@ -121,30 +137,18 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
try {
await Promise.all([
saveTxToHistory({ ...txArgs, txHash }),
storeTx(
mockedTx.withMutations((record) => {
record
.updateIn(
['ownersWithPendingActions', mockedTx.isCancellationTx ? 'reject' : 'confirm'],
(previous) => previous.push(from),
)
.set('status', TransactionStatus.PENDING)
}),
safeAddress,
dispatch,
state,
),
storeSignedTx({ transaction: mockedTx, from, isExecution, safeAddress, dispatch, state }),
])
dispatch(fetchTransactions(safeAddress))
} catch (e) {
dispatch(closeSnackbarAction(pendingExecutionKey))
await storeTx(tx, safeAddress, dispatch, state)
await storeTx({ transaction: tx, safeAddress, dispatch, state })
console.error(e)
}
})
.on('error', (error) => {
dispatch(closeSnackbarAction(pendingExecutionKey))
storeTx(tx, safeAddress, dispatch, state)
storeTx({ transaction: tx, safeAddress, dispatch, state })
console.error('Processing transaction error: ', error)
})
.then(async (receipt) => {
@ -160,43 +164,7 @@ const processTransaction = ({ approveAndExecute, notifiedTransaction, safeAddres
),
)
const toStoreTx = isExecution
? mockedTx.withMutations((record) => {
record
.set('executionTxHash', receipt.transactionHash)
.set('blockNumber', receipt.blockNumber)
.set('executionDate', record.submissionDate)
.set('executor', from)
.set('isExecuted', true)
.set('isSuccessful', receipt.status)
.set(
'status',
receipt.status
? isCancelTransaction(record, safeAddress)
? TransactionStatus.CANCELLED
: TransactionStatus.SUCCESS
: TransactionStatus.FAILED,
)
.updateIn(['ownersWithPendingActions', 'reject'], (prev) => prev.clear())
.updateIn(['ownersWithPendingActions', 'confirm'], (prev) => prev.clear())
})
: mockedTx.withMutations((record) => {
record
.updateIn(['ownersWithPendingActions', toStoreTx.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
previous.pop(),
)
.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
})
await storeTx(
toStoreTx.update('confirmations', (confirmations) => {
const index = confirmations.findIndex(({ owner }) => owner === from)
return index === -1 ? confirmations.push(makeConfirmation({ owner: from })) : confirmations
}),
safeAddress,
dispatch,
state,
)
await storeExecutedTx({ transaction: mockedTx, from, safeAddress, isExecution, receipt, dispatch, state })
dispatch(fetchTransactions(safeAddress))

View File

@ -0,0 +1,169 @@
import { List, Map } from 'immutable'
import { batch } from 'react-redux'
import { TransactionReceipt } from 'web3-core'
import { addOrUpdateCancellationTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import { removeCancellationTransaction } from 'src/logic/safe/store/actions/transactions/removeCancellationTransaction'
import { removeTransaction } from 'src/logic/safe/store/actions/transactions/removeTransaction'
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
import { makeConfirmation } from 'src/logic/safe/store/models/confirmation'
import { Transaction, TransactionStatus } from 'src/logic/safe/store/models/types/transaction'
import { safeTransactionsSelector } from 'src/logic/safe/store/selectors'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { web3ReadOnly } from 'src/logic/wallets/getWeb3'
import { AppReduxState } from 'src/store'
type SetPendingTransactionParams = {
transaction: Transaction
from: string
}
const setTxStatusAsPending = ({ transaction, from }: SetPendingTransactionParams): Transaction =>
transaction.withMutations((transaction) => {
transaction
// setting user as the one who has triggered the tx
// this allows to display the owner's "pending" status
.updateIn(['ownersWithPendingActions', transaction.isCancellationTx ? 'reject' : 'confirm'], (previous) =>
previous.push(from),
)
// global transaction status
.set('status', TransactionStatus.PENDING)
})
type SetOptimisticTransactionParams = {
transaction: Transaction
from: string
isExecution: boolean
receipt: TransactionReceipt
}
const updateTxBasedOnReceipt = ({
transaction,
from,
isExecution,
receipt,
}: SetOptimisticTransactionParams): Transaction => {
const txToStore = isExecution
? transaction.withMutations((tx) => {
tx.set('executionTxHash', receipt.transactionHash)
.set('blockNumber', receipt.blockNumber)
.set('executionDate', tx.submissionDate)
.set('fee', web3ReadOnly.utils.toWei(`${receipt.gasUsed}`, 'gwei'))
.set('executor', from)
.set('isExecuted', true)
.set('isSuccessful', receipt.status)
.set('status', receipt.status ? TransactionStatus.SUCCESS : TransactionStatus.FAILED)
})
: transaction.set('status', TransactionStatus.AWAITING_CONFIRMATIONS)
return txToStore.withMutations((tx) => {
const senderHasAlreadyConfirmed = tx.confirmations.findIndex(({ owner }) => sameAddress(owner, from)) !== -1
if (!senderHasAlreadyConfirmed) {
// updates confirmations status
tx.update('confirmations', (confirmations) => confirmations.push(makeConfirmation({ owner: from })))
}
tx.updateIn(['ownersWithPendingActions', 'reject'], (prev) => prev.clear()).updateIn(
['ownersWithPendingActions', 'confirm'],
(prev) => prev.clear(),
)
})
}
type StoreTxParams = {
transaction: Transaction
safeAddress: string
dispatch: Dispatch
state: AppReduxState
}
export const storeTx = async ({ transaction, safeAddress, dispatch, state }: StoreTxParams): Promise<void> => {
if (transaction.isCancellationTx) {
// `transaction` is the Cancellation tx
// So we need to decide the `status` for the main transaction this `transaction` is cancelling
let status: TransactionStatus = TransactionStatus.AWAITING_YOUR_CONFIRMATION
// `cancelled`, will become true if its corresponding Cancellation tx was successfully executed
let cancelled = false
switch (transaction.status) {
case TransactionStatus.SUCCESS:
status = TransactionStatus.CANCELLED
cancelled = true
break
case TransactionStatus.PENDING:
status = TransactionStatus.PENDING
break
default:
break
}
const safeTransactions = safeTransactionsSelector(state)
const transactions = safeTransactions.withMutations((txs) => {
const txIndex = txs.findIndex(({ nonce }) => Number(nonce) === Number(transaction.nonce))
txs.update(txIndex, (tx) => tx.set('status', status).set('cancelled', cancelled))
})
batch(() => {
dispatch(
addOrUpdateCancellationTransactions({
safeAddress,
transactions: Map({ [`${transaction.nonce}`]: transaction }),
}),
)
dispatch(addOrUpdateTransactions({ safeAddress, transactions }))
})
} else {
dispatch(addOrUpdateTransactions({ safeAddress, transactions: List([transaction]) }))
}
}
type StoreSignedTxParams = StoreTxParams & {
from: string
isExecution: boolean
}
export const storeSignedTx = ({ transaction, from, isExecution, ...rest }: StoreSignedTxParams): Promise<void> =>
storeTx({
transaction: isExecution ? setTxStatusAsPending({ transaction, from }) : transaction,
...rest,
})
type StoreExecParams = StoreTxParams & {
from: string
isExecution: boolean
safeAddress: string
receipt: TransactionReceipt
}
export const storeExecutedTx = ({ safeAddress, dispatch, state, ...rest }: StoreExecParams): Promise<void> =>
storeTx({
transaction: updateTxBasedOnReceipt({ ...rest }),
safeAddress,
dispatch,
state,
})
export const removeTxFromStore = (
transaction: Transaction,
safeAddress: string,
dispatch: Dispatch,
state: AppReduxState,
): void => {
if (transaction.isCancellationTx) {
const safeTransactions = safeTransactionsSelector(state)
const transactions = safeTransactions.withMutations((txs) => {
const txIndex = txs.findIndex(({ nonce }) => Number(nonce) === Number(transaction.nonce))
txs[txIndex].set('status', TransactionStatus.AWAITING_YOUR_CONFIRMATION)
})
batch(() => {
dispatch(addOrUpdateTransactions({ safeAddress, transactions }))
dispatch(removeCancellationTransaction({ safeAddress, transaction }))
})
} else {
dispatch(removeTransaction({ safeAddress, transaction }))
}
}

View File

@ -21,11 +21,12 @@ import {
TxArgs,
RefundParams,
} from 'src/logic/safe/store/models/types/transaction'
import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/cancellationTransactions'
import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
import { TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/transactions'
import { AppReduxState, store } from 'src/store'
import { safeSelector, safeTransactionsSelector } from 'src/logic/safe/store/selectors'
import {
safeSelector,
safeTransactionsSelector,
safeCancellationTransactionsSelector,
} from 'src/logic/safe/store/selectors'
import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions'
import {
BatchProcessTxsProps,
@ -323,9 +324,13 @@ export type TxToMock = TxArgs & {
export const mockTransaction = (tx: TxToMock, safeAddress: string, state: AppReduxState): Promise<Transaction> => {
const knownTokens: Map<string, Token> = state[TOKEN_REDUCER_ID]
const safe: SafeRecord = state[SAFE_REDUCER_ID].getIn(['safes', safeAddress])
const cancellationTxs = state[CANCELLATION_TRANSACTIONS_REDUCER_ID].get(safeAddress) || Map()
const outgoingTxs = state[TRANSACTIONS_REDUCER_ID].get(safeAddress) || List()
const safe = safeSelector(state)
const cancellationTxs = safeCancellationTransactionsSelector(state)
const outgoingTxs = safeTransactionsSelector(state)
if (!safe) {
throw new Error('Failed to recover Safe from the store')
}
return buildTx({
cancellationTxs,

View File

@ -59,7 +59,7 @@ export type TransactionProps = {
isCollectibleTransfer: boolean
isExecuted: boolean
isPending?: boolean
isSuccessful: boolean
isSuccessful?: boolean
isTokenTransfer: boolean
masterCopy: string
modifySettingsTx: boolean

View File

@ -1,11 +1,10 @@
import CircularProgress from '@material-ui/core/CircularProgress'
import { withStyles } from '@material-ui/core/styles'
import * as React from 'react'
import React, { ReactElement } from 'react'
import AwaitingIcon from './assets/awaiting.svg'
import ErrorIcon from './assets/error.svg'
import OkIcon from './assets/ok.svg'
import { styles } from './style'
import { useStyles } from './style'
import Block from 'src/components/layout/Block'
import Img from 'src/components/layout/Img'
@ -19,7 +18,7 @@ const statusToIcon = {
awaiting_confirmations: AwaitingIcon,
awaiting_execution: AwaitingIcon,
pending: <CircularProgress size={14} />,
}
} as const
const statusToLabel = {
success: 'Success',
@ -29,15 +28,16 @@ const statusToLabel = {
awaiting_confirmations: 'Awaiting confirmations',
awaiting_execution: 'Awaiting execution',
pending: 'Pending',
}
} as const
const statusIconStyle = {
height: '14px',
width: '14px',
}
const Status = ({ classes, status }) => {
const Icon = statusToIcon[status]
const Status = ({ status }: { status: keyof typeof statusToLabel }): ReactElement => {
const classes = useStyles()
const Icon: typeof statusToIcon[keyof typeof statusToIcon] = statusToIcon[status]
return (
<Block className={`${classes.container} ${classes[status]}`}>
@ -49,4 +49,4 @@ const Status = ({ classes, status }) => {
)
}
export default withStyles(styles as any)(Status)
export default Status

View File

@ -1,49 +1,52 @@
import { createStyles, makeStyles } from '@material-ui/core/styles'
import { boldFont, disabled, error, extraSmallFontSize, lg, secondary, sm } from 'src/theme/variables'
export const styles = () => ({
container: {
display: 'flex',
fontSize: extraSmallFontSize,
fontWeight: boldFont,
padding: sm,
alignItems: 'center',
boxSizing: 'border-box',
height: lg,
marginTop: sm,
marginBottom: sm,
borderRadius: '3px',
},
success: {
backgroundColor: '#A1D2CA',
color: secondary,
},
cancelled: {
backgroundColor: 'transparent',
color: error,
border: `1px solid ${error}`,
},
failed: {
backgroundColor: 'transparent',
color: error,
border: `1px solid ${error}`,
},
awaiting_your_confirmation: {
backgroundColor: '#d4d5d3',
color: disabled,
},
awaiting_confirmations: {
backgroundColor: '#d4d5d3',
color: disabled,
},
awaiting_execution: {
backgroundColor: '#d4d5d3',
color: disabled,
},
pending: {
backgroundColor: '#fff3e2',
color: '#e8673c',
},
statusText: {
padding: '0 7px',
},
})
export const useStyles = makeStyles(
createStyles({
container: {
display: 'flex',
fontSize: extraSmallFontSize,
fontWeight: boldFont,
padding: sm,
alignItems: 'center',
boxSizing: 'border-box',
height: lg,
marginTop: sm,
marginBottom: sm,
borderRadius: '3px',
},
success: {
backgroundColor: '#A1D2CA',
color: secondary,
},
cancelled: {
backgroundColor: 'transparent',
color: error,
border: `1px solid ${error}`,
},
failed: {
backgroundColor: 'transparent',
color: error,
border: `1px solid ${error}`,
},
awaiting_your_confirmation: {
backgroundColor: '#d4d5d3',
color: disabled,
},
awaiting_confirmations: {
backgroundColor: '#d4d5d3',
color: disabled,
},
awaiting_execution: {
backgroundColor: '#d4d5d3',
color: disabled,
},
pending: {
backgroundColor: '#fff3e2',
color: '#e8673c',
},
statusText: {
padding: '0 7px',
},
}),
)