refactor: (cancellation)transactions reducers and actions

This commit is contained in:
fernandomg 2020-05-22 16:49:48 -03:00
parent d0dbd8a28c
commit 89261d0ed3
11 changed files with 125 additions and 278 deletions

View File

@ -1,5 +0,0 @@
import { createAction } from 'redux-actions'
export const ADD_CANCELLATION_TRANSACTION = 'ADD_CANCELLATION_TRANSACTION'
export const addCancellationTransaction = createAction(ADD_CANCELLATION_TRANSACTION)

View File

@ -0,0 +1,5 @@
import { createAction } from 'redux-actions'
export const ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS = 'ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS'
export const addOrUpdateCancellationTransactions = createAction(ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS)

View File

@ -0,0 +1,5 @@
import { createAction } from 'redux-actions'
export const ADD_OR_UPDATE_TRANSACTIONS = 'ADD_OR_UPDATE_TRANSACTIONS'
export const addOrUpdateTransactions = createAction(ADD_OR_UPDATE_TRANSACTIONS)

View File

@ -1,5 +0,0 @@
import { createAction } from 'redux-actions'
export const ADD_TRANSACTION = 'ADD_TRANSACTION'
export const addTransaction = createAction(ADD_TRANSACTION)

View File

@ -1,252 +0,0 @@
import { push } from 'connected-react-router'
import { List } from 'immutable'
// import semverSatisfies from 'semver/functions/satisfies'
import { makeConfirmation } from '../../models/confirmation'
import fetchTransactions from './fetchTransactions'
import updateTransaction from './updateTransaction'
import { onboardUser } from 'src/components/ConnectButton'
import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { getNotificationsFromTxType, showSnackbar } from 'src/logic/notifications'
import { CALL, getApprovalTransaction, getExecutionTransaction, saveTxToHistory } from 'src/logic/safe/transactions'
import { estimateSafeTxGas } from 'src/logic/safe/transactions/gasNew'
// import { SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES, tryOffchainSigning } from 'src/logic/safe/transactions/offchainSigner'
// import { getCurrentSafeVersion } from 'src/logic/safe/utils/safeVersion'
import { TOKEN_REDUCER_ID } from 'src/logic/tokens/store/reducer/tokens'
import { ZERO_ADDRESS, sameAddress } from 'src/logic/wallets/ethAddresses'
import { EMPTY_DATA } from 'src/logic/wallets/ethTransactions'
import { providerSelector } from 'src/logic/wallets/store/selectors'
import { SAFELIST_ADDRESS } from 'src/routes/routes'
import { addCancellationTransaction } from 'src/routes/safe/store/actions/transactions/addCancellationTransaction'
import { addTransaction } from 'src/routes/safe/store/actions/transactions/addTransaction'
import { buildTransactionFrom } from 'src/routes/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
import { updateCancellationTransaction } from 'src/routes/safe/store/actions/transactions/updateCancellationTransaction'
import { getLastTx, getNewTxNonce, shouldExecuteTransaction } from 'src/routes/safe/store/actions/utils'
import { CANCELLATION_TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/cancellationTransactions'
import { TRANSACTIONS_REDUCER_ID } from 'src/routes/safe/store/reducer/transactions'
import { getErrorMessage } from 'src/test/utils/ethereumErrors'
async function mockMyTransaction(safeAddress: string, state, tx: any) {
const submissionDate = new Date().toISOString()
const knownTokens = state[TOKEN_REDUCER_ID]
const cancellationTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !tx.data
const existentTx =
state[cancellationTx ? CANCELLATION_TRANSACTIONS_REDUCER_ID : TRANSACTIONS_REDUCER_ID]
.get(safeAddress)
.find(({ nonce }) => nonce === tx.nonce) || null
const transactionStructure = {
...tx,
value: tx.valueInWei,
blockNumber: null,
confirmations: [], // this is used to determine if a tx is pending or not. See `getTxStatus` selector
confirmationsRequired: null,
dataDecoded: {},
ethGasPrice: null,
executionDate: null,
executor: null,
fee: null,
gasUsed: null,
isExecuted: false,
isSuccessful: null,
origin: null,
safeTxHash: null,
signatures: null,
transactionHash: null,
...existentTx,
modified: submissionDate,
submissionDate,
safe: safeAddress,
}
const mockedTransaction = await buildTransactionFrom(safeAddress, transactionStructure, knownTokens, null)
return { cancellationTx, existentTx, mockedTransaction }
}
const createTransaction = ({
safeAddress,
to,
valueInWei,
txData = EMPTY_DATA,
notifiedTransaction,
enqueueSnackbar,
closeSnackbar,
txNonce,
operation = CALL,
navigateToTransactionsTab = true,
origin = null,
}) => async (dispatch, getState) => {
const state = getState()
if (navigateToTransactionsTab) {
dispatch(push(`${SAFELIST_ADDRESS}/${safeAddress}/transactions`))
}
const ready = await onboardUser()
if (!ready) return
const { account: from /*, hardwareWallet, smartContractWallet*/ } = providerSelector(state)
const safeInstance = await getGnosisSafeInstanceAt(safeAddress)
const lastTx = await getLastTx(safeAddress)
const nonce = await getNewTxNonce(txNonce, lastTx, safeInstance)
const isExecution = await shouldExecuteTransaction(safeInstance, nonce, lastTx)
// const safeVersion = await getCurrentSafeVersion(safeInstance)
const safeTxGas = await estimateSafeTxGas(safeInstance, safeAddress, txData, to, valueInWei, operation)
// https://docs.gnosis.io/safe/docs/docs5/#pre-validated-signatures
const sigs = `0x000000000000000000000000${from.replace(
'0x',
'',
)}000000000000000000000000000000000000000000000000000000000000000001`
const notificationsQueue = getNotificationsFromTxType(notifiedTransaction, origin)
const beforeExecutionKey = showSnackbar(notificationsQueue.beforeExecution, enqueueSnackbar, closeSnackbar)
let pendingExecutionKey
let txHash
let tx
const txArgs = {
safeInstance,
to,
valueInWei,
data: txData,
operation,
nonce,
safeTxGas,
baseGas: 0,
gasPrice: 0,
gasToken: ZERO_ADDRESS,
refundReceiver: ZERO_ADDRESS,
sender: from,
sigs,
}
try {
// Here we're checking that safe contract version is greater or equal 1.1.1, but
// theoretically EIP712 should also work for 1.0.0 contracts
// TODO: revert this
// const canTryOffchainSigning =
// !isExecution && !smartContractWallet && semverSatisfies(safeVersion, SAFE_VERSION_FOR_OFFCHAIN_SIGNATURES)
// if (canTryOffchainSigning) {
// const signature = await tryOffchainSigning({ ...txArgs, safeAddress }, hardwareWallet)
//
// if (signature) {
// closeSnackbar(beforeExecutionKey)
//
// await saveTxToHistory({
// ...txArgs,
// signature,
// origin,
// })
// showSnackbar(notificationsQueue.afterExecution.moreConfirmationsNeeded, enqueueSnackbar, closeSnackbar)
//
// dispatch(fetchTransactions(safeAddress))
// return
// }
// }
tx = isExecution ? await getExecutionTransaction(txArgs) : await getApprovalTransaction(txArgs)
const sendParams: any = { from, value: 0 }
// if not set owner management tests will fail on ganache
if (process.env.NODE_ENV === 'test') {
sendParams.gas = '7000000'
}
await tx
.send(sendParams)
.once('transactionHash', async (hash) => {
txHash = hash
closeSnackbar(beforeExecutionKey)
pendingExecutionKey = showSnackbar(notificationsQueue.pendingExecution, enqueueSnackbar, closeSnackbar)
try {
// TODO: let's mock the tx
const { cancellationTx, existentTx, mockedTransaction } = await mockMyTransaction(safeAddress, state, {
...txArgs,
txHash,
})
if (cancellationTx) {
if (existentTx) {
dispatch(updateCancellationTransaction({ safeAddress, transaction: mockedTransaction }))
} else {
dispatch(addCancellationTransaction({ safeAddress, transaction: mockedTransaction }))
}
} else {
if (existentTx) {
dispatch(updateTransaction({ safeAddress, transaction: mockedTransaction }))
} else {
dispatch(addTransaction({ safeAddress, transaction: mockedTransaction }))
}
}
await saveTxToHistory({ ...txArgs, txHash, origin })
await dispatch(fetchTransactions(safeAddress))
} catch (err) {
console.error(err)
}
})
.on('error', (error) => {
console.error('Tx error: ', error)
})
.then((receipt) => {
closeSnackbar(pendingExecutionKey)
const safeTxHash = isExecution
? receipt.events.ExecutionSuccess.returnValues[0]
: receipt.events.ApproveHash.returnValues[0]
dispatch(
updateTransaction({
safeAddress,
transaction: {
safeTxHash,
isExecuted: isExecution,
isSuccessful: isExecution ? true : null,
executionTxHash: isExecution ? receipt.transactionHash : null,
executor: isExecution ? from : null,
confirmations: List([
makeConfirmation({
type: 'confirmation',
hash: receipt.transactionHash,
signature: sigs,
owner: from,
}),
]),
},
}),
)
showSnackbar(
isExecution
? notificationsQueue.afterExecution.noMoreConfirmationsNeeded
: notificationsQueue.afterExecution.moreConfirmationsNeeded,
enqueueSnackbar,
closeSnackbar,
)
dispatch(fetchTransactions(safeAddress))
return receipt.transactionHash
})
} catch (err) {
console.error(err)
closeSnackbar(beforeExecutionKey)
closeSnackbar(pendingExecutionKey)
showSnackbar(notificationsQueue.afterExecutionError, enqueueSnackbar, closeSnackbar)
const executeDataUsedSignatures = safeInstance.contract.methods
.execTransaction(to, valueInWei, txData, operation, 0, 0, 0, ZERO_ADDRESS, ZERO_ADDRESS, sigs)
.encodeABI()
const errMsg = await getErrorMessage(safeInstance.address, 0, executeDataUsedSignatures, from)
console.error(`Error creating the TX: ${errMsg}`)
}
return txHash
}
export default createTransaction

View File

@ -0,0 +1,5 @@
import { createAction } from 'redux-actions'
export const REMOVE_CANCELLATION_TRANSACTION = 'REMOVE_CANCELLATION_TRANSACTION'
export const removeCancellationTransaction = createAction(REMOVE_CANCELLATION_TRANSACTION)

View File

@ -0,0 +1,5 @@
import { createAction } from 'redux-actions'
export const REMOVE_TRANSACTION = 'REMOVE_TRANSACTION'
export const removeTransaction = createAction(REMOVE_TRANSACTION)

View File

@ -1,5 +0,0 @@
import { createAction } from 'redux-actions'
export const UPDATE_CANCELLATION_TRANSACTION = 'UPDATE_CANCELLATION_TRANSACTION'
export const updateCancellationTransaction = createAction(UPDATE_CANCELLATION_TRANSACTION)

View File

@ -1,7 +0,0 @@
import { createAction } from 'redux-actions'
export const UPDATE_TRANSACTION = 'UPDATE_TRANSACTION'
const updateTransaction = createAction(UPDATE_TRANSACTION)
export default updateTransaction

View File

@ -1,13 +1,57 @@
import { Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { ADD_CANCELLATION_TRANSACTIONS } from 'src/routes/safe/store/actions/addCancellationTransactions'
import { ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS } from 'src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions'
import { REMOVE_CANCELLATION_TRANSACTION } from 'src/routes/safe/store/actions/transactions/removeCancellationTransaction'
export const CANCELLATION_TRANSACTIONS_REDUCER_ID = 'cancellationTransactions'
export default handleActions(
{
[ADD_CANCELLATION_TRANSACTIONS]: (state, action) => action.payload,
[ADD_OR_UPDATE_CANCELLATION_TRANSACTIONS]: (state, action) => {
const { safeAddress, transactions } = action.payload
if (!safeAddress || !transactions || !transactions.size) {
return state
}
return state.withMutations((map) => {
const stateTransactionsMap = map.get(safeAddress)
if (stateTransactionsMap) {
transactions.forEach((updateTx) => {
const keyPath = [safeAddress, `${updateTx.nonce}`]
if (updateTx.confirmations.size) {
// if there are confirmations then we replace what's stored with the new tx
// as we assume that this is the newest tx returned by the server
map.setIn(keyPath, updateTx)
} else {
// if there's no confirmations, we assume this is a mocked tx
// as txs without confirmation are not being returned by the server (?has_confirmations=true)
map.mergeDeepIn(keyPath, updateTx)
}
})
} else {
map.set(safeAddress, transactions)
}
})
},
[REMOVE_CANCELLATION_TRANSACTION]: (state, action) => {
const { safeAddress, transaction } = action.payload
if (!safeAddress || !transaction) {
return state
}
return state.withMutations((map) => {
const stateTransactionsMap = map.get(safeAddress)
if (stateTransactionsMap) {
map.deleteIn([safeAddress, `${transaction.nonce}`])
}
})
},
},
Map(),
)

View File

@ -1,13 +1,70 @@
import { Map } from 'immutable'
import { handleActions } from 'redux-actions'
import { ADD_TRANSACTIONS } from 'src/routes/safe/store/actions/addTransactions'
import { ADD_OR_UPDATE_TRANSACTIONS } from 'src/routes/safe/store/actions/transactions/addOrUpdateTransactions'
import { REMOVE_TRANSACTION } from 'src/routes/safe/store/actions/transactions/removeTransaction'
export const TRANSACTIONS_REDUCER_ID = 'transactions'
export default handleActions(
{
[ADD_TRANSACTIONS]: (state, action) => action.payload,
[ADD_OR_UPDATE_TRANSACTIONS]: (state, action) => {
const { safeAddress, transactions } = action.payload
if (!safeAddress || !transactions || !transactions.size) {
return state
}
return state.withMutations((map) => {
const stateTransactionsList = map.get(safeAddress)
if (stateTransactionsList) {
const txsToStore = stateTransactionsList.withMutations((txsList) => {
transactions.forEach((updateTx) => {
const storedTxIndex = txsList.findIndex((txIterator) => txIterator.nonce === updateTx.nonce)
if (storedTxIndex !== -1) {
// Update
if (updateTx.confirmations.size) {
// if there are confirmations then we replace what's stored with the new tx
// as we assume that this is the newest tx returned by the server
txsList.update(storedTxIndex, () => updateTx)
} else {
// if there's no confirmations, we assume this is a mocked tx
// as txs without confirmation are not being returned by the server (?has_confirmations=true)
txsList.update(storedTxIndex, (storedTx) => storedTx.mergeDeep(updateTx))
}
} else {
// Add new
txsList.unshift(updateTx)
}
})
})
map.set(safeAddress, txsToStore)
} else {
map.set(safeAddress, transactions)
}
})
},
[REMOVE_TRANSACTION]: (state, action) => {
const { safeAddress, transaction } = action.payload
if (!safeAddress || !transaction) {
return state
}
return state.withMutations((map) => {
const stateTransactionsList = map.get(safeAddress)
if (stateTransactionsList) {
const storedTxIndex = stateTransactionsList.findIndex((storedTx) => storedTx.equals(transaction))
if (storedTxIndex !== -1) {
map.deleteIn([safeAddress, storedTxIndex])
}
}
})
},
},
Map(),
)