diff --git a/src/routes/safe/store/actions/transactions/addCancellationTransaction.ts b/src/routes/safe/store/actions/transactions/addCancellationTransaction.ts deleted file mode 100644 index 51cb321c..00000000 --- a/src/routes/safe/store/actions/transactions/addCancellationTransaction.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAction } from 'redux-actions' - -export const ADD_CANCELLATION_TRANSACTION = 'ADD_CANCELLATION_TRANSACTION' - -export const addCancellationTransaction = createAction(ADD_CANCELLATION_TRANSACTION) diff --git a/src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions.ts b/src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions.ts new file mode 100644 index 00000000..cf156eed --- /dev/null +++ b/src/routes/safe/store/actions/transactions/addOrUpdateCancellationTransactions.ts @@ -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) diff --git a/src/routes/safe/store/actions/transactions/addOrUpdateTransactions.ts b/src/routes/safe/store/actions/transactions/addOrUpdateTransactions.ts new file mode 100644 index 00000000..8d6cb275 --- /dev/null +++ b/src/routes/safe/store/actions/transactions/addOrUpdateTransactions.ts @@ -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) diff --git a/src/routes/safe/store/actions/transactions/addTransaction.ts b/src/routes/safe/store/actions/transactions/addTransaction.ts deleted file mode 100644 index 2d42ed23..00000000 --- a/src/routes/safe/store/actions/transactions/addTransaction.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAction } from 'redux-actions' - -export const ADD_TRANSACTION = 'ADD_TRANSACTION' - -export const addTransaction = createAction(ADD_TRANSACTION) diff --git a/src/routes/safe/store/actions/transactions/createTransaction.ts b/src/routes/safe/store/actions/transactions/createTransaction.ts deleted file mode 100644 index d040c0d0..00000000 --- a/src/routes/safe/store/actions/transactions/createTransaction.ts +++ /dev/null @@ -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 diff --git a/src/routes/safe/store/actions/transactions/removeCancellationTransaction.ts b/src/routes/safe/store/actions/transactions/removeCancellationTransaction.ts new file mode 100644 index 00000000..985f313a --- /dev/null +++ b/src/routes/safe/store/actions/transactions/removeCancellationTransaction.ts @@ -0,0 +1,5 @@ +import { createAction } from 'redux-actions' + +export const REMOVE_CANCELLATION_TRANSACTION = 'REMOVE_CANCELLATION_TRANSACTION' + +export const removeCancellationTransaction = createAction(REMOVE_CANCELLATION_TRANSACTION) diff --git a/src/routes/safe/store/actions/transactions/removeTransaction.ts b/src/routes/safe/store/actions/transactions/removeTransaction.ts new file mode 100644 index 00000000..3a84f3d4 --- /dev/null +++ b/src/routes/safe/store/actions/transactions/removeTransaction.ts @@ -0,0 +1,5 @@ +import { createAction } from 'redux-actions' + +export const REMOVE_TRANSACTION = 'REMOVE_TRANSACTION' + +export const removeTransaction = createAction(REMOVE_TRANSACTION) diff --git a/src/routes/safe/store/actions/transactions/updateCancellationTransaction.ts b/src/routes/safe/store/actions/transactions/updateCancellationTransaction.ts deleted file mode 100644 index 633372f8..00000000 --- a/src/routes/safe/store/actions/transactions/updateCancellationTransaction.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAction } from 'redux-actions' - -export const UPDATE_CANCELLATION_TRANSACTION = 'UPDATE_CANCELLATION_TRANSACTION' - -export const updateCancellationTransaction = createAction(UPDATE_CANCELLATION_TRANSACTION) diff --git a/src/routes/safe/store/actions/transactions/updateTransaction.ts b/src/routes/safe/store/actions/transactions/updateTransaction.ts deleted file mode 100644 index 41af0547..00000000 --- a/src/routes/safe/store/actions/transactions/updateTransaction.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from 'redux-actions' - -export const UPDATE_TRANSACTION = 'UPDATE_TRANSACTION' - -const updateTransaction = createAction(UPDATE_TRANSACTION) - -export default updateTransaction diff --git a/src/routes/safe/store/reducer/cancellationTransactions.ts b/src/routes/safe/store/reducer/cancellationTransactions.ts index 13be3cac..45d2c541 100644 --- a/src/routes/safe/store/reducer/cancellationTransactions.ts +++ b/src/routes/safe/store/reducer/cancellationTransactions.ts @@ -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(), ) diff --git a/src/routes/safe/store/reducer/transactions.ts b/src/routes/safe/store/reducer/transactions.ts index a8b4f8fa..dbe26943 100644 --- a/src/routes/safe/store/reducer/transactions.ts +++ b/src/routes/safe/store/reducer/transactions.ts @@ -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(), )