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

Commit

Permalink
feat: filter historical transactions (#3870)
Browse files Browse the repository at this point in the history
* fix: update gateway sdk + add filter to `search`

* fix: type collision

* fix: fetch + show filtered results

* fix: use address for module

* fix: fetches after filtering + history pointers

* fix: fetching filtered endpoints

* fix: cleanup code

* fix: repair lock file + add stricter typing

* fix: drill filter + separate paging logic

* fix: add tests

* fix: only save certain query params

* fix: filters and layout

* fix: use native searchParams + add `executed`

* fix: remove dev button, upgrade sdk + tests

* fix: don't append second ampersand

* fix: persist addresses

* fix: add no results message

* fix: remove date filtering + add tracking

* fix: display date filters as coming soon

* fix: don't apply the same filter twice + cleanup

* fix: `isTxFilter`, catch URL + remove hidden field

* fix: only check a subset of keys

* fix: change icon

* fix: add `box-shadow` + filter type to button

* fix: change button style

* fix: remove unnecessary flag + fix type

* fix: remove unnecessary pointer

* fix: convert `value to wei + always show clear

* fix: test

* fix: remove `recipient` from incoming filter
  • Loading branch information
iamacook authored Jun 15, 2022
1 parent 49e7afc commit 5d15f00
Show file tree
Hide file tree
Showing 25 changed files with 679 additions and 368 deletions.
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
7 changes: 3 additions & 4 deletions src/components/InfiniteScroll/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@ InfiniteScrollProvider.displayName = 'InfiniteScrollProvider'

type InfiniteScrollProps = {
children: ReactNode
hasMore: boolean
next: () => Promise<void>
config?: InViewHookResponse
}

export const InfiniteScroll = ({ children, hasMore, next, config }: InfiniteScrollProps): ReactElement => {
export const InfiniteScroll = ({ children, next, config }: InfiniteScrollProps): ReactElement => {
const { ref, inView } = useInView({
threshold: 0,
root: document.querySelector(`#${INFINITE_SCROLL_CONTAINER}`),
Expand All @@ -47,14 +46,14 @@ export const InfiniteScroll = ({ children, hasMore, next, config }: InfiniteScro
// Avoid memory leak - queue/history have separate InfiniteScroll wrappers
let isMounted = true

if (isMounted && inView && hasMore) {
if (isMounted && inView) {
next()
}

return () => {
isMounted = false
}
}, [inView, hasMore, next])
}, [inView, next])

return <InfiniteScrollProvider ref={ref}>{children}</InfiniteScrollProvider>
}
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,99 @@
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
}
}
} = {}

const getHistoryTxListPage = async (
chainId: ChainId,
safeAddress: string,
filter?: FilterForm | Partial<FilterForm>,
): Promise<TransactionListPage> => {
let txListPage: TransactionListPage = {
next: undefined,
previous: undefined,
results: [],
}

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

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

switch (filter?.[FILTER_TYPE_FIELD_NAME]) {
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 || !query) {
return pageUrl
}

let url: URL

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

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

return url.toString()
}

historyPointers[chainId][safeAddress].next = getPageUrl(txListPage?.next)
historyPointers[chainId][safeAddress].previous = getPageUrl(txListPage?.previous)

return txListPage
}

/**
* Fetch next page if there is a next pointer for the safeAddress.
Expand All @@ -18,39 +104,39 @@ 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 }
if (!historyPointers[chainId][safeAddress] || filter) {
historyPointers[chainId][safeAddress] = {
next: undefined,
previous: undefined,
}
}

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'
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

0 comments on commit 5d15f00

Please sign in to comment.