diff --git a/src/config/index.ts b/src/config/index.ts index 929cbf1b38..314f5cf0a1 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -56,6 +56,9 @@ export const getTxServiceHost = () => { export const getTxServiceUriFrom = (safeAddress) => `safes/${safeAddress}/transactions/` +export const getModuleTxServiceUriFrom = (safeAddress) => + `safes/${safeAddress}/module-transactions/` + export const getIncomingTxServiceUriTo = (safeAddress) => `safes/${safeAddress}/incoming-transfers/` diff --git a/src/logic/contracts/methodIds.ts b/src/logic/contracts/methodIds.ts index fbcde50b75..29876fdb72 100644 --- a/src/logic/contracts/methodIds.ts +++ b/src/logic/contracts/methodIds.ts @@ -1,15 +1,20 @@ import { web3ReadOnly as web3 } from 'src/logic/wallets/getWeb3' -import { DataDecoded, METHOD_TO_ID } from 'src/routes/safe/store/models/types/transactions.d' +import { + DataDecoded, + SAFE_METHOD_ID_TO_NAME, + SPENDING_LIMIT_METHOD_ID_TO_NAME, + SPENDING_LIMIT_METHODS_NAMES, +} from 'src/logic/safe/store/models/types/transactions.d' export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => { - const [methodId, params] = [data.slice(0, 10) as keyof typeof METHOD_TO_ID | string, data.slice(10)] + const [methodId, params] = [data.slice(0, 10) as keyof typeof SAFE_METHOD_ID_TO_NAME | string, data.slice(10)] switch (methodId) { // swapOwner case '0xe318b52b': { const decodedParameters = web3.eth.abi.decodeParameters(['uint', 'address', 'address'], params) as string[] return { - method: METHOD_TO_ID[methodId], + method: SAFE_METHOD_ID_TO_NAME[methodId], parameters: [ { name: 'oldOwner', type: 'address', value: decodedParameters[1] }, { name: 'newOwner', type: 'address', value: decodedParameters[2] }, @@ -21,7 +26,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => case '0x0d582f13': { const decodedParameters = web3.eth.abi.decodeParameters(['address', 'uint'], params) return { - method: METHOD_TO_ID[methodId], + method: SAFE_METHOD_ID_TO_NAME[methodId], parameters: [ { name: 'owner', type: 'address', value: decodedParameters[0] }, { name: '_threshold', type: 'uint', value: decodedParameters[1] }, @@ -33,7 +38,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => case '0xf8dc5dd9': { const decodedParameters = web3.eth.abi.decodeParameters(['address', 'address', 'uint'], params) return { - method: METHOD_TO_ID[methodId], + method: SAFE_METHOD_ID_TO_NAME[methodId], parameters: [ { name: 'oldOwner', type: 'address', value: decodedParameters[1] }, { name: '_threshold', type: 'uint', value: decodedParameters[2] }, @@ -45,7 +50,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => case '0x694e80c3': { const decodedParameters = web3.eth.abi.decodeParameters(['uint'], params) return { - method: METHOD_TO_ID[methodId], + method: SAFE_METHOD_ID_TO_NAME[methodId], parameters: [ { name: '_threshold', type: 'uint', value: decodedParameters[0] }, ], @@ -56,7 +61,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => case '0x610b5925': { const decodedParameters = web3.eth.abi.decodeParameters(['address'], params) return { - method: METHOD_TO_ID[methodId], + method: SAFE_METHOD_ID_TO_NAME[methodId], parameters: [ { name: 'module', type: 'address', value: decodedParameters[0] }, ], @@ -67,7 +72,7 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => case '0xe009cfde': { const decodedParameters = web3.eth.abi.decodeParameters(['address', 'address'], params) return { - method: METHOD_TO_ID[methodId], + method: SAFE_METHOD_ID_TO_NAME[methodId], parameters: [ { name: 'prevModule', type: 'address', value: decodedParameters[0] }, { name: 'module', type: 'address', value: decodedParameters[1] }, @@ -80,32 +85,20 @@ export const decodeParamsFromSafeMethod = (data: string): DataDecoded | null => } } -export const SPENDING_LIMIT_METHODS_NAMES = { - ADD_DELEGATE: 'addDelegate', - SET_ALLOWANCE: 'setAllowance', - EXECUTE_ALLOWANCE_TRANSFER: 'executeAllowanceTransfer', -} - -export const SPENDING_LIMIT_METHOD_TO_ID = { - '0xe71bdf41': SPENDING_LIMIT_METHODS_NAMES.ADD_DELEGATE, - '0xbeaeb388': SPENDING_LIMIT_METHODS_NAMES.SET_ALLOWANCE, - '0x4515641a': SPENDING_LIMIT_METHODS_NAMES.EXECUTE_ALLOWANCE_TRANSFER, -} - export const isSetAllowanceMethod = (data: string): boolean => { - const methodId = data.slice(0, 10) as keyof typeof SPENDING_LIMIT_METHOD_TO_ID - return SPENDING_LIMIT_METHOD_TO_ID[methodId] === SPENDING_LIMIT_METHODS_NAMES.SET_ALLOWANCE + const methodId = data.slice(0, 10) as keyof typeof SPENDING_LIMIT_METHOD_ID_TO_NAME + return SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId] === SPENDING_LIMIT_METHODS_NAMES.SET_ALLOWANCE } export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | null => { - const [methodId, params] = [data.slice(0, 10) as keyof typeof SPENDING_LIMIT_METHOD_TO_ID | string, data.slice(10)] + const [methodId, params] = [data.slice(0, 10) as keyof typeof SPENDING_LIMIT_METHOD_ID_TO_NAME | string, data.slice(10)] switch (methodId) { // addDelegate case '0xe71bdf41': { const decodedParameter = (web3.eth.abi.decodeParameter('address', params) as unknown) as string return { - method: SPENDING_LIMIT_METHOD_TO_ID[methodId], + method: SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId], parameters: [ { name: 'delegate', type: 'address', value: decodedParameter }, ], @@ -116,7 +109,7 @@ export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | null case '0xbeaeb388': { const decodedParameters = web3.eth.abi.decodeParameters(['address', 'address', 'uint96', 'uint16', 'uint32'], params) return { - method: SPENDING_LIMIT_METHOD_TO_ID[methodId], + method: SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId], parameters: [ { name: 'delegate', type: 'address', value: decodedParameters[0] }, { name: 'token', type: 'address', value: decodedParameters[1] }, @@ -131,7 +124,7 @@ export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | null case '0x4515641a': { const decodedParameters = web3.eth.abi.decodeParameters(['address', 'address', 'address', 'uint96', 'address', 'uint96', 'address', 'bytes'], params) return { - method: SPENDING_LIMIT_METHOD_TO_ID[methodId], + method: SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId], parameters: [ { name: "safe", type: "address", value: decodedParameters[0] }, { name: "token", type: "address", value: decodedParameters[1] }, @@ -151,11 +144,11 @@ export const decodeParamsFromSpendingLimit = (data: string): DataDecoded | null } const isSafeMethod = (methodId: string): boolean => { - return !!METHOD_TO_ID[methodId] + return !!SAFE_METHOD_ID_TO_NAME[methodId] } const isSpendingLimitMethod = (methodId: string): boolean => { - return !!SPENDING_LIMIT_METHOD_TO_ID[methodId] + return !!SPENDING_LIMIT_METHOD_ID_TO_NAME[methodId] } export const decodeMethods = (data: string | null): DataDecoded | null => { diff --git a/src/logic/safe/store/actions/addModuleTransactions.ts b/src/logic/safe/store/actions/addModuleTransactions.ts new file mode 100644 index 0000000000..6c26cb5971 --- /dev/null +++ b/src/logic/safe/store/actions/addModuleTransactions.ts @@ -0,0 +1,13 @@ +import { createAction } from 'redux-actions' +import { ModuleTxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadModuleTransactions' + +export const ADD_MODULE_TRANSACTIONS = 'ADD_MODULE_TRANSACTIONS' + +export type AddModuleTransactionsAction = { + payload: { + safeAddress: string + modules: ModuleTxServiceModel[] + } +} + +export const addModuleTransactions = createAction(ADD_MODULE_TRANSACTIONS) diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions.ts index f4907bed07..ed31deb8e9 100644 --- a/src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions.ts +++ b/src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions.ts @@ -2,17 +2,25 @@ import axios from 'axios' import { buildTxServiceUrl } from 'src/logic/safe/transactions' import { buildIncomingTxServiceUrl } from 'src/logic/safe/transactions/incomingTxHistory' +import { buildModuleTxServiceUrl } from 'src/logic/safe/transactions/moduleTxHistory' import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions' import { IncomingTxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadIncomingTransactions' import { TransactionTypes } from 'src/logic/safe/store/models/types/transaction' +import { ModuleTxServiceModel } from './loadModuleTransactions' const getServiceUrl = (txType: string, safeAddress: string): string => { return { [TransactionTypes.INCOMING]: buildIncomingTxServiceUrl, [TransactionTypes.OUTGOING]: buildTxServiceUrl, + [TransactionTypes.MODULE]: buildModuleTxServiceUrl, }[txType](safeAddress) } +async function fetchTransactions( + txType: TransactionTypes.MODULE, + safeAddress: string, + eTag: string | null, +): Promise<{ eTag: string | null; results: ModuleTxServiceModel[] }> async function fetchTransactions( txType: TransactionTypes.INCOMING, safeAddress: string, @@ -24,10 +32,10 @@ async function fetchTransactions( eTag: string | null, ): Promise<{ eTag: string | null; results: TxServiceModel[] }> async function fetchTransactions( - txType: TransactionTypes.INCOMING | TransactionTypes.OUTGOING, + txType: TransactionTypes.MODULE | TransactionTypes.INCOMING | TransactionTypes.OUTGOING, safeAddress: string, eTag: string | null, -): Promise<{ eTag: string | null; results: TxServiceModel[] | IncomingTxServiceModel[] }> { +): Promise<{ eTag: string | null; results: ModuleTxServiceModel[] | TxServiceModel[] | IncomingTxServiceModel[] }> { try { const url = getServiceUrl(txType, safeAddress) const response = await axios.get(url, eTag ? { headers: { 'If-None-Match': eTag } } : undefined) diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts index 09d00a0563..b15aaa6c93 100644 --- a/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts +++ b/src/logic/safe/store/actions/transactions/fetchTransactions/index.ts @@ -1,17 +1,18 @@ +import { backOff } from 'exponential-backoff' import { batch } from 'react-redux' -import { ThunkAction, ThunkDispatch } from 'redux-thunk' import { AnyAction } from 'redux' -import { backOff } from 'exponential-backoff' - -import { addIncomingTransactions } from '../../addIncomingTransactions' - -import { loadIncomingTransactions } from './loadIncomingTransactions' -import { loadOutgoingTransactions } from './loadOutgoingTransactions' +import { ThunkAction, ThunkDispatch } from 'redux-thunk' +import { addIncomingTransactions } from 'src/logic/safe/store/actions/addIncomingTransactions' +import { addModuleTransactions } from 'src/logic/safe/store/actions/addModuleTransactions' import { addOrUpdateCancellationTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateCancellationTransactions' import { addOrUpdateTransactions } from 'src/logic/safe/store/actions/transactions/addOrUpdateTransactions' import { AppReduxState } from 'src/store' +import { loadIncomingTransactions } from './loadIncomingTransactions' +import { loadModuleTransactions } from './loadModuleTransactions' +import { loadOutgoingTransactions } from './loadOutgoingTransactions' + const noFunc = () => {} export default (safeAddress: string): ThunkAction, AppReduxState, undefined, AnyAction> => async ( @@ -44,6 +45,12 @@ export default (safeAddress: string): ThunkAction, AppReduxState, if (safeIncomingTxs?.size) { dispatch(addIncomingTransactions(incomingTransactions)) } + + const moduleTransactions = await loadModuleTransactions(safeAddress) + + if (moduleTransactions.length) { + dispatch(addModuleTransactions({ modules: moduleTransactions, safeAddress })) + } } catch (error) { console.log('Error fetching transactions:', error) } diff --git a/src/logic/safe/store/actions/transactions/fetchTransactions/loadModuleTransactions.ts b/src/logic/safe/store/actions/transactions/fetchTransactions/loadModuleTransactions.ts new file mode 100644 index 0000000000..7dd1b7b7d3 --- /dev/null +++ b/src/logic/safe/store/actions/transactions/fetchTransactions/loadModuleTransactions.ts @@ -0,0 +1,35 @@ +import fetchTransactions from 'src/logic/safe/store/actions/transactions/fetchTransactions/fetchTransactions' +import { TransactionTypes } from 'src/logic/safe/store/models/types/transaction' +import { DataDecoded, Operation } from 'src/logic/safe/store/models/types/transactions.d' + +export type ModuleTxServiceModel = { + created: string + executionDate: string + blockNumber: number + transactionHash: string + safe: string + module: string + to: string + value: string + data: string + operation: Operation + dataDecoded: DataDecoded +} + +type ETag = string | null + +let previousETag: ETag = null +export const loadModuleTransactions = async (safeAddress: string): Promise => { + if (!safeAddress) { + return [] + } + + const { eTag, results }: { eTag: ETag; results: ModuleTxServiceModel[] } = await fetchTransactions( + TransactionTypes.MODULE, + safeAddress, + previousETag, + ) + previousETag = eTag + + return results +} diff --git a/src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts b/src/logic/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts similarity index 91% rename from src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts rename to src/logic/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts index 3573c2755a..4dceaa9140 100644 --- a/src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts +++ b/src/logic/safe/store/actions/transactions/utils/multiSendDecodedDetails.ts @@ -12,8 +12,8 @@ import { extractERC721TransferDetails, extractETHTransferDetails, extractUnknownTransferDetails, -} from './transferDetails' -import { isMultiSendParameter } from './newTransactionHelpers' +} from 'src/logic/safe/store/actions/transactions/utils/transferDetails' +import { isMultiSendParameter } from 'src/logic/safe/store/actions/transactions/utils/newTransactionHelpers' import { Transaction } from 'src/logic/safe/store/models/types/transaction' export type MultiSendDetails = { diff --git a/src/routes/safe/store/actions/transactions/utils/newTransactionHelpers.ts b/src/logic/safe/store/actions/transactions/utils/newTransactionHelpers.ts similarity index 92% rename from src/routes/safe/store/actions/transactions/utils/newTransactionHelpers.ts rename to src/logic/safe/store/actions/transactions/utils/newTransactionHelpers.ts index 0b1ddeabc8..00570d02bb 100644 --- a/src/routes/safe/store/actions/transactions/utils/newTransactionHelpers.ts +++ b/src/logic/safe/store/actions/transactions/utils/newTransactionHelpers.ts @@ -6,7 +6,7 @@ import { Parameter, Transaction, TxType, -} from 'src/routes/safe/store/models/types/transactions.d' +} from 'src/logic/safe/store/models/types/transactions.d' export const isMultiSigTx = (tx: Transaction): tx is MultiSigTransaction => { return TxType[tx.txType] === TxType.MULTISIG_TRANSACTION diff --git a/src/logic/safe/store/actions/transactions/utils/newTransactionsHelpers.ts b/src/logic/safe/store/actions/transactions/utils/newTransactionsHelpers.ts deleted file mode 100644 index dbf8c096f4..0000000000 --- a/src/logic/safe/store/actions/transactions/utils/newTransactionsHelpers.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Transaction, TxType } from 'src/logic/safe/store/models/types/transactions' - -export const isMultiSigTx = (tx: Transaction): boolean => { - return TxType[tx.txType] === TxType.MULTISIG_TRANSACTION -} -export const isModuleTx = (tx: Transaction): boolean => { - return TxType[tx.txType] === TxType.MODULE_TRANSACTION -} -export const isEthereumTx = (tx: Transaction): boolean => { - return TxType[tx.txType] === TxType.ETHEREUM_TRANSACTION -} diff --git a/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts b/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts index db80263277..20045b0820 100644 --- a/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts +++ b/src/logic/safe/store/actions/transactions/utils/transactionHelpers.ts @@ -35,7 +35,7 @@ import { TypedDataUtils } from 'eth-sig-util' import { Token } from 'src/logic/tokens/store/model/token' import { ProviderRecord } from 'src/logic/wallets/store/model/provider' import { SafeRecord } from 'src/logic/safe/store/models/safe' -import { DecodedParams } from 'src/routes/safe/store/models/types/transactions.d' +import { DecodedParams } from 'src/logic/safe/store/models/types/transactions.d' export const isEmptyData = (data?: string | null): boolean => { return !data || data === EMPTY_DATA diff --git a/src/routes/safe/store/actions/transactions/utils/transferDetails.d.ts b/src/logic/safe/store/actions/transactions/utils/transferDetails.d.ts similarity index 100% rename from src/routes/safe/store/actions/transactions/utils/transferDetails.d.ts rename to src/logic/safe/store/actions/transactions/utils/transferDetails.d.ts diff --git a/src/routes/safe/store/actions/transactions/utils/transferDetails.ts b/src/logic/safe/store/actions/transactions/utils/transferDetails.ts similarity index 93% rename from src/routes/safe/store/actions/transactions/utils/transferDetails.ts rename to src/logic/safe/store/actions/transactions/utils/transferDetails.ts index 12363dff05..353f34ff89 100644 --- a/src/routes/safe/store/actions/transactions/utils/transferDetails.ts +++ b/src/logic/safe/store/actions/transactions/utils/transferDetails.ts @@ -1,4 +1,4 @@ -import { Transfer, TxConstants } from 'src/routes/safe/store/models/types/transactions.d' +import { Transfer, TxConstants } from 'src/logic/safe/store/models/types/transactions.d' import { sameAddress } from 'src/logic/wallets/ethAddresses' import { store } from 'src/store' import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' @@ -7,7 +7,7 @@ import { ERC721TransferDetails, ETHTransferDetails, UnknownTransferDetails, -} from './transferDetails.d' +} from 'src/logic/safe/store/actions/transactions/utils/transferDetails.d' import { humanReadableValue } from 'src/logic/tokens/utils/humanReadableValue' const isIncomingTransfer = (transfer: Transfer): boolean => { diff --git a/src/logic/safe/store/models/types/transaction.ts b/src/logic/safe/store/models/types/transaction.ts index 1ecd114a44..cf44e6566c 100644 --- a/src/logic/safe/store/models/types/transaction.ts +++ b/src/logic/safe/store/models/types/transaction.ts @@ -1,8 +1,10 @@ import { List, Map, RecordOf } from 'immutable' +import { ModuleTxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadModuleTransactions' +import { Token } from 'src/logic/tokens/store/model/token' import { Confirmation } from './confirmation' import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' import { DataDecoded, Transfer } from './transactions' -import { DecodedParams } from 'src/routes/safe/store/models/types/transactions' +import { DecodedParams } from 'src/logic/safe/store/models/types/transactions' export enum TransactionTypes { INCOMING = 'incoming', @@ -14,6 +16,8 @@ export enum TransactionTypes { UPGRADE = 'upgrade', TOKEN = 'token', COLLECTIBLE = 'collectible', + MODULE = 'module', + SPENDING_LIMIT = 'spendingLimit', } export type TransactionTypeValues = typeof TransactionTypes[keyof typeof TransactionTypes] @@ -101,3 +105,13 @@ export type TxArgs = { to: string valueInWei: string } + +export type SafeModuleTransaction = ModuleTxServiceModel & { + nonce?: string // not required for this tx: added for compatibility + fee?: number // not required for this tx: added for compatibility + executionTxHash?: string // not required for this tx: added for compatibility + safeTxHash: string // table uses this key as a unique row identifier, added for compatibility + status: TransactionStatus + type: TransactionTypes + tokenInfo?: Token +} diff --git a/src/logic/safe/store/models/types/transactions.d.ts b/src/logic/safe/store/models/types/transactions.d.ts index a9fcc709ce..723e68eb3a 100644 --- a/src/logic/safe/store/models/types/transactions.d.ts +++ b/src/logic/safe/store/models/types/transactions.d.ts @@ -230,7 +230,7 @@ export const SAFE_METHODS_NAMES = { DISABLE_MODULE: 'disableModule', } -export const METHOD_TO_ID = { +export const SAFE_METHOD_ID_TO_NAME = { '0xe318b52b': SAFE_METHODS_NAMES.SWAP_OWNER, '0x0d582f13': SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD, '0xf8dc5dd9': SAFE_METHODS_NAMES.REMOVE_OWNER, @@ -239,6 +239,18 @@ export const METHOD_TO_ID = { '0xe009cfde': SAFE_METHODS_NAMES.DISABLE_MODULE, } +export const SPENDING_LIMIT_METHODS_NAMES = { + ADD_DELEGATE: 'addDelegate', + SET_ALLOWANCE: 'setAllowance', + EXECUTE_ALLOWANCE_TRANSFER: 'executeAllowanceTransfer', +} + +export const SPENDING_LIMIT_METHOD_ID_TO_NAME = { + '0xe71bdf41': SPENDING_LIMIT_METHODS_NAMES.ADD_DELEGATE, + '0xbeaeb388': SPENDING_LIMIT_METHODS_NAMES.SET_ALLOWANCE, + '0x4515641a': SPENDING_LIMIT_METHODS_NAMES.EXECUTE_ALLOWANCE_TRANSFER, +} + export type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES] type TokenMethods = 'transfer' | 'transferFrom' | 'safeTransferFrom' diff --git a/src/logic/safe/store/reducer/moduleTransactions.ts b/src/logic/safe/store/reducer/moduleTransactions.ts new file mode 100644 index 0000000000..27b310b237 --- /dev/null +++ b/src/logic/safe/store/reducer/moduleTransactions.ts @@ -0,0 +1,32 @@ +import { handleActions } from 'redux-actions' + +import { + ADD_MODULE_TRANSACTIONS, + AddModuleTransactionsAction, +} from 'src/logic/safe/store/actions/addModuleTransactions' +import { ModuleTxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadModuleTransactions' + +export const MODULE_TRANSACTIONS_REDUCER_ID = 'moduleTransactions' + +export interface ModuleTransactionsState { + [safeAddress: string]: ModuleTxServiceModel[] +} + +export default handleActions( + { + [ADD_MODULE_TRANSACTIONS]: (state: ModuleTransactionsState, action: AddModuleTransactionsAction) => { + const { modules, safeAddress } = action.payload + const oldModuleTxs = state[safeAddress] ?? [] + const oldModuleTxsHashes = oldModuleTxs.map(({ transactionHash }) => transactionHash) + // As backend is returning the whole list of txs on every request, + // to avoid duplicates, filtering happens in this level. + const newModuleTxs = modules.filter((moduleTx) => !oldModuleTxsHashes.includes(moduleTx.transactionHash)) + + return { + ...state, + [safeAddress]: [...oldModuleTxs, ...newModuleTxs], + } + }, + }, + {}, +) diff --git a/src/logic/safe/store/selectors/index.ts b/src/logic/safe/store/selectors/index.ts index e396557e8a..db6d016eab 100644 --- a/src/logic/safe/store/selectors/index.ts +++ b/src/logic/safe/store/selectors/index.ts @@ -1,20 +1,29 @@ import { List, Map, Set } from 'immutable' import { matchPath, RouteComponentProps } from 'react-router-dom' import { createSelector } from 'reselect' -import { SAFELIST_ADDRESS, SAFE_PARAM_ADDRESS } from 'src/routes/routes' +import makeSafe, { SafeRecord, SafeRecordProps } from 'src/logic/safe/store/models/safe' +import { + SafeModuleTransaction, + TransactionStatus, + TransactionTypes, +} from 'src/logic/safe/store/models/types/transaction' import { CANCELLATION_TRANSACTIONS_REDUCER_ID, CancellationTransactions, } from 'src/logic/safe/store/reducer/cancellationTransactions' import { INCOMING_TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/incomingTransactions' +import { MODULE_TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/moduleTransactions' import { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe' import { TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/transactions' +import { tokenListSelector } from 'src/logic/tokens/store/selectors' +import { sameAddress } from 'src/logic/wallets/ethAddresses' +import { SAFE_PARAM_ADDRESS, SAFELIST_ADDRESS } from 'src/routes/routes' +import { SafesMap } from 'src/routes/safe/store/reducer/types/safe' import { AppReduxState } from 'src/store' - import { checksumAddress } from 'src/utils/checksumAddress' -import makeSafe, { SafeRecord, SafeRecordProps } from '../models/safe' -import { SafesMap } from 'src/routes/safe/store/reducer/types/safe' +import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants' +import { getEthAsToken } from 'src/logic/tokens/utils/tokenHelpers' const safesStateSelector = (state: AppReduxState) => state[SAFE_REDUCER_ID] @@ -36,6 +45,8 @@ const cancellationTransactionsSelector = (state: AppReduxState) => state[CANCELL const incomingTransactionsSelector = (state: AppReduxState) => state[INCOMING_TRANSACTIONS_REDUCER_ID] +const moduleTransactionsSelector = (state: AppReduxState) => state[MODULE_TRANSACTIONS_REDUCER_ID] + export const safeParamAddressFromStateSelector = (state: AppReduxState): string | undefined => { const match = matchPath<{ safeAddress: string }>(state.router.location.pathname, { path: `${SAFELIST_ADDRESS}/:safeAddress`, @@ -114,6 +125,49 @@ export const safeIncomingTransactionsSelector = createSelector( }, ) +export const safeModuleTransactionsSelector = createSelector( + tokenListSelector, + moduleTransactionsSelector, + safeParamAddressFromStateSelector, + (tokens, moduleTransactions, safeAddress): SafeModuleTransaction[] => { + // no module tx for the current safe so far + if (!moduleTransactions || !safeAddress || !moduleTransactions[safeAddress]) { + return [] + } + + return moduleTransactions[safeAddress]?.map((moduleTx) => { + // if not spendingLimit module tx, then it's an generic module tx + const type = sameAddress(moduleTx.module, SPENDING_LIMIT_MODULE_ADDRESS) + ? TransactionTypes.SPENDING_LIMIT + : TransactionTypes.MODULE + + // TODO: this is strictly attached to Spending Limit Module. + // This has to be moved nearest the module info rendering. + // add token info to the model, so data can be properly displayed in the UI + let tokenInfo + if (type === TransactionTypes.SPENDING_LIMIT) { + if (moduleTx.data) { + // if `data` is defined, then it's a token transfer + tokenInfo = tokens.find(({ address }) => sameAddress(address, moduleTx.to)) + } else { + // if `data` is not defined, then it's an ETH transfer + // ETH does not exist in the list of tokens, so we recreate the record here + tokenInfo = getEthAsToken(0) + } + } + + return { + ...moduleTx, + safeTxHash: moduleTx.transactionHash, + executionTxHash: moduleTx.transactionHash, + status: TransactionStatus.SUCCESS, + tokenInfo, + type, + } + }) + }, +) + export const safeSelector = createSelector( safesMapSelector, safeParamAddressFromStateSelector, diff --git a/src/logic/safe/store/selectors/transactions.ts b/src/logic/safe/store/selectors/transactions.ts index 123f360edc..6a6c275f45 100644 --- a/src/logic/safe/store/selectors/transactions.ts +++ b/src/logic/safe/store/selectors/transactions.ts @@ -1,11 +1,17 @@ import { List } from 'immutable' import { createSelector } from 'reselect' -import { safeIncomingTransactionsSelector, safeTransactionsSelector } from 'src/logic/safe/store/selectors' -import { Transaction } from 'src/logic/safe/store/models/types/transaction' +import { + safeIncomingTransactionsSelector, + safeTransactionsSelector, + safeModuleTransactionsSelector, +} from 'src/logic/safe/store/selectors' +import { Transaction, SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction' export const extendedTransactionsSelector = createSelector( safeTransactionsSelector, safeIncomingTransactionsSelector, - (transactions, incomingTransactions): List => List([...transactions, ...incomingTransactions]), + safeModuleTransactionsSelector, + (transactions, incomingTransactions, moduleTransactions): List => + List([...transactions, ...incomingTransactions, ...moduleTransactions]), ) diff --git a/src/logic/safe/transactions/moduleTxHistory.ts b/src/logic/safe/transactions/moduleTxHistory.ts new file mode 100644 index 0000000000..2f8f52c494 --- /dev/null +++ b/src/logic/safe/transactions/moduleTxHistory.ts @@ -0,0 +1,10 @@ +import { getModuleTxServiceUriFrom, getTxServiceHost } from 'src/config' +import { checksumAddress } from 'src/utils/checksumAddress' + +export const buildModuleTxServiceUrl = (safeAddress: string): string => { + const host = getTxServiceHost() + const address = checksumAddress(safeAddress) + const base = getModuleTxServiceUriFrom(address) + + return `${host}${base}` +} diff --git a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx index 1bbb0c2559..6c1eb8f6e4 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ReviewTx/index.tsx @@ -2,7 +2,6 @@ import StandardToken from '@gnosis.pm/util-contracts/build/contracts/GnosisStand import IconButton from '@material-ui/core/IconButton' import { makeStyles } from '@material-ui/core/styles' import Close from '@material-ui/icons/Close' -import { withSnackbar } from 'notistack' import React, { useEffect, useState, useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' import { AbiItem } from 'web3-utils' @@ -38,7 +37,7 @@ import { styles } from './style' const useStyles = makeStyles(styles) -const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { +const ReviewTx = ({ onClose, onPrev, tx }) => { const classes = useStyles() const dispatch = useDispatch() const { address: safeAddress } = useSelector(safeSelector) || {} @@ -93,37 +92,37 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { // txAmount should be 0 if we send tokens // the real value is encoded in txData and will be used by the contract // if txAmount > 0 it would send ETH from the Safe - const txAmount = isSendingETH ? web3.utils.toWei(tx.amount, 'ether') : '0' - - if (isSpendingLimit && txToken) { - const spendingLimit = getSpendingLimitContract() - spendingLimit.methods - .executeAllowanceTransfer( - safeAddress, - txToken.address === ETH_ADDRESS ? ZERO_ADDRESS : txToken.address, - tx.recipientAddress, - toTokenUnit(tx.amount, txToken.decimals), - ZERO_ADDRESS, - 0, - tx.tokenSpendingLimit.delegate, - EMPTY_DATA, + const txAmount = isSendingETH ? web3.utils.toWei(tx.amount, 'ether').toString() : '0' + + if (safeAddress) { + if (isSpendingLimit && txToken) { + const spendingLimit = getSpendingLimitContract() + spendingLimit.methods + .executeAllowanceTransfer( + safeAddress, + txToken.address === ETH_ADDRESS ? ZERO_ADDRESS : txToken.address, + tx.recipientAddress, + toTokenUnit(tx.amount, txToken.decimals), + ZERO_ADDRESS, + 0, + tx.tokenSpendingLimit.delegate, + EMPTY_DATA, + ) + .send({ from: tx.tokenSpendingLimit.delegate }) + .on('transactionHash', () => onClose()) + .catch(console.error) + } else { + dispatch( + createTransaction({ + safeAddress, + to: txRecipient, + valueInWei: txAmount, + txData: data, + notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, + }), ) - .send({ from: tx.tokenSpendingLimit.delegate }) - .on('transactionHash', () => onClose()) - .catch(console.error) - } else { - dispatch( - createTransaction({ - safeAddress, - to: txRecipient, - valueInWei: txAmount, - txData: data, - notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX, - enqueueSnackbar, - closeSnackbar, - } as any), - ) - onClose() + onClose() + } } } @@ -217,4 +216,4 @@ const ReviewTx = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }) => { ) } -export default withSnackbar(ReviewTx) +export default ReviewTx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/style.ts b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/style.ts deleted file mode 100644 index f149263b7d..0000000000 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/style.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { border, lg, md } from 'src/theme/variables' - -const cssStyles = { - expandedTxBlock: { - borderBottom: `2px solid ${border}`, - }, - txDataContainer: { - padding: `${lg} ${md}`, - }, - txHash: { - paddingRight: '3px', - }, - incomingTxBlock: { - borderRight: '2px solid rgb(232, 231, 230)', - }, - emptyRowDataContainer: { - paddingTop: lg, - paddingLeft: md, - paddingBottom: md, - borderRight: '2px solid rgb(232, 231, 230)', - }, -} - -export const styles = (): typeof cssStyles => cssStyles diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/ApproveTxModal/index.tsx similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/ApproveTxModal/index.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/style.ts b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/ApproveTxModal/style.ts similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/style.ts rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/ApproveTxModal/style.ts diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/CreationTx/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/CreationTx/index.tsx similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/CreationTx/index.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/CreationTx/index.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/IncomingTx/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/IncomingTx/index.tsx similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/IncomingTx/index.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/IncomingTx/index.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/IncomingTxDescription/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/IncomingTxDescription/index.tsx similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/IncomingTxDescription/index.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/IncomingTxDescription/index.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OutgoingTx/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OutgoingTx/index.tsx similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OutgoingTx/index.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OutgoingTx/index.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnerComponent.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/OwnerComponent.tsx similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnerComponent.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/OwnerComponent.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnersList.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/OwnersList.tsx similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/OwnersList.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/OwnersList.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/cancel-small-filled.svg b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/cancel-small-filled.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/cancel-small-filled.svg rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/cancel-small-filled.svg diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/check-large-filled-green.svg b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/check-large-filled-green.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/check-large-filled-green.svg rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/check-large-filled-green.svg diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/check-large-filled-red.svg b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/check-large-filled-red.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/check-large-filled-red.svg rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/check-large-filled-red.svg diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-large-green.svg b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-large-green.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-large-green.svg rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-large-green.svg diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-large-grey.svg b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-large-grey.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-large-grey.svg rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-large-grey.svg diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-large-red.svg b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-large-red.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-large-red.svg rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-large-red.svg diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-small-filled.svg b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-small-filled.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-small-filled.svg rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-small-filled.svg diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-small-green.svg b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-small-green.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-small-green.svg rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-small-green.svg diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-small-grey.svg b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-small-grey.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-small-grey.svg rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-small-grey.svg diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-small-red.svg b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-small-red.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-small-red.svg rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-small-red.svg diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-small-yellow.svg b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-small-yellow.svg similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/assets/confirm-small-yellow.svg rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/assets/confirm-small-yellow.svg diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/index.tsx similarity index 99% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/index.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/index.tsx index 03cfea3f24..9c7a7b43f0 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/index.tsx @@ -91,7 +91,7 @@ const useStyles = makeStyles(styles) type ownersColumnProps = { tx: Transaction - cancelTx: Transaction + cancelTx?: Transaction thresholdReached: boolean cancelThresholdReached: boolean onTxConfirm: () => void diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/style.ts b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/style.ts similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/style.ts rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/OwnersColumn/style.ts diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/RejectTxModal/index.tsx similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/index.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/RejectTxModal/index.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/style.ts b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/RejectTxModal/style.ts similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/RejectTxModal/style.ts rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/RejectTxModal/style.ts diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/CustomDescription.tsx similarity index 91% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/CustomDescription.tsx index 36f2427885..57219d3287 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/CustomDescription.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/CustomDescription.tsx @@ -15,7 +15,7 @@ import Block from 'src/components/layout/Block' import { extractMultiSendDataDecoded, MultiSendDetails, -} from 'src/routes/safe/store/actions/transactions/utils/multiSendDecodedDetails' +} from 'src/logic/safe/store/actions/transactions/utils/multiSendDecodedDetails' import Bold from 'src/components/layout/Bold' import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell' import EtherscanLink from 'src/components/EtherscanLink' @@ -26,8 +26,8 @@ import { getNameFromAddressBook } from 'src/logic/addressBook/store/selectors' import Paragraph from 'src/components/layout/Paragraph' import LinkWithRef from 'src/components/layout/Link' import { shortVersionOf } from 'src/logic/wallets/ethAddresses' -import { Transaction } from 'src/logic/safe/store/models/types/transaction' -import { DataDecoded } from 'src/routes/safe/store/models/types/transactions.d' +import { Transaction, SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction' +import { DataDecoded } from 'src/logic/safe/store/models/types/transactions.d' import DividerLine from 'src/components/DividerLine' import { decodeMethods, isSetAllowanceMethod } from 'src/logic/contracts/methodIds' @@ -78,7 +78,7 @@ interface NewSpendingLimitDetailsProps { data: DataDecoded } -const NewSpendingLimitDetails = ({ data }: NewSpendingLimitDetailsProps): React.ReactElement => { +const ModifySpendingLimitDetails = ({ data }: NewSpendingLimitDetailsProps): React.ReactElement => { const [beneficiary, tokenAddress, amount, resetTimeMin] = React.useMemo( () => data.parameters.map(({ value }) => value), [data.parameters], @@ -94,7 +94,7 @@ const NewSpendingLimitDetails = ({ data }: NewSpendingLimitDetailsProps): React. return ( <> - New Spending Limit: + Modify Spending Limit: @@ -127,7 +127,7 @@ const MultiSendCustomDataAction = ({ tx, order }: { tx: MultiSendDetails; order: > {isNewSpendingLimit && data ? ( - + ) : ( @@ -199,7 +199,7 @@ const TxData = ({ data }: { data: string }): React.ReactElement => { ) } -const TxActionData = ({ dataDecoded }: { dataDecoded: DataDecoded }): React.ReactElement => { +export const TxActionData = ({ dataDecoded }: { dataDecoded: DataDecoded }): React.ReactElement => { const classes = useStyles() return ( @@ -235,16 +235,21 @@ interface GenericCustomDataProps { amount?: string data: string recipient: string - storedTx: Transaction + storedTx: Transaction | SafeModuleTransaction } -const GenericCustomData = ({ amount = '0', data, recipient, storedTx }: GenericCustomDataProps): React.ReactElement => { +export const GenericCustomData = ({ + amount = '0', + data, + recipient, + storedTx, +}: GenericCustomDataProps): React.ReactElement => { const recipientName = useSelector((state) => getNameFromAddressBook(state, recipient)) const txData = storedTx?.dataDecoded ?? decodeMethods(data) const isNewSpendingLimit = isSetAllowanceMethod(data || '') return isNewSpendingLimit && txData ? ( - + ) : ( diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/SettingsDescription.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/SettingsDescription.tsx similarity index 97% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/SettingsDescription.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/SettingsDescription.tsx index 974382b91e..c810267f6e 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/SettingsDescription.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/SettingsDescription.tsx @@ -7,7 +7,7 @@ import Bold from 'src/components/layout/Bold' import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell' import EtherscanLink from 'src/components/EtherscanLink' import Paragraph from 'src/components/layout/Paragraph' -import { SAFE_METHODS_NAMES, SafeMethods } from 'src/routes/safe/store/models/types/transactions.d' +import { SAFE_METHODS_NAMES, SafeMethods } from 'src/logic/safe/store/models/types/transactions.d' export const TRANSACTIONS_DESC_ADD_OWNER_TEST_ID = 'tx-description-add-owner' export const TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID = 'tx-description-remove-owner' diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/TransferDescription.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/TransferDescription.tsx similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/TransferDescription.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/TransferDescription.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/Value.tsx similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/Value.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/Value.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/index.tsx similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/index.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/index.tsx diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/styles.ts b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/styles.ts similarity index 100% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/styles.ts rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/styles.ts diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/utils.ts b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/utils.ts similarity index 97% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/utils.ts rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/utils.ts index d204b86318..4b7272a254 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/utils.ts +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/utils.ts @@ -1,5 +1,5 @@ import { Transaction } from 'src/logic/safe/store/models/types/transaction' -import { SAFE_METHODS_NAMES } from 'src/routes/safe/store/models/types/transactions.d' +import { SAFE_METHODS_NAMES } from 'src/logic/safe/store/models/types/transactions.d' const getSafeVersion = (data) => { const contractAddress = data.substr(340, 40).toLowerCase() diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/index.tsx similarity index 67% rename from src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx rename to src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/index.tsx index 7bd08ad8c7..1a64f0467a 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/index.tsx @@ -1,8 +1,13 @@ -import { makeStyles } from '@material-ui/core/styles' import cn from 'classnames' import React, { useState } from 'react' import { useSelector } from 'react-redux' import { EthHashInfo } from '@gnosis.pm/safe-react-components' +import { + getModuleAmount, + TableData, + TX_TABLE_RAW_CANCEL_TX_ID, + TX_TABLE_RAW_TX_ID, +} from 'src/routes/safe/components/Transactions/TxsTable/columns' import ApproveTxModal from './ApproveTxModal' import OwnersColumn from './OwnersColumn' @@ -11,7 +16,7 @@ import TxDescription from './TxDescription' import { IncomingTx } from './IncomingTx' import { CreationTx } from './CreationTx' import { OutgoingTx } from './OutgoingTx' -import { styles } from './style' +import useStyles from './style' import { getNetwork } from 'src/config' import Block from 'src/components/layout/Block' @@ -24,13 +29,57 @@ import Span from 'src/components/layout/Span' import { getWeb3 } from 'src/logic/wallets/getWeb3' import { INCOMING_TX_TYPES } from 'src/logic/safe/store/models/incomingTransaction' import { safeNonceSelector, safeThresholdSelector } from 'src/logic/safe/store/selectors' -import { Transaction, TransactionTypes } from 'src/logic/safe/store/models/types/transaction' +import { Transaction, TransactionTypes, SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction' import IncomingTxDescription from './IncomingTxDescription' +import TransferDescription from './TxDescription/TransferDescription' -const useStyles = makeStyles(styles as any) +const ExpandedModuleTx = ({ tx }: { tx: SafeModuleTransaction }): React.ReactElement => { + const classes = useStyles() + + const recipient = React.useMemo(() => { + if (tx.type === TransactionTypes.SPENDING_LIMIT) { + if (tx.dataDecoded) { + // if `dataDecoded` is defined, then it's a token transfer + return tx.dataDecoded?.parameters[0].value + } else { + // if `data` is not defined, then it's an ETH transfer + return tx.to + } + } + }, [tx.dataDecoded, tx.to, tx.type]) + + return ( + + + + +
+ Hash: + {tx.executionTxHash ? ( + + ) : ( + 'n/a' + )} +
+
+ + + {recipient && } + + +
+
+ ) +} interface ExpandedTxProps { - cancelTx: Transaction + cancelTx?: Transaction tx: Transaction } @@ -125,7 +174,7 @@ const ExpandedTx = ({ cancelTx, tx }: ExpandedTxProps): React.ReactElement => { /> )} {openModal === 'rejectTx' && } - {openModal === 'executeRejectTx' && ( + {openModal === 'executeRejectTx' && cancelTx && ( { ) } -export default ExpandedTx +const ExpandedTxRow = ({ row }: { row: TableData }): React.ReactElement => { + const isModuleTx = [TransactionTypes.SPENDING_LIMIT, TransactionTypes.MODULE].includes(row.tx.type) + + if (isModuleTx) { + return + } + + return +} + +export default ExpandedTxRow diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/style.ts b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/style.ts new file mode 100644 index 0000000000..0a7034b56d --- /dev/null +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/style.ts @@ -0,0 +1,25 @@ +import { createStyles, makeStyles } from '@material-ui/core' +import { border, lg, md } from 'src/theme/variables' + +export default makeStyles( + createStyles({ + expandedTxBlock: { + borderBottom: `2px solid ${border}`, + }, + txDataContainer: { + padding: `${lg} ${md}`, + }, + txHash: { + paddingRight: '3px', + }, + incomingTxBlock: { + borderRight: '2px solid rgb(232, 231, 230)', + }, + emptyRowDataContainer: { + paddingTop: lg, + paddingLeft: md, + paddingBottom: md, + borderRight: '2px solid rgb(232, 231, 230)', + }, + }), +) diff --git a/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx b/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx index 28c54f67f7..2201cf9ef1 100644 --- a/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx @@ -20,6 +20,8 @@ const typeToIcon = { creation: SettingsTxIcon, cancellation: SettingsTxIcon, upgrade: SettingsTxIcon, + module: SettingsTxIcon, + spendingLimit: SettingsTxIcon, } const typeToLabel = { @@ -32,6 +34,8 @@ const typeToLabel = { creation: 'Safe created', cancellation: 'Cancellation transaction', upgrade: 'Contract Upgrade', + module: 'Module transaction', + spendingLimit: 'Spending Limit', } interface TxTypeProps { @@ -73,4 +77,5 @@ const TxType = ({ origin, txType }: TxTypeProps): React.ReactElement => { return loading ? : } + export default TxType diff --git a/src/routes/safe/components/Transactions/TxsTable/columns.tsx b/src/routes/safe/components/Transactions/TxsTable/columns.tsx index 39eca8d5db..6a5b285347 100644 --- a/src/routes/safe/components/Transactions/TxsTable/columns.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/columns.tsx @@ -9,10 +9,10 @@ import TxType from './TxType' import { buildOrderFieldFrom } from 'src/components/Table/sorting' import { TableColumn } from 'src/components/Table/types.d' -import { formatAmount } from 'src/logic/tokens/utils/formatAmount' import { INCOMING_TX_TYPES } from 'src/logic/safe/store/models/incomingTransaction' -import { Transaction } from 'src/logic/safe/store/models/types/transaction' +import { Transaction, TransactionTypes, SafeModuleTransaction } from 'src/logic/safe/store/models/types/transaction' import { CancellationTransactions } from 'src/logic/safe/store/reducer/cancellationTransactions' +import { formatAmount } from 'src/logic/tokens/utils/formatAmount' export const TX_TABLE_ID = 'id' export const TX_TABLE_TYPE_ID = 'type' @@ -71,6 +71,27 @@ export const getTxAmount = (tx: Transaction, formatted = true): string => { return getAmountWithSymbol({ decimals: decimals as string, symbol: symbol as string, value }, formatted) } +export const getModuleAmount = (tx: SafeModuleTransaction, formatted = true): string => { + if (tx.type === TransactionTypes.SPENDING_LIMIT && tx.tokenInfo) { + const { decimals, symbol } = tx.tokenInfo + + let value + + if (tx.dataDecoded) { + // if `dataDecoded` is defined, then it's a token transfer + const [, amount] = tx.dataDecoded.parameters + value = amount.value + } else { + // if `dataDecoded` is not defined, then it's an ETH transfer + value = tx.value + } + + return getAmountWithSymbol({ decimals, symbol, value }, formatted) + } + + return NOT_AVAILABLE +} + export interface TableData { amount: string cancelTx?: Transaction @@ -78,45 +99,67 @@ export interface TableData { dateOrder?: number id: string status: string - tx?: Transaction + tx: Transaction | SafeModuleTransaction type: any } +const getModuleTxTableData = (tx: SafeModuleTransaction): TableData => ({ + [TX_TABLE_ID]: tx.blockNumber?.toString() ?? '', + [TX_TABLE_TYPE_ID]: , + [TX_TABLE_DATE_ID]: formatDate(tx.executionDate), + [buildOrderFieldFrom(TX_TABLE_DATE_ID)]: getTime(parseISO(tx.executionDate)), + [TX_TABLE_AMOUNT_ID]: getModuleAmount(tx), + [TX_TABLE_STATUS_ID]: tx.status, + [TX_TABLE_RAW_TX_ID]: tx, +}) + const getIncomingTxTableData = (tx: Transaction): TableData => ({ [TX_TABLE_ID]: tx.blockNumber?.toString() ?? '', [TX_TABLE_TYPE_ID]: , [TX_TABLE_DATE_ID]: formatDate(tx.executionDate || '0'), [buildOrderFieldFrom(TX_TABLE_DATE_ID)]: getTime(parseISO(tx.executionDate || '0')), [TX_TABLE_AMOUNT_ID]: getIncomingTxAmount(tx), - [TX_TABLE_STATUS_ID]: tx.status, + [TX_TABLE_STATUS_ID]: tx.status ?? '', [TX_TABLE_RAW_TX_ID]: tx, }) const getTransactionTableData = (tx: Transaction, cancelTx?: Transaction): TableData => { const txDate = tx.submissionDate + // given that setAllowance will always be part of + // a spendingLimit related tx (as of now, until removeDelegate is implemented) + // we can use this method to identify an SpendingLimit tx + const setAllowanceHash = 'beaeb388' + const txType = tx.data?.includes(setAllowanceHash) ? TransactionTypes.SPENDING_LIMIT : tx.type return { [TX_TABLE_ID]: tx.blockNumber?.toString() ?? '', - [TX_TABLE_TYPE_ID]: , + [TX_TABLE_TYPE_ID]: , [TX_TABLE_DATE_ID]: txDate ? formatDate(txDate) : '', [buildOrderFieldFrom(TX_TABLE_DATE_ID)]: txDate ? getTime(parseISO(txDate)) : null, [TX_TABLE_AMOUNT_ID]: getTxAmount(tx), - [TX_TABLE_STATUS_ID]: tx.status, + [TX_TABLE_STATUS_ID]: tx.status ?? '', [TX_TABLE_RAW_TX_ID]: tx, [TX_TABLE_RAW_CANCEL_TX_ID]: cancelTx, } } export const getTxTableData = ( - transactions: List, + transactions: List, cancelTxs: CancellationTransactions, ): List => { return transactions.map((tx) => { - if (INCOMING_TX_TYPES[tx.type] !== undefined) { - return getIncomingTxTableData(tx) + const isModuleTx = [TransactionTypes.SPENDING_LIMIT, TransactionTypes.MODULE].includes(tx.type) + const isIncomingTx = INCOMING_TX_TYPES[tx.type] !== undefined + + if (isModuleTx) { + return getModuleTxTableData(tx as SafeModuleTransaction) + } + + if (isIncomingTx) { + return getIncomingTxTableData(tx as Transaction) } - return getTransactionTableData(tx, cancelTxs.get(`${tx.nonce}`)) + return getTransactionTableData(tx as Transaction, cancelTxs.get(`${tx.nonce}`)) }) } diff --git a/src/routes/safe/components/Transactions/TxsTable/index.tsx b/src/routes/safe/components/Transactions/TxsTable/index.tsx index 522c277a08..4cd4e337a2 100644 --- a/src/routes/safe/components/Transactions/TxsTable/index.tsx +++ b/src/routes/safe/components/Transactions/TxsTable/index.tsx @@ -1,28 +1,28 @@ import Collapse from '@material-ui/core/Collapse' import IconButton from '@material-ui/core/IconButton' +import { withStyles } from '@material-ui/core/styles' import TableCell from '@material-ui/core/TableCell' import TableContainer from '@material-ui/core/TableContainer' import TableRow from '@material-ui/core/TableRow' -import { withStyles } from '@material-ui/core/styles' import ExpandLess from '@material-ui/icons/ExpandLess' import ExpandMore from '@material-ui/icons/ExpandMore' import cn from 'classnames' import React, { useState, useEffect } from 'react' import { useSelector } from 'react-redux' -import ExpandedTxComponent from './ExpandedTx' -import Status from './Status' -import { TX_TABLE_ID, TX_TABLE_RAW_CANCEL_TX_ID, TX_TABLE_RAW_TX_ID, generateColumns, getTxTableData } from './columns' -import { styles } from './style' - -import Table from 'src/components/Table' -import { cellWidth } from 'src/components/Table/TableHead' import Block from 'src/components/layout/Block' import Row from 'src/components/layout/Row' +import Table from 'src/components/Table' +import { cellWidth } from 'src/components/Table/TableHead' import { safeCancellationTransactionsSelector } from 'src/logic/safe/store/selectors' import { extendedTransactionsSelector } from 'src/logic/safe/store/selectors/transactions' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' +import { generateColumns, getTxTableData, TX_TABLE_ID } from './columns' +import ExpandedTxRow from './ExpandedTxRow' +import Status from './Status' +import { styles } from './style' + export const TRANSACTION_ROW_TEST_ID = 'transaction-row' const TxsTable = ({ classes }) => { @@ -44,8 +44,8 @@ const TxsTable = ({ classes }) => { const filteredData = getTxTableData(transactions, cancellationTransactions) .sort((tx1, tx2) => { // First order by nonce - const aNonce = tx1.tx?.nonce - const bNonce = tx1.tx?.nonce + const aNonce = Number(tx1.tx?.nonce) + const bNonce = Number(tx2.tx?.nonce) if (aNonce && bNonce) { const difference = aNonce - bNonce if (difference !== 0) { @@ -82,8 +82,8 @@ const TxsTable = ({ classes }) => { size={filteredData.size} > {(sortedData) => - sortedData.map((row, index) => ( - + sortedData.map((row) => ( + { style={{ paddingBottom: 0, paddingTop: 0 }} > ( - - )} + component={() => } in={expandedTx === row.tx.safeTxHash} timeout="auto" unmountOnExit diff --git a/src/routes/safe/store/models/types/transactions.d.ts b/src/routes/safe/store/models/types/transactions.d.ts deleted file mode 100644 index 0ee072822f..0000000000 --- a/src/routes/safe/store/models/types/transactions.d.ts +++ /dev/null @@ -1,255 +0,0 @@ -// TODO this file is duplicated with src/logic/safe/store/model/types/transaction.d.ts, we should remove it -export enum TxConstants { - MULTI_SEND = 'multiSend', - UNKNOWN = 'UNKNOWN', -} - -export enum Operation { - CALL, - DELEGATE_CALL, - CREATE, -} - -// types comes from: https://github.com/gnosis/safe-client-gateway/blob/752e76b6d1d475791dbd7917b174bb41d2d9d8be/src/utils.rs -export enum TransferMethods { - TRANSFER = 'transfer', - TRANSFER_FROM = 'transferFrom', - SAFE_TRANSFER_FROM = 'safeTransferFrom', -} - -export enum SettingsChangeMethods { - SETUP = 'setup', - SET_FALLBACK_HANDLER = 'setFallbackHandler', - ADD_OWNER_WITH_THRESHOLD = 'addOwnerWithThreshold', - REMOVE_OWNER = 'removeOwner', - REMOVE_OWNER_WITH_THRESHOLD = 'removeOwnerWithThreshold', - SWAP_OWNER = 'swapOwner', - CHANGE_THRESHOLD = 'changeThreshold', - CHANGE_MASTER_COPY = 'changeMasterCopy', - ENABLE_MODULE = 'enableModule', - DISABLE_MODULE = 'disableModule', - EXEC_TRANSACTION_FROM_MODULE = 'execTransactionFromModule', - APPROVE_HASH = 'approveHash', - EXEC_TRANSACTION = 'execTransaction', -} - -// note: this extends SAFE_METHODS_NAMES in /logic/contracts/methodIds.ts, we need to figure out which one we are going to use -export type DataDecodedMethod = TransferMethods | SettingsChangeMethods | string - -export interface ValueDecoded { - operation: Operation - to: string - value: number - data: string - dataDecoded: DataDecoded -} - -export interface SingleTransactionMethodParameter { - name: string - type: string - value: string -} - -export interface MultiSendMethodParameter extends SingleTransactionMethodParameter { - valueDecoded: ValueDecoded[] -} - -export type Parameter = MultiSendMethodParameter | SingleTransactionMethodParameter - -export interface DataDecoded { - method: DataDecodedMethod - parameters: Parameter[] -} - -export enum ConfirmationType { - CONFIRMATION = 'CONFIRMATION', - EXECUTION = 'EXECUTION', -} - -export enum SignatureType { - CONTRACT_SIGNATURE = 'CONTRACT_SIGNATURE', - APPROVED_HASH = 'APPROVED_HASH', - EOA = 'EOA', - ETH_SIGN = 'ETH_SIGN', -} - -export interface Confirmation { - owner: string - submissionDate: string - transactionHash: string | null - confirmationType: ConfirmationType - signature: string - signatureType: SignatureType -} - -export enum TokenType { - ERC20 = 'ERC20', - ERC721 = 'ERC721', - OTHER = 'OTHER', -} - -export interface TokenInfo { - type: TokenType - address: string - name: string - symbol: string - decimals: number - logoUri: string -} - -export enum TransferType { - ETHER_TRANSFER = 'ETHER_TRANSFER', - ERC20_TRANSFER = 'ERC20_TRANSFER', - ERC721_TRANSFER = 'ERC721_TRANSFER', - UNKNOWN = 'UNKNOWN', -} - -export interface Transfer { - type: TransferType - executionDate: string - blockNumber: number - transactionHash: string | null - to: string - value: string | null - tokenId: string | null - tokenAddress: string - tokenInfo: TokenInfo | null - from: string -} - -export enum TxType { - MULTISIG_TRANSACTION = 'MULTISIG_TRANSACTION', - ETHEREUM_TRANSACTION = 'ETHEREUM_TRANSACTION', - MODULE_TRANSACTION = 'MODULE_TRANSACTION', -} - -export interface MultiSigTransaction { - safe: string - to: string - value: string - data: string | null - operation: number - gasToken: string - safeTxGas: number - baseGas: number - gasPrice: string - refundReceiver: string - nonce: number - executionDate: string | null - submissionDate: string - modified: string - blockNumber: number | null - transactionHash: string | null - safeTxHash: string - executor: string | null - isExecuted: boolean - isSuccessful: boolean | null - ethGasPrice: string | null - gasUsed: number | null - fee: string | null - origin: string | null - dataDecoded: DataDecoded | null - confirmationsRequired: number | null - confirmations: Confirmation[] - signatures: string | null - transfers: Transfer[] - txType: TxType.MULTISIG_TRANSACTION -} - -export interface ModuleTransaction { - created: string - executionDate: string - blockNumber: number - transactionHash: string - safe: string - module: string - to: string - value: string - data: string - operation: Operation - transfers: Transfer[] - txType: TxType.MODULE_TRANSACTION -} - -export interface EthereumTransaction { - executionDate: string - to: string - data: string | null - txHash: string - blockNumber: number - transfers: Transfer[] - txType: TxType.ETHEREUM_TRANSACTION - from: string -} - -export type Transaction = MultiSigTransaction | ModuleTransaction | EthereumTransaction - -// SAFE METHODS TO ITS ID -// https://github.com/gnosis/safe-contracts/blob/development/test/safeMethodNaming.js -// https://github.com/gnosis/safe-contracts/blob/development/contracts/GnosisSafe.sol -// [ -// { name: "addOwnerWithThreshold", id: "0x0d582f13" }, -// { name: "DOMAIN_SEPARATOR_TYPEHASH", id: "0x1db61b54" }, -// { name: "isOwner", id: "0x2f54bf6e" }, -// { name: "execTransactionFromModule", id: "0x468721a7" }, -// { name: "signedMessages", id: "0x5ae6bd37" }, -// { name: "enableModule", id: "0x610b5925" }, -// { name: "changeThreshold", id: "0x694e80c3" }, -// { name: "approvedHashes", id: "0x7d832974" }, -// { name: "changeMasterCopy", id: "0x7de7edef" }, -// { name: "SENTINEL_MODULES", id: "0x85e332cd" }, -// { name: "SENTINEL_OWNERS", id: "0x8cff6355" }, -// { name: "getOwners", id: "0xa0e67e2b" }, -// { name: "NAME", id: "0xa3f4df7e" }, -// { name: "nonce", id: "0xaffed0e0" }, -// { name: "getModules", id: "0xb2494df3" }, -// { name: "SAFE_MSG_TYPEHASH", id: "0xc0856ffc" }, -// { name: "SAFE_TX_TYPEHASH", id: "0xccafc387" }, -// { name: "disableModule", id: "0xe009cfde" }, -// { name: "swapOwner", id: "0xe318b52b" }, -// { name: "getThreshold", id: "0xe75235b8" }, -// { name: "domainSeparator", id: "0xf698da25" }, -// { name: "removeOwner", id: "0xf8dc5dd9" }, -// { name: "VERSION", id: "0xffa1ad74" }, -// { name: "setup", id: "0xa97ab18a" }, -// { name: "execTransaction", id: "0x6a761202" }, -// { name: "requiredTxGas", id: "0xc4ca3a9c" }, -// { name: "approveHash", id: "0xd4d9bdcd" }, -// { name: "signMessage", id: "0x85a5affe" }, -// { name: "isValidSignature", id: "0x20c13b0b" }, -// { name: "getMessageHash", id: "0x0a1028c4" }, -// { name: "encodeTransactionData", id: "0xe86637db" }, -// { name: "getTransactionHash", id: "0xd8d11f78" } -// ] - -export const SAFE_METHODS_NAMES = { - ADD_OWNER_WITH_THRESHOLD: 'addOwnerWithThreshold', - CHANGE_THRESHOLD: 'changeThreshold', - REMOVE_OWNER: 'removeOwner', - SWAP_OWNER: 'swapOwner', - ENABLE_MODULE: 'enableModule', - DISABLE_MODULE: 'disableModule', -} - -export const METHOD_TO_ID = { - '0xe318b52b': SAFE_METHODS_NAMES.SWAP_OWNER, - '0x0d582f13': SAFE_METHODS_NAMES.ADD_OWNER_WITH_THRESHOLD, - '0xf8dc5dd9': SAFE_METHODS_NAMES.REMOVE_OWNER, - '0x694e80c3': SAFE_METHODS_NAMES.CHANGE_THRESHOLD, - '0x610b5925': SAFE_METHODS_NAMES.ENABLE_MODULE, - '0xe009cfde': SAFE_METHODS_NAMES.DISABLE_MODULE, -} - -export type SafeMethods = typeof SAFE_METHODS_NAMES[keyof typeof SAFE_METHODS_NAMES] - -type TokenMethods = 'transfer' | 'transferFrom' | 'safeTransferFrom' - -type SafeDecodedParams = { - [key in SafeMethods]?: Record -} - -type TokenDecodedParams = { - [key in TokenMethods]?: Record -} - -export type DecodedParams = SafeDecodedParams | TokenDecodedParams | null diff --git a/src/store/index.ts b/src/store/index.ts index 8fc2340868..e3dc4491c9 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -38,6 +38,10 @@ import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/redu import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea' import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe' import allTransactions, { TRANSACTIONS, TransactionsState } from '../logic/safe/store/reducer/allTransactions' +import moduleTransactions, { + MODULE_TRANSACTIONS_REDUCER_ID, + ModuleTransactionsState, +} from 'src/logic/safe/store/reducer/moduleTransactions' export const history = createHashHistory() @@ -65,6 +69,7 @@ const reducers = combineReducers({ [TRANSACTIONS_REDUCER_ID]: transactions, [CANCELLATION_TRANSACTIONS_REDUCER_ID]: cancellationTransactions, [INCOMING_TRANSACTIONS_REDUCER_ID]: incomingTransactions, + [MODULE_TRANSACTIONS_REDUCER_ID]: moduleTransactions, [NOTIFICATIONS_REDUCER_ID]: notifications, [CURRENCY_VALUES_KEY]: currencyValues, [COOKIES_REDUCER_ID]: cookies, @@ -82,6 +87,7 @@ export type AppReduxState = CombinedState<{ [TRANSACTIONS_REDUCER_ID]: Map [CANCELLATION_TRANSACTIONS_REDUCER_ID]: CancellationTxState [INCOMING_TRANSACTIONS_REDUCER_ID]: Map + [MODULE_TRANSACTIONS_REDUCER_ID]: ModuleTransactionsState [NOTIFICATIONS_REDUCER_ID]: Map [CURRENCY_VALUES_KEY]: CurrencyValuesState [COOKIES_REDUCER_ID]: Map diff --git a/src/test/safe.dom.funds.thresholdGt1.ts b/src/test/safe.dom.funds.thresholdGt1.ts index 7331aba493..9dbc3004b6 100644 --- a/src/test/safe.dom.funds.thresholdGt1.ts +++ b/src/test/safe.dom.funds.thresholdGt1.ts @@ -1,4 +1,4 @@ -// +// import { fireEvent, waitForElement } from '@testing-library/react' import { aNewStore } from 'src/store' import { aMinedSafe } from 'src/test/builder/safe.redux.builder' @@ -13,7 +13,7 @@ import { TRANSACTIONS_TAB_BTN_TEST_ID } from 'src/routes/safe/container' import { TRANSACTION_ROW_TEST_ID } from 'src/routes/safe/components/Transactions/TxsTable' import { useTestAccountAt, resetTestAccount } from './utils/accounts' //import { CONFIRM_TX_BTN_TEST_ID } from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/OwnersColumn/ButtonRow' -import { APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID } from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal' +import { APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID } from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/ApproveTxModal' afterEach(resetTestAccount) diff --git a/src/test/utils/transactions/transactionList.helper.ts b/src/test/utils/transactions/transactionList.helper.ts index 0483c950b1..6eff55b735 100644 --- a/src/test/utils/transactions/transactionList.helper.ts +++ b/src/test/utils/transactions/transactionList.helper.ts @@ -5,11 +5,11 @@ import { TRANSACTIONS_TAB_BTN_TEST_ID } from 'src/routes/safe/container' import { TRANSACTION_ROW_TEST_ID } from 'src/routes/safe/components/Transactions/TxsTable' import { TRANSACTIONS_DESC_SEND_TEST_ID, -} from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription' +} from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription' import { TRANSACTIONS_DESC_ADD_OWNER_TEST_ID, TRANSACTIONS_DESC_REMOVE_OWNER_TEST_ID, -} from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTx/TxDescription/SettingsDescription' +} from 'src/routes/safe/components/Transactions/TxsTable/ExpandedTxRow/TxDescription/SettingsDescription' export const getLastTransaction = async (SafeDom) => { // Travel to transactions