diff --git a/packages/apps/public/locales/en/app-explorer.json b/packages/apps/public/locales/en/app-explorer.json index 1d1f9cbe55..053e6312b0 100644 --- a/packages/apps/public/locales/en/app-explorer.json +++ b/packages/apps/public/locales/en/app-explorer.json @@ -10,6 +10,7 @@ "No justifications available": "No justifications available", "No logs available": "No logs available", "Node info": "Node info", + "Show my events": "Show my events", "Unable to decode the block events. {{error}}": "Unable to decode the block events. {{error}}", "Unable to retrieve the specified block details. {{error}}": "Unable to retrieve the specified block details. {{error}}", "Waiting for next block...": "Waiting for next block...", diff --git a/packages/apps/public/locales/en/translation.json b/packages/apps/public/locales/en/translation.json index 659f8fded6..8e88ad0b9c 100644 --- a/packages/apps/public/locales/en/translation.json +++ b/packages/apps/public/locales/en/translation.json @@ -701,6 +701,7 @@ "Setup account as recoverable": "", "Share": "", "Show address on hardware device": "", + "Show my events": "", "Sign (no submission)": "", "Sign and Submit": "", "Sign and Upload": "", diff --git a/packages/page-explorer/src/BlockInfo/Extrinsics.tsx b/packages/page-explorer/src/BlockInfo/Extrinsics.tsx index bf448990b8..36ef41d8a8 100644 --- a/packages/page-explorer/src/BlockInfo/Extrinsics.tsx +++ b/packages/page-explorer/src/BlockInfo/Extrinsics.tsx @@ -2,12 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types'; +import type { GenericExtrinsic } from '@polkadot/types'; import type { BlockNumber, Extrinsic } from '@polkadot/types/interfaces'; +import type { Vec } from '@polkadot/types-codec'; +import type { AnyTuple } from '@polkadot/types-codec/types'; import type { BN } from '@polkadot/util'; import React, { useMemo } from 'react'; -import { Table } from '@polkadot/react-components'; +import { styled, Table, Toggle } from '@polkadot/react-components'; +import { useAccounts, useToggle } from '@polkadot/react-hooks'; +import { isEventFromMyAccounts } from '@polkadot/react-hooks/utils/isEventFromMyAccounts'; import { useTranslation } from '../translate.js'; import ExtrinsicDisplay from './Extrinsic.js'; @@ -24,17 +29,34 @@ interface Props { function Extrinsics ({ blockNumber, className = '', events, label, maxBlockWeight, value, withLink }: Props): React.ReactElement { const { t } = useTranslation(); + const { allAccounts } = useAccounts(); + const [showOnlyUserEvents, onToggleUserEvents] = useToggle(); const header = useMemo<[React.ReactNode?, string?, number?][]>( () => [ [label || t('extrinsics'), 'start', 2], [t('events'), 'start media--1000', 2], [t('weight'), 'media--1400'], - [undefined, 'address media--1200'] + [ + , + 'end media--1000' + ] ], - [label, t] + [label, onToggleUserEvents, showOnlyUserEvents, t] ); + const filteredEvents = useMemo(() => { + if (!showOnlyUserEvents) { + return events; + } + + return events?.filter((event) => isEventFromMyAccounts(event.record, value as Vec>, undefined, allAccounts)); + }, [allAccounts, events, showOnlyUserEvents, value]); + return ( void; +} + +const EventsToggle = ({ onToggleUserEvents, showOnlyUserEvents }: EventsToggleProps) => { + const { t } = useTranslation(); + + return ( + + + + ); +}; + +const StyledDiv = styled.div` + .ui--Toggle { + label{ + font-size: var(--font-size-base); + } + } +`; + export default React.memo(Extrinsics); diff --git a/packages/page-explorer/src/Events.tsx b/packages/page-explorer/src/Events.tsx index 53b38cdf7b..5e36b18c73 100644 --- a/packages/page-explorer/src/Events.tsx +++ b/packages/page-explorer/src/Events.tsx @@ -2,16 +2,24 @@ // SPDX-License-Identifier: Apache-2.0 import type { KeyedEvent } from '@polkadot/react-hooks/ctx/types'; +import type { GenericExtrinsic, Vec } from '@polkadot/types'; +import type { AccountId } from '@polkadot/types/interfaces'; +import type { AnyTuple } from '@polkadot/types-codec/types'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; -import { MarkError, Table } from '@polkadot/react-components'; +import { MarkError, styled, Table, Toggle } from '@polkadot/react-components'; +import { useAccounts, useApi, useToggle } from '@polkadot/react-hooks'; +import { isEventFromMyAccounts } from '@polkadot/react-hooks/utils/isEventFromMyAccounts'; import { formatNumber } from '@polkadot/util'; import Event from './Event.js'; import { useTranslation } from './translate.js'; +const MAX_CACHE = 200; +const blockCache = new Map>}>(); + interface Props { className?: string; error?: Error | null; @@ -19,6 +27,7 @@ interface Props { events?: KeyedEvent[] | null; eventClassName?: string; label?: React.ReactNode; + showToggle?: boolean } function renederEvent (className: string | undefined, { blockHash, blockNumber, indexes, key, record }: KeyedEvent): React.ReactNode { @@ -40,8 +49,12 @@ function renederEvent (className: string | undefined, { blockHash, blockNumber, ); } -function Events ({ className = '', emptyLabel, error, eventClassName, events, label }: Props): React.ReactElement { +function Events ({ className = '', emptyLabel, error, eventClassName, events, label, showToggle = false }: Props): React.ReactElement { const { t } = useTranslation(); + const { api } = useApi(); + const { allAccounts } = useAccounts(); + const [showOnlyUserEvents, onToggleUserEvents] = useToggle(); + const [filteredEvents, setFilteredEvents] = useState([]); const header = useMemo<[React.ReactNode?, string?, number?][]>( () => [ @@ -50,25 +63,98 @@ function Events ({ className = '', emptyLabel, error, eventClassName, events, la [label, t] ); - return ( -
- {error - ? ( - - - - ) - : events?.map((e) => renederEvent(eventClassName, e)) + useEffect(() => { + const filter = async () => { + if (!events || !showOnlyUserEvents) { + return; } -
+ + for (const event of events) { + const { blockHash, record } = event; + + if (!blockHash) { + continue; + } + + // use cached block info if available + let blockData = blockCache.get(blockHash); + + if (!blockData) { + const [{ author }, block] = await Promise.all([ + await api.derive.chain.getHeader(blockHash), + await api.rpc.chain.getBlock(blockHash) + ]); + const extrinsics = block.block.extrinsics; + + blockData = { author, extrinsics }; + blockCache.set(blockHash, blockData); + + // Evict oldest key + if (blockCache.size > MAX_CACHE) { + const oldest = blockCache.keys().next().value; + + oldest && blockCache.delete(oldest); + } + } + + const { author, extrinsics } = blockData; + + if (isEventFromMyAccounts(record, extrinsics, author, allAccounts)) { + setFilteredEvents((prev) => { + const alreadyExists = (prev ?? []).some((e) => e.key === event.key); + + if (alreadyExists) { + return prev; + } + + return [event, ...(prev ?? [])]; + }); + } + } + }; + + filter().catch(console.error); + }, [allAccounts, api.derive.chain, api.rpc.chain, events, showOnlyUserEvents]); + + return ( + + + {error + ? ( + + + + ) + : (showOnlyUserEvents ? filteredEvents : events)?.map((e) => renederEvent(eventClassName, e)) + } +
+ {showToggle && + + } +
); } +const StyledSection = styled.section` + position: relative; + + .ui--Toggle { + position: absolute; + top: 1.4rem; + right: 1rem; + z-index: 999; + } +`; + export default React.memo(Events); diff --git a/packages/page-explorer/src/Main.tsx b/packages/page-explorer/src/Main.tsx index 6cea381a66..628a206fff 100644 --- a/packages/page-explorer/src/Main.tsx +++ b/packages/page-explorer/src/Main.tsx @@ -29,7 +29,10 @@ function Main ({ eventCount, events, headers }: Props): React.ReactElement - + diff --git a/packages/react-hooks/src/utils/isEventFromMyAccounts.ts b/packages/react-hooks/src/utils/isEventFromMyAccounts.ts new file mode 100644 index 0000000000..214ae34a1a --- /dev/null +++ b/packages/react-hooks/src/utils/isEventFromMyAccounts.ts @@ -0,0 +1,34 @@ +// Copyright 2017-2025 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { GenericExtrinsic, Vec } from '@polkadot/types'; +import type { AccountId, EventRecord } from '@polkadot/types/interfaces'; +import type { AnyTuple } from '@polkadot/types-codec/types'; + +import { stringToHex } from '@polkadot/util'; + +export const isEventFromMyAccounts = (newEvent: EventRecord, extrinsics: Vec>, author: AccountId | undefined, allAccounts: string[]) => { + const { event, phase } = newEvent; + + // 1. check if any event arg matches one of the accounts + const involvesInArgs = event.data.some((args) => + allAccounts.some((account) => args.toString().includes(account) || args.toString().includes(stringToHex(account))) + ); + + // 2. check if extrinsic signer matches + let involvesInSigner = false; + + if (phase.isApplyExtrinsic) { + const index = phase.asApplyExtrinsic.toNumber(); + const ext = extrinsics[index]; + + if (ext?.signer) { + involvesInSigner = allAccounts.includes(ext.signer.toString()); + } + } + + // 3. check if block author is in user's accounts + const involvesInAuthor = author && allAccounts.includes(author.toString()); + + return involvesInArgs || involvesInSigner || involvesInAuthor; +};