Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

feat: filter historical transactions #3870

Merged
merged 34 commits into from
Jun 15, 2022
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f0875cf
fix: update gateway sdk + add filter to `search`
iamacook May 9, 2022
9109594
fix: type collision
iamacook May 10, 2022
54f063f
Merge branch 'filter-endpoints' of github.com:gnosis/safe-react into …
iamacook May 10, 2022
a861400
fix: fetch + show filtered results
iamacook May 10, 2022
4535e76
fix: use address for module
iamacook May 10, 2022
4f7fec7
fix: fetches after filtering + history pointers
iamacook May 10, 2022
4cf1557
fix: fetching filtered endpoints
iamacook May 10, 2022
4cc6b92
fix: cleanup code
iamacook May 11, 2022
9c5378d
fix: repair lock file + add stricter typing
iamacook May 12, 2022
724c1ef
fix: drill filter + separate paging logic
iamacook May 12, 2022
b6a9550
fix: add tests
iamacook May 12, 2022
03f0ddc
fix: only save certain query params
iamacook May 12, 2022
eb7ba6c
fix: filters and layout
iamacook May 18, 2022
14816ca
Merge branch 'tx-filtering' into filter-endpoints
iamacook May 18, 2022
2e12ba1
fix: use native searchParams + add `executed`
iamacook May 18, 2022
d2e1cf1
fix: remove dev button, upgrade sdk + tests
iamacook May 19, 2022
6a88189
fix: don't append second ampersand
iamacook May 20, 2022
4ae2a50
fix: persist addresses
iamacook May 21, 2022
3f3cef9
fix: add no results message
iamacook May 21, 2022
8f4a713
fix: remove date filtering + add tracking
iamacook Jun 7, 2022
a7be388
Merge branch 'dev' into filter-endpoints
iamacook Jun 7, 2022
bd0b068
fix: display date filters as coming soon
iamacook Jun 7, 2022
83f94ea
Merge branch 'tx-filtering' into filter-endpoints
iamacook Jun 7, 2022
c9a7ba9
fix: don't apply the same filter twice + cleanup
iamacook Jun 7, 2022
450d61a
fix: `isTxFilter`, catch URL + remove hidden field
iamacook Jun 7, 2022
a4d2029
fix: only check a subset of keys
iamacook Jun 7, 2022
cdbcbdc
fix: change icon
iamacook Jun 8, 2022
4ca2d77
fix: add `box-shadow` + filter type to button
iamacook Jun 8, 2022
7b78db1
fix: change button style
iamacook Jun 8, 2022
204e752
fix: remove unnecessary flag + fix type
iamacook Jun 9, 2022
e896286
fix: remove unnecessary pointer
iamacook Jun 9, 2022
2175044
fix: convert `value to wei + always show clear
iamacook Jun 14, 2022
5c5f9d9
fix: test
iamacook Jun 14, 2022
966342b
fix: remove `recipient` from incoming filter
iamacook Jun 15, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"@openzeppelin/contracts": "4.4.2",
"@sentry/react": "^6.10.0",
"@sentry/tracing": "^6.10.0",
"@truffle/hdwallet-provider": "^2.0.8",
"@unstoppabledomains/resolution": "^1.17.0",
"abi-decoder": "^2.4.0",
"axios": "0.21.4",
Expand Down Expand Up @@ -139,8 +140,7 @@
"web3": "1.7.0",
"web3-core": "^1.7.0",
"web3-eth-contract": "^1.7.0",
"web3-utils": "^1.7.0",
"@truffle/hdwallet-provider": "^2.0.8"
"web3-utils": "^1.7.0"
},
"devDependencies": {
"@gnosis.pm/safe-core-sdk-types": "1.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { ThunkDispatch } from 'redux-thunk'
import { AppReduxState } from 'src/store'
import { AvailableCurrenciesPayload } from 'src/logic/currencyValues/store/reducer/currencyValues'
import { setAvailableCurrencies } from 'src/logic/currencyValues/store/actions/setAvailableCurrencies'
import { Errors, logError } from 'src/logic/exceptions/CodedException'
import { getFiatCurrencies } from '@gnosis.pm/safe-react-gateway-sdk'
import { Errors, logError } from 'src/logic/exceptions/CodedException'

export const updateAvailableCurrencies =
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,30 @@ import {
} from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
import { loadHistoryTransactions, loadQueuedTransactions } from './loadGatewayTransactions'
import { AppReduxState } from 'src/store'
import { history } from 'src/routes/routes'
import { isTxFilter } from 'src/routes/safe/components/Transactions/TxList/Filter/utils'

export default (chainId: string, safeAddress: string) =>
async (dispatch: ThunkDispatch<AppReduxState, undefined, AnyAction>): Promise<void> => {
const loadTxs = async (
loadFn: typeof loadHistoryTransactions | typeof loadQueuedTransactions,
actionFn: typeof addHistoryTransactions | typeof addQueuedTransactions,
) => {
const loadHistory = async () => {
try {
const values = (await loadFn(safeAddress)) as any[]
dispatch(actionFn({ chainId, safeAddress, values }))
const query = Object.fromEntries(new URLSearchParams(history.location.search))
const filter = isTxFilter(query) ? query : undefined
const values = await loadHistoryTransactions(safeAddress, filter)
dispatch(addHistoryTransactions({ chainId, safeAddress, values }))
} catch (e) {
e.log()
}
}

await Promise.all([
loadTxs(loadHistoryTransactions, addHistoryTransactions),
loadTxs(loadQueuedTransactions, addQueuedTransactions),
])
const loadQueue = async () => {
try {
const values = await loadQueuedTransactions(safeAddress)
dispatch(addQueuedTransactions({ chainId, safeAddress, values }))
} catch (e) {
e.log()
}
}

await Promise.all([loadHistory(), loadQueue()])
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,100 @@
import { getTransactionHistory, getTransactionQueue } from '@gnosis.pm/safe-react-gateway-sdk'
import {
getTransactionHistory,
getTransactionQueue,
TransactionListPage,
getIncomingTransfers,
getMultisigTransactions,
getModuleTransactions,
} from '@gnosis.pm/safe-react-gateway-sdk'
import { _getChainId } from 'src/config'
import { HistoryGatewayResponse, QueuedGatewayResponse } from 'src/logic/safe/store/models/types/gateway.d'
import { checksumAddress } from 'src/utils/checksumAddress'
import { Errors, CodedException } from 'src/logic/exceptions/CodedException'
import { FilterForm, FilterType, FILTER_TYPE_FIELD_NAME } from 'src/routes/safe/components/Transactions/TxList/Filter'
import {
getIncomingFilter,
getMultisigFilter,
getModuleFilter,
} from 'src/routes/safe/components/Transactions/TxList/Filter/utils'
import { ChainId } from 'src/config/chain.d'
import { operations } from '@gnosis.pm/safe-react-gateway-sdk/dist/types/api'

/*************/
/* HISTORY */
/*************/
const historyPointers: { [chainId: string]: { [safeAddress: string]: { next?: string; previous?: string } } } = {}
const historyPointers: {
[chainId: string]: {
[safeAddress: string]: {
next?: string
previous?: string
filterType?: FilterType
}
}
} = {}

const getHistoryTxListPage = async (
chainId: ChainId,
safeAddress: string,
filter?: FilterForm | Partial<FilterForm>,
): Promise<TransactionListPage> => {
let txListPage: TransactionListPage = {
next: undefined,
previous: undefined,
results: [],
}
usame-algan marked this conversation as resolved.
Show resolved Hide resolved

const { next, filterType } = historyPointers[chainId]?.[safeAddress] || {}

let query:
| operations['incoming_transfers' | 'incoming_transfers' | 'module_transactions']['parameters']['query']
| undefined

switch (filterType) {
case FilterType.INCOMING: {
query = filter ? getIncomingFilter(filter) : undefined
txListPage = await getIncomingTransfers(chainId, safeAddress, query, next)
break
}
case FilterType.MULTISIG: {
query = filter ? getMultisigFilter(filter, true) : undefined
txListPage = await getMultisigTransactions(chainId, safeAddress, query, next)
break
}
case FilterType.MODULE: {
query = filter ? getModuleFilter(filter) : undefined
txListPage = await getModuleTransactions(chainId, safeAddress, query, next)
break
}
default: {
txListPage = await getTransactionHistory(chainId, safeAddress, next)
}
}

const getPageUrl = (pageUrl?: string): string | undefined => {
if (!pageUrl || !filterType || !query) {
return pageUrl
}

let url: URL

try {
url = new URL(pageUrl)
} catch {
return pageUrl
}

Object.entries(query).forEach(([key, value]) => {
url.searchParams.set(key, value)
})

return url.toString()
}

historyPointers[chainId][safeAddress].next = getPageUrl(txListPage?.next)
historyPointers[chainId][safeAddress].previous = getPageUrl(txListPage?.previous)
usame-algan marked this conversation as resolved.
Show resolved Hide resolved

return txListPage
}

/**
* Fetch next page if there is a next pointer for the safeAddress.
Expand All @@ -18,39 +105,41 @@ export const loadPagedHistoryTransactions = async (
safeAddress: string,
): Promise<{ values: HistoryGatewayResponse['results']; next?: string } | undefined> => {
const chainId = _getChainId()
// if `historyPointers[safeAddress] is `undefined` it means `loadHistoryTransactions` wasn't called
// if `historyPointers[safeAddress].next is `null`, it means it reached the last page in gateway-client
if (!historyPointers[chainId][safeAddress]?.next) {

if (!historyPointers[chainId]?.[safeAddress]?.next) {
throw new CodedException(Errors._608)
}

try {
const { results, next, previous } = await getTransactionHistory(
chainId,
checksumAddress(safeAddress),
historyPointers[chainId][safeAddress].next,
)

historyPointers[chainId][safeAddress] = { next, previous }
const { results, next } = await getHistoryTxListPage(chainId, safeAddress)

return { values: results, next: historyPointers[chainId][safeAddress].next }
return { values: results, next }
} catch (e) {
throw new CodedException(Errors._602, e.message)
}
}

