Skip to content

feat: add transaction page to view all messages in a tx#261

Open
yorhodes wants to merge 4 commits intofeat/ica-decodingfrom
feat/tx-page
Open

feat: add transaction page to view all messages in a tx#261
yorhodes wants to merge 4 commits intofeat/ica-decodingfrom
feat/tx-page

Conversation

@yorhodes
Copy link
Member

Summary

Adds a /tx/[txHash] page that displays all Hyperlane messages dispatched in a single origin transaction. This is useful for multi-message transactions like superswaps (warp transfer + ICA calls).

Features

  • Transaction page (/tx/[txHash]) - Shows origin transaction info and a list of all messages with their status
  • Auto-redirect from search - When searching for a message ID or tx hash:
    • Single result → redirects to /message/[msgId]
    • Multiple results matching origin tx hash → redirects to /tx/[txHash]
  • "View all messages" link - Message detail page shows a link when the transaction contains multiple messages
  • Smooth redirect UX - Shows "Found it! Redirecting..." state instead of flashing results

Screenshots

Transaction page with multiple messages:

  • Shows origin chain, tx hash, sender, mailbox, time, and block
  • Lists each message with type detection (Warp Transfer, ICA Commitment, ICA Reveal, ICA Calls)
  • Displays delivery status and duration for each message

Stacked on #259 (feat/ica-decoding)

@yorhodes yorhodes requested a review from Xaroz as a code owner January 28, 2026 23:46
@vercel
Copy link

vercel bot commented Jan 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
hyperlane-explorer Ready Ready Preview, Comment Jan 29, 2026 5:29pm

Request Review

<SpinnerIcon width={40} height={40} />
</div>
<div className="mt-4 text-center font-light leading-loose text-gray-700">
Found it! Redirecting...
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in practice this text never actually shows up and it just flashes me from the search page to the tx page

Comment on lines +118 to +121
>
View all {txMessageCount} messages in tx →
</Link>
)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the msg count is inaccurate (probs need to increase the fetch size or something?)

Image

@paulbalaji
Copy link
Collaborator

codex review

[P1] ICA decode can throw and crash render for valid CALLS messages — src/features/messages/ica.ts:136-138.
BigNumber.from(body).isZero() throws for payloads > 32 bytes (common for ICA CALLS) and it runs outside the try/catch, so any such message blows up IcaDetailsCard, MessageSummaryRow, and debugMessage. Suggest moving the zero check inside the try and using a safe hex check (e.g. const hex = strip0x(body); if (!hex || /^0*$/.test(hex)) return null;).

[P2] Search redirect can send users to an empty /tx page when results only come from PI — src/features/messages/MessageSearch.tsx:145-158 + src/features/transactions/useTransactionMessagesQuery.ts:24-42.
The redirect uses messageListResult (PI if GraphQL is empty), but the /tx page only queries GraphQL. If PI returns multiple messages matching the origin tx hash, you’ll redirect to a tx page that shows “No messages found.” Gate the tx redirect on isMessagesFound (GraphQL) or add PI support to the tx view.

[P2] Auto‑redirect triggers even with no user input — src/features/messages/MessageSearch.tsx:145-152.
When the default “latest messages” view has exactly one result, the page navigates away immediately. Consider requiring hasInput (or a valid hash match) before auto‑redirecting.

[P3] Tx message count is capped at 10, so the “View all N messages” link can undercount — src/features/messages/queries/useMessageQuery.ts:153-189.
If a transaction has >10 messages, the UI shows “View all 10…” even though there are more. Consider a count‑only query or a higher limit (or change the copy to avoid implying a full count).

[P3] Related COMMITMENT/REVEAL lookup is limited to 10 messages — src/features/messages/ica.ts:823-855.
In large fan‑out txs, the related ICA message can be missed, showing incorrect “pending” status and missing cross‑links. Raise the limit or query by commitment hash instead of scanning a capped list.

- 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
- 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
Comment on lines +137 to +166
const redirectUrl = (() => {
// 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;

// 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
// Only redirect if GraphQL found results (tx page uses GraphQL only, not PI)
const inputLower = sanitizedInput.toLowerCase();
if (isMessagesFound && firstMessage.origin?.hash?.toLowerCase() === inputLower) {
return `/tx/${firstMessage.origin.hash}`;
}

return null;
})();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than using a IIFE, I'd prefer if we just wrap it in a memo hook, this ensure it only applies when state changes happen

Comment on lines +169 to +173
useEffect(() => {
if (redirectUrl) {
router.replace(redirectUrl);
}
}, [redirectUrl, router]);
Copy link
Contributor

@Xaroz Xaroz Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use router.push, using replace will "replace" the current stack history and if I wanted to go back it wouldn't work, although with push we might run into an issue with the search looping itself 🤔, still if I press back I'd like to go back to the previous page

Comment on lines +42 to +44
useEffect(() => {
setIsManuallyToggled(false);
}, [forceExpanded]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move effects to the end (right before the first return)

Comment on lines +121 to +134
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]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't this be combined with the summaryLine memo? Ideally wanna avoid usage of hooks if there is no need

Comment on lines +211 to +235
return (
<div className="flex items-center gap-1.5 rounded-full bg-green-100 px-2.5 py-1">
<Image src={CheckmarkIcon} width={14} height={14} alt="" />
<span className="text-xs font-medium text-green-700">
Delivered{duration && ` (${duration})`}
</span>
</div>
);
}

if (status === MessageStatus.Failing) {
return (
<div className="flex items-center gap-1.5 rounded-full bg-red-100 px-2.5 py-1">
<span className="text-xs font-medium text-red-700">Failing</span>
</div>
);
}

return (
<div className="flex items-center gap-1.5 rounded-full bg-amber-100 px-2.5 py-1">
<SpinnerIcon width={14} height={14} color="#b45309" />
<span className="text-xs font-medium text-amber-700">{toTitleCase(status)}</span>
</div>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that some of these can be made into a reusable component that you can a children too

Comment on lines +21 to +68
if (isFetching && !hasRun) {
return (
<div className="mx-auto max-w-2xl">
<Card className="flex min-h-[20rem] items-center justify-center">
<div className="flex flex-col items-center gap-4">
<SpinnerIcon width={40} height={40} />
<p className="text-gray-500">Loading transaction messages...</p>
</div>
</Card>
</div>
);
}

// Error state
if (isError) {
return (
<div className="mx-auto max-w-2xl">
<Card className="flex min-h-[20rem] items-center justify-center">
<div className="flex flex-col items-center gap-4 text-center">
<p className="text-red-500">Error loading transaction</p>
<p className="text-sm text-gray-500">
Please check the transaction hash and try again.
</p>
</div>
</Card>
</div>
);
}

// Not found state
if (hasRun && !isMessagesFound) {
return (
<div className="mx-auto max-w-2xl">
<Card className="flex min-h-[20rem] items-center justify-center">
<div className="flex flex-col items-center gap-4 text-center">
<p className="text-gray-700">No messages found</p>
<p className="max-w-md text-sm text-gray-500">
No Hyperlane messages were found for this transaction hash. The transaction may not
have dispatched any messages, or it may not be indexed yet.
</p>
</div>
</Card>
</div>
);
}

return (
<div className="mx-auto max-w-2xl">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for these

* 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this not basically the same hook as message query, can't it not be re-used/modified to fit this feature?

Comment on lines +174 to +179
/* Ensure tooltips appear above the sticky header (z-10) */
[role='tooltip'],
.htw-tooltip {
z-index: 50 !important;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the tooltip component has a prop called tooltipClassName you can use

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants