diff --git a/.changelog/990.feature.md b/.changelog/990.feature.md
new file mode 100644
index 000000000..9c024abc9
--- /dev/null
+++ b/.changelog/990.feature.md
@@ -0,0 +1 @@
+Show block-level evens in block details
diff --git a/src/app/components/RuntimeEvents/EventListFilterSwitch.tsx b/src/app/components/RuntimeEvents/EventListFilterSwitch.tsx
new file mode 100644
index 000000000..c7ce3f9c8
--- /dev/null
+++ b/src/app/components/RuntimeEvents/EventListFilterSwitch.tsx
@@ -0,0 +1,71 @@
+import { FC } from 'react'
+import Chip from '@mui/material/Chip'
+import CheckIcon from '@mui/icons-material/Check'
+import { useTranslation } from 'react-i18next'
+import { COLORS } from '../../../styles/theme/colors'
+import { TFunction } from 'i18next'
+import Box from '@mui/material/Box'
+
+export const EventFilterMode = {
+ All: 'all',
+ NonTX: 'nonTX',
+} as const
+
+const eventFilterModes: EventFilterMode[] = [EventFilterMode.All, EventFilterMode.NonTX]
+
+const getEventFilterModeName = (t: TFunction, mode: EventFilterMode): string => {
+ switch (mode) {
+ case 'all':
+ return t('runtimeEvent.filter.all')
+ case 'nonTX':
+ return t('runtimeEvent.filter.nonTx')
+ }
+}
+
+const Pill: FC<{ label: string; selected: boolean; onSelect: () => void }> = ({
+ label,
+ selected,
+ onSelect,
+}) => {
+ return (
+ : undefined}
+ label={label}
+ onClick={onSelect}
+ sx={{
+ display: 'flex',
+ height: 30,
+ padding: '3px 10px',
+ alignItems: 'center',
+ borderRadius: 9,
+ border: `1px solid ${COLORS.brandMedium}`,
+ background: selected ? COLORS.brandMedium : COLORS.brandLightBlue,
+ color: selected ? 'white' : 'black',
+ }}
+ />
+ )
+}
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export type EventFilterMode = (typeof EventFilterMode)[keyof typeof EventFilterMode]
+
+type EventFilterSwitchProps = {
+ selected?: EventFilterMode
+ onSelectionChange: (selection: EventFilterMode) => void
+}
+
+export const EventFilterSwitch: FC = ({ selected, onSelectionChange }) => {
+ const { t } = useTranslation()
+ return (
+
+ {eventFilterModes.map(mode => (
+ onSelectionChange(mode)}
+ />
+ ))}
+
+ )
+}
diff --git a/src/app/pages/AccountDetailsPage/AccountEventsCard.tsx b/src/app/pages/AccountDetailsPage/AccountEventsCard.tsx
index a7e048d27..fd148700b 100644
--- a/src/app/pages/AccountDetailsPage/AccountEventsCard.tsx
+++ b/src/app/pages/AccountDetailsPage/AccountEventsCard.tsx
@@ -5,19 +5,32 @@ import { useTranslation } from 'react-i18next'
import { RuntimeEventsDetailedList } from '../../components/RuntimeEvents/RuntimeEventsDetailedList'
import { SearchScope } from '../../../types/searchScope'
import { AddressSwitchOption } from '../../components/AddressSwitch'
+import { EventFilterMode, EventFilterSwitch } from '../../components/RuntimeEvents/EventListFilterSwitch'
type AccountEventProps = {
scope: SearchScope
isLoading: boolean
+ filterMode: EventFilterMode
+ setFilterMode: (mode: EventFilterMode) => void
isError: boolean
events: RuntimeEvent[] | undefined
}
-export const AccountEventsCard: FC = ({ scope, isLoading, isError, events }) => {
+export const AccountEventsCard: FC = ({
+ scope,
+ isLoading,
+ filterMode,
+ setFilterMode,
+ isError,
+ events,
+}) => {
const { t } = useTranslation()
return (
-
+ }
+ >
{
const { network, layer } = scope
@@ -60,7 +62,7 @@ export const useAccountTransactions = (scope: SearchScope, address: string) => {
}
}
-export const useAccountEvents = (scope: SearchScope, address: string) => {
+export const useAccountEvents = (scope: SearchScope, address: string, filterMode: EventFilterMode) => {
const { network, layer } = scope
if (layer === Layer.consensus) {
throw AppErrors.UnsupportedLayer
@@ -72,6 +74,8 @@ export const useAccountEvents = (scope: SearchScope, address: string) => {
// TODO: implement filtering for non-transactional events
})
const { isFetched, isLoading, isError, data } = query
- const events = data?.data.events
+ const events = data?.data.events.filter(
+ event => filterMode === EventFilterMode.All || event.type !== RuntimeEventType.accountstransfer,
+ )
return { isFetched, isLoading, isError, events }
}
diff --git a/src/app/pages/AccountDetailsPage/index.tsx b/src/app/pages/AccountDetailsPage/index.tsx
index b442c5d0c..67ef94525 100644
--- a/src/app/pages/AccountDetailsPage/index.tsx
+++ b/src/app/pages/AccountDetailsPage/index.tsx
@@ -1,4 +1,4 @@
-import { FC } from 'react'
+import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useHref, useLoaderData, useOutletContext } from 'react-router-dom'
import { PageLayout } from '../../components/PageLayout'
@@ -17,6 +17,7 @@ import { getTokenTypePluralName } from '../../../types/tokens'
import { SearchScope } from '../../../types/searchScope'
import { AccountDetailsCard } from './AccountDetailsCard'
import { AccountEventsCard } from './AccountEventsCard'
+import { EventFilterMode } from '../../components/RuntimeEvents/EventListFilterSwitch'
export type AccountDetailsContext = {
scope: SearchScope
@@ -35,8 +36,13 @@ export const AccountDetailsPage: FC = () => {
const { token, isLoading: isTokenLoading } = useTokenInfo(scope, address, isContract)
const tokenPriceInfo = useTokenPrice(account?.ticker || Ticker.ROSE)
+ const [eventFilterMode, setEventFilterMode] = useState(EventFilterMode.All)
- const { isLoading: areEventsLoading, isError: isEventsError, events } = useAccountEvents(scope, address)
+ const {
+ isLoading: areEventsLoading,
+ isError: isEventsError,
+ events,
+ } = useAccountEvents(scope, address, eventFilterMode)
const tokenTransfersLink = useHref(`token-transfers#${accountTokenTransfersContainerId}`)
const erc20Link = useHref(`tokens/erc-20#${accountTokenContainerId}`)
@@ -76,7 +82,14 @@ export const AccountDetailsPage: FC = () => {
]}
context={context}
/>
-
+
)
}
diff --git a/src/app/pages/BlockDetailPage/EventsCard.tsx b/src/app/pages/BlockDetailPage/EventsCard.tsx
new file mode 100644
index 000000000..2f8d1e118
--- /dev/null
+++ b/src/app/pages/BlockDetailPage/EventsCard.tsx
@@ -0,0 +1,82 @@
+import { FC, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { ScrollingCard } from '../../components/PageLayout/ScrollingCard'
+import CardHeader from '@mui/material/CardHeader'
+import CardContent from '@mui/material/CardContent'
+
+import { Layer, RuntimeEventType, useGetRuntimeEvents } from '../../../oasis-nexus/api'
+import { ErrorBoundary } from '../../components/ErrorBoundary'
+import { AppErrors } from '../../../types/errors'
+import { SearchScope } from '../../../types/searchScope'
+import { RuntimeEventsDetailedList } from '../../components/RuntimeEvents/RuntimeEventsDetailedList'
+import { AddressSwitchOption } from '../../components/AddressSwitch'
+import { EventFilterMode, EventFilterSwitch } from '../../components/RuntimeEvents/EventListFilterSwitch'
+import { EmptyState } from '../../components/EmptyState'
+
+export const eventsContainerId = 'events'
+
+const EventsList: FC<{ scope: SearchScope; blockHeight: number; filterMode: EventFilterMode }> = ({
+ scope,
+ blockHeight,
+ filterMode,
+}) => {
+ const { t } = useTranslation()
+ if (scope.layer === Layer.consensus) {
+ // Loading events for consensus blocks is not yet supported.
+ // Should use useGetConsensusEvents()
+ throw AppErrors.UnsupportedLayer
+ }
+ const eventsQuery = useGetRuntimeEvents(scope.network, scope.layer, {
+ block: blockHeight,
+ // TODO: search for tx_hash = null
+ limit: 100, // We want to avoid pagination here, if possible
+ })
+
+ const { isLoading, isError, data } = eventsQuery
+
+ const events = data?.data.events.filter(
+ event =>
+ !event.tx_hash && // TODO: remove filtering here if it's implemented using the query parameters
+ (filterMode === EventFilterMode.All || event.type !== RuntimeEventType.accountstransfer),
+ )
+
+ if (!events?.length && !isLoading) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+export const EventsCard: FC<{ scope: SearchScope; blockHeight: number }> = ({ scope, blockHeight }) => {
+ const [filterMode, setFilterMode] = useState(EventFilterMode.All)
+ const { t } = useTranslation()
+ return (
+
+ }
+ />
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/pages/BlockDetailPage/index.tsx b/src/app/pages/BlockDetailPage/index.tsx
index 19211a05f..230ac226b 100644
--- a/src/app/pages/BlockDetailPage/index.tsx
+++ b/src/app/pages/BlockDetailPage/index.tsx
@@ -18,6 +18,7 @@ import { BlockLink, BlockHashLink } from '../../components/Blocks/BlockLink'
import { RouteUtils } from '../../utils/route-utils'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
import { DashboardLink } from '../ParatimeDashboardPage/DashboardLink'
+import { EventsCard } from './EventsCard'
export const BlockDetailPage: FC = () => {
const { t } = useTranslation()
@@ -43,6 +44,7 @@ export const BlockDetailPage: FC = () => {
{!!block?.num_transactions && }
+
)
}
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 051a958d6..378a986bc 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -286,12 +286,18 @@
},
"runtimeEvent": {
"cantLoadEvents": "Unfortunately we couldn't load the list of events. Please try again later.",
+ "noEvents": "No events",
+ "cantFindMatchingEvents": "We can't find any matching events.",
"accountsburn": "Tokens burnt",
"accountsmint": "Tokens minted",
"accountstransfer": "Transfer",
"consensusDeposit": "Deposit from consensus",
"consensusWithdrawal": "Withdrawal to consensus",
"evmLog": "EVM log message",
+ "filter": {
+ "all": "All events",
+ "nonTx": "Non-transactional"
+ },
"gasUsed": "Gas used",
"fields": {
"amount": "Amount",
diff --git a/src/styles/theme/colors.ts b/src/styles/theme/colors.ts
index 92c0060e6..e2f41b5f4 100644
--- a/src/styles/theme/colors.ts
+++ b/src/styles/theme/colors.ts
@@ -9,6 +9,7 @@ export const COLORS = {
brandExtraDark: '#000062',
brandExtraLight: '#e5e5ef',
brandLight: '#6665d8',
+ brandLightBlue: '#E8F5FF',
brandMedium: '#0092f6',
brandMedium15: '#d9effe',
brightGray2: '#ececec',