export const loadHistoryTransactions = async (safeAddress: string): Promise<HistoryGatewayResponse['results']> => {
export const loadHistoryTransactions = async (
safeAddress: string,
filter?: FilterForm | Partial<FilterForm>,
): Promise<HistoryGatewayResponse['results']> => {
const chainId = _getChainId()
try {
const { results, next, previous } = await getTransactionHistory(chainId, checksumAddress(safeAddress))

if (!historyPointers[chainId]) {
historyPointers[chainId] = {}
}
if (!historyPointers[chainId]) {
historyPointers[chainId] = {}
}

if (!historyPointers[chainId][safeAddress]) {
historyPointers[chainId][safeAddress] = { next, previous }
const isNewFilter = filter?.type !== historyPointers?.[chainId]?.[safeAddress]?.filterType
if (!historyPointers[chainId][safeAddress] || isNewFilter) {
historyPointers[chainId][safeAddress] = {
next: undefined,
previous: undefined,
filterType: filter?.[FILTER_TYPE_FIELD_NAME],
}
}

try {
const { results } = await getHistoryTxListPage(chainId, safeAddress, filter)

return results
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { createAction } from 'redux-actions'

import { HistoryPayload, QueuedPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'
import { HistoryPayload, QueuedPayload, RemoveHistoryPayload } from 'src/logic/safe/store/reducer/gatewayTransactions'

export const ADD_HISTORY_TRANSACTIONS = 'ADD_HISTORY_TRANSACTIONS'
export const addHistoryTransactions = createAction<HistoryPayload>(ADD_HISTORY_TRANSACTIONS)

export const REMOVE_HISTORY_TRANSACTIONS = 'REMOVE_HISTORY_TRANSACTIONS'
DiogoSoaress marked this conversation as resolved.
Show resolved Hide resolved
export const removeHistoryTransactions = createAction<RemoveHistoryPayload>(REMOVE_HISTORY_TRANSACTIONS)

export const ADD_QUEUED_TRANSACTIONS = 'ADD_QUEUED_TRANSACTIONS'
export const addQueuedTransactions = createAction<QueuedPayload>(ADD_QUEUED_TRANSACTIONS)
18 changes: 17 additions & 1 deletion src/logic/safe/store/reducer/gatewayTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LabelValue } from '@gnosis.pm/safe-react-gateway-sdk'
import {
ADD_HISTORY_TRANSACTIONS,
ADD_QUEUED_TRANSACTIONS,
REMOVE_HISTORY_TRANSACTIONS,
} from 'src/logic/safe/store/actions/transactions/gatewayTransactions'
import {
HistoryGatewayResponse,
Expand All @@ -31,6 +32,8 @@ type BasePayload = { chainId: string; safeAddress: string; isTail?: boolean }

export type HistoryPayload = BasePayload & { values: HistoryGatewayResponse['results'] }

export type RemoveHistoryPayload = Omit<BasePayload, 'isTail'>

export type QueuedPayload = BasePayload & { values: QueuedGatewayResponse['results'] }

export type TransactionDetailsPayload = {
Expand All @@ -40,7 +43,7 @@ export type TransactionDetailsPayload = {
value: Transaction['txDetails']
}

type Payload = HistoryPayload | QueuedPayload | TransactionDetailsPayload
type Payload = HistoryPayload | RemoveHistoryPayload | QueuedPayload | TransactionDetailsPayload

/**
* Create a hash map of transactions by nonce.
Expand Down Expand Up @@ -106,6 +109,19 @@ export const gatewayTransactionsReducer = handleActions<GatewayTransactionsState
}
},

[REMOVE_HISTORY_TRANSACTIONS]: (state, action: Action<{ chainId: string; safeAddress: string }>) => {
const { chainId, safeAddress } = action.payload
return {
...state,
[chainId]: {
[safeAddress]: {
...state[chainId]?.[safeAddress],
history: {},
},
},
}
},

// Queue is overwritten completely on every update
// CGW sends a list of items where some items are LABELS (next and queued),
// some are CONFLICT_HEADERS (ignored),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { fireEvent, render, screen, getByText, waitFor, queryByText } from 'src/
import { CurrencyDropdown } from '.'
import { history, ROOT_ROUTE } from 'src/routes/routes'
import { mockedEndpoints } from 'src/setupTests'
import {} from 'src/utils/constants'

const mockedAvailableCurrencies = ['USD', 'EUR', 'AED', 'AFN', 'ALL', 'ARS']
const rinkebyNetworkId = '4'
Expand Down
Loading