From 1411bdb3ccd711529c629289e8bf8f52c537c63a Mon Sep 17 00:00:00 2001 From: Yorke Rhodes IV Date: Wed, 28 Jan 2026 18:42:05 -0500 Subject: [PATCH 1/3] feat: add transaction page to view all messages in a tx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add /tx/[txHash] page showing all messages dispatched in a transaction - Add auto-redirect from search: single result → message page, tx hash match → tx page - Add 'View all messages in tx' link on message detail page when tx has multiple messages - Add SearchRedirecting state for smoother UX during redirects - Add useTransactionMessageCount hook for efficient message count queries - Make TransactionCard and KeyValueRow more responsive --- src/components/search/SearchStates.tsx | 19 ++ src/features/messages/MessageDetails.tsx | 29 ++- src/features/messages/MessageSearch.tsx | 56 ++++- src/features/messages/cards/KeyValueRow.tsx | 6 +- .../messages/cards/TransactionCard.tsx | 13 +- .../messages/queries/useMessageQuery.ts | 39 +++ .../transactions/MessageSummaryRow.tsx | 235 ++++++++++++++++++ .../transactions/TransactionDetails.tsx | 111 +++++++++ .../useTransactionMessagesQuery.ts | 84 +++++++ src/pages/tx/[txHash].tsx | 33 +++ src/styles/global.css | 10 + 11 files changed, 609 insertions(+), 26 deletions(-) create mode 100644 src/features/transactions/MessageSummaryRow.tsx create mode 100644 src/features/transactions/TransactionDetails.tsx create mode 100644 src/features/transactions/useTransactionMessagesQuery.ts create mode 100644 src/pages/tx/[txHash].tsx diff --git a/src/components/search/SearchStates.tsx b/src/components/search/SearchStates.tsx index 765de27d..91de3762 100644 --- a/src/components/search/SearchStates.tsx +++ b/src/components/search/SearchStates.tsx @@ -129,3 +129,22 @@ export function SearchChainError({ show }: { show: boolean }) { /> ); } + +export function SearchRedirecting({ show }: { show: boolean }) { + return ( +
+ +
+
+
+ +
+
+ Found it! Redirecting... +
+
+
+
+
+ ); +} diff --git a/src/features/messages/MessageDetails.tsx b/src/features/messages/MessageDetails.tsx index aeae6e90..3d7210bd 100644 --- a/src/features/messages/MessageDetails.tsx +++ b/src/features/messages/MessageDetails.tsx @@ -1,6 +1,7 @@ import { toTitleCase, trimToLength } from '@hyperlane-xyz/utils'; import { SpinnerIcon } from '@hyperlane-xyz/widgets'; import Image from 'next/image'; +import Link from 'next/link'; import { useEffect, useMemo } from 'react'; import { toast } from 'react-toastify'; import { Card } from '../../components/layout/Card'; @@ -21,7 +22,7 @@ import { WarpTransferDetailsCard } from './cards/WarpTransferDetailsCard'; import { useIsIcaMessage } from './ica'; import { usePiChainMessageQuery } from './pi-queries/usePiChainMessageQuery'; import { PLACEHOLDER_MESSAGE } from './placeholderMessages'; -import { useMessageQuery } from './queries/useMessageQuery'; +import { useMessageQuery, useTransactionMessageCount } from './queries/useMessageQuery'; import { parseWarpRouteMessageDetails } from './utils'; interface Props { @@ -96,15 +97,29 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro [message, warpRouteChainAddressMap, multiProvider], ); + // Check if there are multiple messages in this origin transaction + const txMessageCount = useTransactionMessageCount(origin?.hash); + const showTxLink = txMessageCount > 1; + return ( <> -

{`${ - isIcaMsg ? 'ICA ' : '' - } Message ${trimToLength(msgId, 6)} to ${getChainDisplayName( - multiProvider, - destinationChainName, - )}`}

+
+

{`${ + isIcaMsg ? 'ICA ' : '' + } Message ${trimToLength(msgId, 6)} to ${getChainDisplayName( + multiProvider, + destinationChainName, + )}`}

+ {showTxLink && ( + + View all {txMessageCount} messages in tx → + + )} +
{ + // Wait for queries to complete + if (!hasAllRun || isAnyFetching) return null; + + // Don't redirect if filters are applied + if (originChainFilter || destinationChainFilter || startTimeFilter || endTimeFilter) + return null; + + // Need at least one result + if (!messageListResult.length) return null; + + const firstMessage = messageListResult[0]; + + // Single result → always go to message page + if (messageListResult.length === 1) { + return `/message/${firstMessage.msgId}`; + } + + // Multiple results + origin tx hash match → go to tx page + const inputLower = sanitizedInput.toLowerCase(); + if (firstMessage.origin?.hash?.toLowerCase() === inputLower) { + return `/tx/${firstMessage.origin.hash}`; + } + + return null; + })(); + + // Perform the redirect + useEffect(() => { + if (redirectUrl) { + router.replace(redirectUrl); + } + }, [redirectUrl, router]); + // Show message list if there are no errors and filters are valid const showMessageTable = !isAnyError && @@ -176,22 +214,28 @@ export function MessageSearch() { - + + - - + +
diff --git a/src/features/messages/cards/KeyValueRow.tsx b/src/features/messages/cards/KeyValueRow.tsx index b1419b79..dff6f5ab 100644 --- a/src/features/messages/cards/KeyValueRow.tsx +++ b/src/features/messages/cards/KeyValueRow.tsx @@ -31,8 +31,10 @@ export function KeyValueRow({ const useFallbackVal = isZeroish(display) && !allowZeroish; return (
- -
+ +
{!useFallbackVal ? display : 'Unknown'} {subDisplay && !useFallbackVal && {subDisplay}}
diff --git a/src/features/messages/cards/TransactionCard.tsx b/src/features/messages/cards/TransactionCard.tsx index 9694f497..2b6cf103 100644 --- a/src/features/messages/cards/TransactionCard.tsx +++ b/src/features/messages/cards/TransactionCard.tsx @@ -212,7 +212,7 @@ function TransactionCard({ children, }: PropsWithChildren<{ chainName: string; title: string; helpText: string }>) { return ( - +
@@ -266,6 +266,7 @@ function TransactionDetails({ displayWidth="w-60 sm:w-64" showCopy={true} blurValue={blur} + link={txExplorerLink} /> )} - {txExplorerLink && ( - - View in block explorer - - )} ); } diff --git a/src/features/messages/queries/useMessageQuery.ts b/src/features/messages/queries/useMessageQuery.ts index 2785ddc3..b5bea9b0 100644 --- a/src/features/messages/queries/useMessageQuery.ts +++ b/src/features/messages/queries/useMessageQuery.ts @@ -153,6 +153,45 @@ export function useMessageQuery({ messageId, pause }: { messageId: string; pause }; } +/** + * Hook to count messages in a given origin transaction. + * Used to determine if we should show the "View all messages in this transaction" link. + */ +export function useTransactionMessageCount(originTxHash: string | undefined) { + const { scrapedDomains: scrapedChains } = useScrapedDomains(); + const multiProvider = useMultiProvider(); + + // Build query for origin tx hash + const { query, variables } = useMemo(() => { + if (!originTxHash) { + // Return a no-op query + return buildMessageQuery( + MessageIdentifierType.OriginTxHash, + '0x0000000000000000000000000000000000000000000000000000000000000000', + 1, + true, + ); + } + return buildMessageQuery(MessageIdentifierType.OriginTxHash, originTxHash, 10, true); + }, [originTxHash]); + + // Execute query + const [{ data }] = useQuery({ + query, + variables, + pause: !originTxHash, + }); + + // Parse results + const messageCount = useMemo(() => { + if (!data || !originTxHash) return 0; + const messages = parseMessageStubResult(multiProvider, scrapedChains, data); + return messages.length; + }, [data, multiProvider, scrapedChains, originTxHash]); + + return messageCount; +} + function isWindowVisible() { return document.visibilityState === 'visible'; } diff --git a/src/features/transactions/MessageSummaryRow.tsx b/src/features/transactions/MessageSummaryRow.tsx new file mode 100644 index 00000000..663ced10 --- /dev/null +++ b/src/features/transactions/MessageSummaryRow.tsx @@ -0,0 +1,235 @@ +import { toTitleCase, trimToLength } from '@hyperlane-xyz/utils'; +import { ChevronIcon, SpinnerIcon } from '@hyperlane-xyz/widgets'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useEffect, useMemo, useState } from 'react'; + +import { ChainLogo } from '../../components/icons/ChainLogo'; +import CheckmarkIcon from '../../images/icons/checkmark-circle.svg'; +import { useMultiProvider, useStore } from '../../store'; +import { Message, MessageStatus } from '../../types'; +import { getHumanReadableDuration } from '../../utils/time'; +import { getChainDisplayName } from '../chains/utils'; +import { ContentDetailsCard } from '../messages/cards/ContentDetailsCard'; +import { IcaDetailsCard } from '../messages/cards/IcaDetailsCard'; +import { DestinationTransactionCard } from '../messages/cards/TransactionCard'; +import { WarpTransferDetailsCard } from '../messages/cards/WarpTransferDetailsCard'; +import { decodeIcaBody, IcaMessageType, isIcaMessage } from '../messages/ica'; +import { parseWarpRouteMessageDetails } from '../messages/utils'; + +interface Props { + message: Message; + index: number; + forceExpanded?: boolean; +} + +type MessageType = 'warp' | 'ica-commitment' | 'ica-reveal' | 'ica-calls' | 'generic'; + +export function MessageSummaryRow({ message, index, forceExpanded }: Props) { + const [isManuallyToggled, setIsManuallyToggled] = useState(false); + const [manualExpandState, setManualExpandState] = useState(false); + + // Use forceExpanded unless user has manually toggled + const isExpanded = isManuallyToggled ? manualExpandState : (forceExpanded ?? false); + + const handleToggle = (e: React.MouseEvent) => { + e.preventDefault(); + setIsManuallyToggled(true); + setManualExpandState((prev) => !prev); + }; + + // Reset manual toggle when forceExpanded changes + useEffect(() => { + setIsManuallyToggled(false); + }, [forceExpanded]); + + const multiProvider = useMultiProvider(); + const warpRouteChainAddressMap = useStore((s) => s.warpRouteChainAddressMap); + + // Use message data directly from GraphQL - no additional RPC calls for performance + const { status, originDomainId, destinationDomainId, destination } = message; + + // Parse warp route details + const warpRouteDetails = useMemo( + () => parseWarpRouteMessageDetails(message, warpRouteChainAddressMap, multiProvider), + [message, warpRouteChainAddressMap, multiProvider], + ); + + // Detect message type + const { messageType } = useMemo(() => { + // Check warp route first + if (warpRouteDetails) { + return { messageType: 'warp' as MessageType }; + } + + // Check ICA + if (isIcaMessage({ sender: message.sender, recipient: message.recipient })) { + const decoded = decodeIcaBody(message.body); + if (decoded) { + if (decoded.messageType === IcaMessageType.COMMITMENT) { + return { messageType: 'ica-commitment' as MessageType }; + } + if (decoded.messageType === IcaMessageType.REVEAL) { + return { messageType: 'ica-reveal' as MessageType }; + } + if (decoded.messageType === IcaMessageType.CALLS) { + return { messageType: 'ica-calls' as MessageType }; + } + } + } + + return { messageType: 'generic' as MessageType }; + }, [message, warpRouteDetails]); + + const originChainName = multiProvider.tryGetChainName(originDomainId) || 'Unknown'; + const destinationChainName = multiProvider.tryGetChainName(destinationDomainId) || 'Unknown'; + + const duration = destination?.timestamp + ? getHumanReadableDuration(destination.timestamp - message.origin.timestamp, 2) + : undefined; + + // Generate summary line based on message type + const summaryLine = useMemo(() => { + const route = `${getChainDisplayName(multiProvider, originChainName, true)} → ${getChainDisplayName(multiProvider, destinationChainName, true)}`; + + switch (messageType) { + case 'warp': + return `${warpRouteDetails?.amount} ${warpRouteDetails?.originToken?.symbol} · ${route}`; + case 'ica-commitment': { + const decoded = decodeIcaBody(message.body); + return `${trimToLength(decoded?.commitment || message.msgId, 12)} · ${route}`; + } + case 'ica-reveal': + return `Reveal · ${route}`; + case 'ica-calls': { + const decoded = decodeIcaBody(message.body); + return `${decoded?.calls?.length || 0} calls · ${route}`; + } + default: + return route; + } + }, [ + messageType, + warpRouteDetails, + multiProvider, + originChainName, + destinationChainName, + message, + ]); + + // Generate title based on message type + const title = useMemo(() => { + switch (messageType) { + case 'warp': + return 'Warp Transfer'; + case 'ica-commitment': + return 'Interchain Account Commitment'; + case 'ica-reveal': + return 'Interchain Account Reveal'; + case 'ica-calls': + return 'Interchain Account Calls'; + default: + return 'Message'; + } + }, [messageType]); + + const isIcaMsg = messageType.startsWith('ica-'); + + return ( +
+ {/* Summary Row (always visible) */} +
+
+ #{index + 1} + +
+
+ {title} + e.stopPropagation()} + className="text-xs text-blue-500 transition-colors hover:text-blue-600" + > + ↗ + +
+

{summaryLine}

+
+
+
+ + +
+
+ + {/* Expanded Content */} + {isExpanded && ( +
+ {/* Destination Transaction Card */} + + + {/* Warp Transfer Details */} + {messageType === 'warp' && warpRouteDetails && ( + + )} + + {/* ICA Details */} + {isIcaMsg && } + + {/* Content Details - only show if no decoded content (warp/ICA) */} + {messageType === 'generic' && } +
+ )} +
+ ); +} + +function StatusBadge({ status, duration }: { status: MessageStatus; duration?: string }) { + if (status === MessageStatus.Delivered) { + return ( +
+ + + Delivered{duration && ` (${duration})`} + +
+ ); + } + + if (status === MessageStatus.Failing) { + return ( +
+ Failing +
+ ); + } + + return ( +
+ + {toTitleCase(status)} +
+ ); +} diff --git a/src/features/transactions/TransactionDetails.tsx b/src/features/transactions/TransactionDetails.tsx new file mode 100644 index 00000000..7ef323ae --- /dev/null +++ b/src/features/transactions/TransactionDetails.tsx @@ -0,0 +1,111 @@ +import { SpinnerIcon, Tooltip } from '@hyperlane-xyz/widgets'; +import { useState } from 'react'; + +import { Card } from '../../components/layout/Card'; +import { OriginTransactionCard } from '../messages/cards/TransactionCard'; + +import { MessageSummaryRow } from './MessageSummaryRow'; +import { useTransactionMessagesQuery } from './useTransactionMessagesQuery'; + +interface Props { + txHash: string; +} + +export function TransactionDetails({ txHash }: Props) { + const [allExpanded, setAllExpanded] = useState(false); + + const { isFetching, isError, hasRun, isMessagesFound, messageList, originInfo } = + useTransactionMessagesQuery(txHash); + + // Loading state + if (isFetching && !hasRun) { + return ( + +
+ +

Loading transaction messages...

+
+
+ ); + } + + // Error state + if (isError) { + return ( + +
+

Error loading transaction

+

Please check the transaction hash and try again.

+
+
+ ); + } + + // Not found state + if (hasRun && !isMessagesFound) { + return ( + +
+

No messages found

+

+ No Hyperlane messages were found for this transaction hash. The transaction may not have + dispatched any messages, or it may not be indexed yet. +

+
+
+ ); + } + + return ( + <> + {/* Origin Transaction Card - reuse existing component */} + {originInfo && ( + + )} + + {/* Messages Section */} + +
+
+

Messages ({messageList.length})

+ +
+ {messageList.length > 1 && ( + + )} +
+ +
+ {messageList.map((message, index) => ( + + ))} +
+ + {isFetching && ( +
+ + Refreshing... +
+ )} +
+ + ); +} diff --git a/src/features/transactions/useTransactionMessagesQuery.ts b/src/features/transactions/useTransactionMessagesQuery.ts new file mode 100644 index 00000000..567af0fa --- /dev/null +++ b/src/features/transactions/useTransactionMessagesQuery.ts @@ -0,0 +1,84 @@ +import { useCallback, useMemo } from 'react'; +import { useQuery } from 'urql'; + +import { useInterval } from '@hyperlane-xyz/widgets'; + +import { useMultiProvider } from '../../store'; +import { Message } from '../../types'; +import { useScrapedDomains } from '../chains/queries/useScrapedChains'; +import { MessageIdentifierType, buildMessageQuery } from '../messages/queries/build'; +import { MessagesQueryResult } from '../messages/queries/fragments'; +import { parseMessageQueryResult } from '../messages/queries/parse'; + +const TX_AUTO_REFRESH_DELAY = 10_000; // 10s +const TX_QUERY_LIMIT = 1000; // Max messages per transaction + +/** + * Hook to query all messages dispatched in a single origin transaction. + * Returns full Message objects (not stubs) for detailed display. + */ +export function useTransactionMessagesQuery(txHash: string) { + const { scrapedDomains: scrapedChains } = useScrapedDomains(); + const multiProvider = useMultiProvider(); + + // Build the GraphQL query for origin tx hash + const { query, variables } = useMemo( + () => buildMessageQuery(MessageIdentifierType.OriginTxHash, txHash, TX_QUERY_LIMIT, false), + [txHash], + ); + + // Execute query + const [result, reexecuteQuery] = useQuery({ + query, + variables, + pause: !txHash, + }); + const { data, fetching: isFetching, error } = result; + + // Parse results into Message objects + const messageList = useMemo(() => { + const messages = parseMessageQueryResult(multiProvider, scrapedChains, data); + // Sort by nonce (ascending) for consistent ordering + return messages.sort((a, b) => a.nonce - b.nonce); + }, [multiProvider, scrapedChains, data]); + + const isMessagesFound = messageList.length > 0; + + // Check if all messages are delivered + const allDelivered = useMemo( + () => messageList.length > 0 && messageList.every((m) => m.destination), + [messageList], + ); + + // Setup interval to re-query (only if not all delivered) + const reExecutor = useCallback(() => { + if (!txHash || allDelivered || !isWindowVisible()) return; + reexecuteQuery({ requestPolicy: 'network-only' }); + }, [reexecuteQuery, txHash, allDelivered]); + useInterval(reExecutor, TX_AUTO_REFRESH_DELAY); + + // Extract common origin transaction info from the first message + const originInfo = useMemo(() => { + if (!messageList.length) return null; + const first = messageList[0]; + return { + chainName: multiProvider.tryGetChainName(first.originDomainId) || 'Unknown', + domainId: first.originDomainId, + transaction: first.origin, + }; + }, [messageList, multiProvider]); + + return { + isFetching, + isError: !!error, + hasRun: !!data, + isMessagesFound, + messageList: messageList as Message[], + originInfo, + refetch: reExecutor, + }; +} + +function isWindowVisible() { + return document.visibilityState === 'visible'; +} diff --git a/src/pages/tx/[txHash].tsx b/src/pages/tx/[txHash].tsx new file mode 100644 index 00000000..bb0d834c --- /dev/null +++ b/src/pages/tx/[txHash].tsx @@ -0,0 +1,33 @@ +import type { NextPage } from 'next'; +import dynamic from 'next/dynamic'; +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; + +import { logger } from '../../utils/logger'; + +const TransactionDetails = dynamic( + () => + import('../../features/transactions/TransactionDetails').then((mod) => mod.TransactionDetails), + { ssr: false }, +); + +const TransactionPage: NextPage = () => { + const router = useRouter(); + const { txHash } = router.query; + + useEffect(() => { + // Only redirect after router is ready and txHash is confirmed missing/invalid + if (router.isReady && (!txHash || typeof txHash !== 'string')) { + router.replace('/').catch((e) => logger.error('Error routing back to home', e)); + } + }, [router, router.isReady, txHash]); + + // Render nothing while waiting for client-side router to be ready + if (!router.isReady || !txHash || typeof txHash !== 'string') { + return null; + } + + return ; +}; + +export default TransactionPage; diff --git a/src/styles/global.css b/src/styles/global.css index 1dccd443..cebe42bc 100755 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -167,6 +167,16 @@ Common animations } } +/* +Tooltips +======== +*/ +/* Ensure tooltips appear above the sticky header (z-10) */ +[role='tooltip'], +.htw-tooltip { + z-index: 50 !important; +} + /* Toasts ====== From 8aee94d90a074715e49f035ec2e43b2eb4e1e959 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes IV Date: Thu, 29 Jan 2026 11:24:50 -0500 Subject: [PATCH 2/3] fix: address PR review comments for tx page - P1: Fix ICA decode crash for payloads >32 bytes by moving zero check inside try block and using safe regex instead of BigNumber.isZero() - P2: Prevent auto-redirect on homepage when latest messages has 1 result - P2: Gate tx page redirect on GraphQL results (not PI) since tx page only queries GraphQL - P3: Increase message count query limit from 10 to 1000 for accurate 'View all N messages' link - P3: Increase related ICA message lookup limit from 10 to 1000 to find related COMMITMENT/REVEAL in large fan-out transactions --- src/features/messages/MessageSearch.tsx | 6 +++++- src/features/messages/ica.ts | 7 +++++-- src/features/messages/queries/useMessageQuery.ts | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/features/messages/MessageSearch.tsx b/src/features/messages/MessageSearch.tsx index f87c5554..a45fea29 100644 --- a/src/features/messages/MessageSearch.tsx +++ b/src/features/messages/MessageSearch.tsx @@ -138,6 +138,9 @@ export function MessageSearch() { // Wait for queries to complete if (!hasAllRun || isAnyFetching) return null; + // Don't redirect without user input (prevents redirect on homepage with latest messages) + if (!hasInput) return null; + // Don't redirect if filters are applied if (originChainFilter || destinationChainFilter || startTimeFilter || endTimeFilter) return null; @@ -153,8 +156,9 @@ export function MessageSearch() { } // Multiple results + origin tx hash match → go to tx page + // Only redirect if GraphQL found results (tx page uses GraphQL only, not PI) const inputLower = sanitizedInput.toLowerCase(); - if (firstMessage.origin?.hash?.toLowerCase() === inputLower) { + if (isMessagesFound && firstMessage.origin?.hash?.toLowerCase() === inputLower) { return `/tx/${firstMessage.origin.hash}`; } diff --git a/src/features/messages/ica.ts b/src/features/messages/ica.ts index d0979ead..8e70c439 100644 --- a/src/features/messages/ica.ts +++ b/src/features/messages/ica.ts @@ -134,11 +134,14 @@ export function useIsIcaMessage({ * - Bytes 33-65: Commitment (bytes32) */ export function decodeIcaBody(body: string): DecodedIcaMessage | null { - if (!body || BigNumber.from(body).isZero()) return null; + if (!body) return null; try { const bodyHex = strip0x(body); + // Safe zero check - handles any length payload without throwing + if (!bodyHex || /^0*$/.test(bodyHex)) return null; + // Minimum length to read message type: 1 byte = 2 hex chars if (bodyHex.length < 2) { logger.warn('ICA body too short to read message type'); @@ -830,7 +833,7 @@ export function useRelatedIcaMessage( true, ); } - return buildMessageQuery(MessageIdentifierType.OriginTxHash, originTxHash, 10, true); + return buildMessageQuery(MessageIdentifierType.OriginTxHash, originTxHash, 1000, true); }, [shouldSearch, originTxHash]); // Execute the query diff --git a/src/features/messages/queries/useMessageQuery.ts b/src/features/messages/queries/useMessageQuery.ts index b5bea9b0..9f52efd2 100644 --- a/src/features/messages/queries/useMessageQuery.ts +++ b/src/features/messages/queries/useMessageQuery.ts @@ -172,7 +172,7 @@ export function useTransactionMessageCount(originTxHash: string | undefined) { true, ); } - return buildMessageQuery(MessageIdentifierType.OriginTxHash, originTxHash, 10, true); + return buildMessageQuery(MessageIdentifierType.OriginTxHash, originTxHash, 1000, true); }, [originTxHash]); // Execute query From f5fa4d3dd34bcd8707bf10fe259f24406244e6d9 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes IV Date: Thu, 29 Jan 2026 12:24:53 -0500 Subject: [PATCH 3/3] fix: constrain tx page width for better layout --- .../transactions/TransactionDetails.tsx | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/features/transactions/TransactionDetails.tsx b/src/features/transactions/TransactionDetails.tsx index 7ef323ae..d29abb35 100644 --- a/src/features/transactions/TransactionDetails.tsx +++ b/src/features/transactions/TransactionDetails.tsx @@ -20,44 +20,52 @@ export function TransactionDetails({ txHash }: Props) { // Loading state if (isFetching && !hasRun) { return ( - -
- -

Loading transaction messages...

-
-
+
+ +
+ +

Loading transaction messages...

+
+
+
); } // Error state if (isError) { return ( - -
-

Error loading transaction

-

Please check the transaction hash and try again.

-
-
+
+ +
+

Error loading transaction

+

+ Please check the transaction hash and try again. +

+
+
+
); } // Not found state if (hasRun && !isMessagesFound) { return ( - -
-

No messages found

-

- No Hyperlane messages were found for this transaction hash. The transaction may not have - dispatched any messages, or it may not be indexed yet. -

-
-
+
+ +
+

No messages found

+

+ No Hyperlane messages were found for this transaction hash. The transaction may not + have dispatched any messages, or it may not be indexed yet. +

+
+
+
); } return ( - <> +
{/* Origin Transaction Card - reuse existing component */} {originInfo && ( )} - +
); }