Skip to content

feat: add ICA message decoding and visualization#259

Open
yorhodes wants to merge 2 commits intomainfrom
feat/ica-decoding
Open

feat: add ICA message decoding and visualization#259
yorhodes wants to merge 2 commits intomainfrom
feat/ica-decoding

Conversation

@yorhodes
Copy link
Member

@yorhodes yorhodes commented Jan 28, 2026

Summary

Add support for decoding and displaying Interchain Account (ICA) messages in the explorer.

Core Logic (src/features/messages/ica.ts)

  • Decode all 3 ICA message types per InterchainAccountMessage.sol:
    • CALLS (0): Direct execution with calls array
    • COMMITMENT (1): Commit hash for later reveal
    • REVEAL (2): Reveal calls matching previous commitment
  • Detect ICA messages by checking sender/recipient against known ICA routers from registry
  • Compute derived ICA addresses using getLocalInterchainAccount() with salt parameter
  • Fetch CCIP Read ISM URLs by calling route() on destination ICA router
  • Link related COMMITMENT/REVEAL messages bidirectionally via origin tx hash

UI Component (src/features/messages/cards/IcaDetailsCard.tsx)

  • Status sections with delivery-aware states:
    • COMMITMENT: Shows "Commitment Revealed" (green) or "Pending Reveal" (amber)
    • REVEAL: Shows "Commitment Revealed" (green) or "Revealing Commitment" (amber)
  • Display Owner, Account (computed ICA), ISM, Salt, and User fields
  • CCIP Read Gateway section (only for pending REVEAL) with gateway URLs
  • Calls section with green/amber styling based on delivery status

Other Changes

  • Add ICA types to src/types.ts
  • Add ICA router address map to Zustand store
  • Update debugger to use new ICA decoding API

Test Messages

Note: This PR adds the ICA decoding logic and UI component. A follow-up PR will integrate the IcaDetailsCard into the MessageDetails page.

Summary by CodeRabbit

  • New Features

    • Enhanced Interchain Account (ICA) message visualization with support for CALLS, COMMITMENT, and REVEAL message types
    • Added linking between related ICA commitment and reveal messages
    • Improved ICA call details display with target addresses, values, and transaction data
    • Expanded debugging information for ICA message failures with per-call error details
  • UI/UX Improvements

    • Refined layout for displaying ICA call information and metadata
    • Added multi-provider address resolution for ICA operations

✏️ Tip: You can customize this high-level summary in your review settings.

@yorhodes yorhodes requested a review from Xaroz as a code owner January 28, 2026 21:39
@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 4:54pm

Request Review

Add support for decoding and displaying Interchain Account (ICA) messages:

- Decode all 3 ICA message types: CALLS, COMMITMENT, REVEAL
- Detect ICA messages by checking sender/recipient against known ICA routers
- Compute derived ICA addresses using the SDK
- Fetch CCIP Read ISM URLs for REVEAL messages
- Link related COMMITMENT and REVEAL messages bidirectionally
- Add IcaDetailsCard component with:
  - Status sections (delivered/pending) with commitment hash
  - Owner, Account, ISM, Salt, User fields
  - CCIP Read Gateway info for pending REVEAL messages
  - Call details with execution status
- Update debugger to use new ICA decoding API
@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

This PR substantially overhauls Interchain Account (ICA) handling across the debugger and message details layers, introducing a comprehensive ICA decoding and routing API, enhanced debugging diagnostics, and data-driven UI rendering for ICA messages with support for multiple message types (CALLS, COMMITMENT, REVEAL).

Changes

Cohort / File(s) Summary
ICA Decoding & Routing Surface
src/features/messages/ica.ts, src/types.ts
New ICA message type enum, decoded message interface, ICA router address mapping, and routing utilities. Added hooks for ICA address derivation, reveal call fetching, CCIP Read ISM URL resolution, and related message linking. Multicall decoding helpers and reveal metadata extraction included.
Debugger Message Analysis
src/features/debugger/debugMessage.ts, src/features/debugger/types.ts
Rewrote ICA debugging logic with new IcaDebugResult interface tracking failed call index and error details. Updated tryDebugIcaMsg to compute actual ICA addresses, iterate calls with improved logging, and return structured failure info. Added callValue parameter to tryCheckIcaCall for gas estimation.
ICA Details Rendering
src/features/messages/cards/IcaDetailsCard.tsx
Replaced narrow decoding with data-driven component supporting full message context, debug data, and multiple providers. Added async data integration hooks for calls, related messages, and ISM URLs. Introduced IcaCallDetails subcomponent for per-call rendering. Public signature now accepts full message object and optional debug result.
Message Details Integration
src/features/messages/MessageDetails.tsx
Repositioned IcaDetailsCard rendering immediately after WarpTransferDetailsCard, removed duplicate rendering. Updated useIsIcaMessage call to pass object with sender/recipient.
Layout Refinements
src/features/messages/cards/KeyValueRow.tsx
Refactored inner layout to inline CopyButton and LinkIcon within display row, using flex container for alignment. Moved styling from container to span elements for tighter control.

Sequence Diagram(s)

sequenceDiagram
    participant Debugger as Debugger
    participant ICALogic as ICA Logic
    participant Router as ICA Router Contract
    participant Provider as Destination Provider

    Debugger->>ICALogic: tryDebugIcaMsg(message)
    
    ICALogic->>ICALogic: Check if CALLS message
    alt Non-CALLS message
        ICALogic-->>Debugger: Return null (early exit)
    end
    
    ICALogic->>Router: computeIcaAddress(owner, ism, salt, ...)
    Router-->>ICALogic: icaAddress
    
    ICALogic->>Provider: Iterate calls with logging
    loop For each call in message
        ICALogic->>ICALogic: tryCheckIcaCall(call, callValue)
        alt Call succeeds
            ICALogic->>Provider: estimateGas(to, data, value)
            Provider-->>ICALogic: Gas estimate OK
        else Call fails
            ICALogic->>ICALogic: Record failedCallIndex & errorReason
            ICALogic-->>Debugger: Return IcaDebugResult with failure details
        end
    end
    
    alt All calls validated
        ICALogic-->>Debugger: Return null (success)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • feat: show collateral status #232 — Modifies the same src/features/debugger/debugMessage.ts file and updates provider access patterns that may conflict with MultiProvider changes in this PR.

Suggested reviewers

  • Xaroz

Poem

🧅 Layers deep in ogre's code,
ICA messages now exposed,
Calls and reveals, no longer hid,
Debug details keep the lid,
Routing through the swamp with pride. 🗺️

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add ICA message decoding and visualization' directly aligns with the changeset's core objective of adding ICA decoding logic and UI components for visualizing ICA messages.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ica-decoding

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@src/features/debugger/debugMessage.ts`:
- Around line 418-426: The loop in debugMessage.ts calls tryCheckIcaCall using
recipient (the ICA router) which leads to misleading gas estimates; instead,
skip ICA call checks until the actual derived ICA address is available—update
the loop that iterates over calls to detect ICA-targeted calls (using recipient
and call.to context) and bypass calling tryCheckIcaCall for those, emitting a
clear debug/info message like "Skipping ICA call check until derived ICA address
is implemented" rather than performing estimateGas with the router address; keep
tryCheckIcaCall intact for future use once proper derivation is added.

In `@src/features/messages/cards/IcaDetailsCard.tsx`:
- Around line 612-613: Wrap the BigNumber conversion for call.value in defensive
handling: validate or try-catch around BigNumber.from(call.value) used to
compute hasValue and formattedValue, and fall back to safe defaults (hasValue =
false and formattedValue = '0') when parsing fails; update the logic in the
block that defines hasValue and formattedValue (referencing BigNumber.from,
call.value, hasValue, formattedValue, and fromWei) so malformed inputs don't
throw and the component can render safely.

In `@src/features/messages/ica.ts`:
- Around line 559-566: The current fallback in the message-matching logic
decodes reveal metadata from processCalls[0] and returns revealData.calls, which
can return data for the wrong message when multiple messages are batched; change
the fallback to return null instead of decoding/returning processCalls[0] so
callers receive a clear "unavailable" signal (update the code path that
references decodeRevealMetadata, processCalls, and revealData to return null
here and ensure callers handle a null result appropriately).
- Around line 136-138: The empty-body/malformed input check in decodeIcaBody is
unsafe because BigNumber.from(body) will throw on non-numeric or non-hex
strings; wrap the conversion in a safe validation or try/catch so malformed
input returns null instead of crashing. Specifically, in decodeIcaBody validate
that body is a non-empty numeric/hex string (or call ethers/utils.isHexString or
similar) before calling BigNumber.from(body), or catch errors from
BigNumber.from and return null; update the logic around BigNumber.from(body) to
perform this safe check so decodeIcaBody never throws on malformed body values.

In `@src/store.ts`:
- Around line 44-45: The store exposes icaRouterAddressMap and
setIcaRouterAddressMap but they are never populated or read; remove the unused
slice to avoid dead state. Delete the icaRouterAddressMap property and
setIcaRouterAddressMap setter from the store type and initialization (references
to icaRouterAddressMap and setIcaRouterAddressMap in store creation), and remove
any tests/consumers that reference them; alternatively, if you prefer
centralizing state, refactor callers that currently read the module-level
ICA_ROUTER_MAP (from buildIcaRouterAddressMap / ICA_ROUTER_MAP) to instead read
from and update the store via setIcaRouterAddressMap and
icaRouterAddressMap—pick one approach and apply consistently so only one source
of truth remains.

In `@src/types.ts`:
- Around line 75-91: Remove the unused IcaMessageDetails type declaration and
its export: delete the IcaMessageDetails interface (keeping IcaCall and
IcaRouterAddressMap intact), then search for any lingering imports/usages and
replace them with the actual DecodedIcaMessage type returned by
decodeIcaBody/parseIcaMessageDetails where appropriate; ensure no other modules
reference IcaMessageDetails and run the typecheck/build to confirm no remaining
references.
🧹 Nitpick comments (3)
src/features/messages/cards/IcaDetailsCard.tsx (2)

143-210: Async explorer URL fetching could be simplified

This callback recreates on every render when dependencies change, and the effect triggers the fetch. It's like walkin' through the swamp the long way. Consider using useQuery from tanstack (already imported in ica.ts) for consistency with other data fetching in this PR, or at minimum add cleanup to prevent setting state on unmounted components.

♻️ Consider adding cleanup for unmount safety
   useEffect(() => {
-    getExplorerUrls().catch(() => setExplorerUrls({}));
-  }, [getExplorerUrls]);
+    let cancelled = false;
+    getExplorerUrls()
+      .then((urls) => {
+        if (!cancelled) setExplorerUrls(urls || {});
+      })
+      .catch(() => {
+        if (!cancelled) setExplorerUrls({});
+      });
+    return () => {
+      cancelled = true;
+    };
+  }, [getExplorerUrls]);

This would require modifying getExplorerUrls to return the urls object instead of setting state directly.


513-527: IIFE pattern works but extraction would be cleaner

This works fine, but usin' an IIFE inside JSX is a bit like buildin' a house in a swamp - technically possible but makes folks scratch their heads. Could extract to a tiny component or move the logic above the return.

src/features/messages/ica.ts (1)

494-499: Avoid any type for multiProvider

Usin' any here is like leavin' the door to my swamp wide open - anything could wander in. The MultiProtocolProvider type is already imported in the store and used elsewhere.

♻️ Proposed type fix
+import { MultiProtocolProvider } from '@hyperlane-xyz/sdk';
+
 export async function fetchRevealCalls(
   destinationChainName: string,
   processTxHash: string,
   messageId: string,
-  multiProvider: any,
+  multiProvider: MultiProtocolProvider,
 ): Promise<IcaCall[] | null> {

- Fix P1: Wrap BigNumber.from() in try/catch to prevent crashes on invalid data
- Fix P2: Remove risky fallback to first process call, return null instead
- Cleanup: Remove unused IcaMessageDetails type and icaRouterAddressMap store
- UI: Fix link icon alignment in KeyValueRow
- Add computeIcaAddress() to get real ICA address for accurate debugging
- Include call.value in gas estimation for ICA calls
- Add failed call indicator (red/amber) in IcaDetailsCard UI
- Return structured IcaDebugResult with failedCallIndex from debugger
Comment on lines +278 to +279
<div className="text-sm font-medium text-green-800">Commitment Revealed</div>
<div className="mt-2 flex items-start gap-2">
Copy link
Collaborator

Choose a reason for hiding this comment

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

imo this can be collapsible, the section is already very very green

Image

Copy link
Collaborator

Choose a reason for hiding this comment

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

nah i take this back, I think this bit is fine and the bit below can be less in-your-face

Copy link
Collaborator

Choose a reason for hiding this comment

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

is it just me eyes or are there 4 different greens in use?
image
image

Copy link
Contributor

Choose a reason for hiding this comment

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

I think ideally we want this section to be collapsible, imo normal users wouldn't really care about the ICA stuff

<label
className={`text-sm font-medium ${isDelivered ? 'text-green-600' : 'text-amber-600'}`}
>
{isDelivered ? 'Calls executed:' : 'Calls to execute:'}
Copy link
Collaborator

Choose a reason for hiding this comment

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

imo this whole calls executed bit could be the regular grey bgs/borders/text? if there's too much green/amber on a page it then loses the value of being an indicator imo

Copy link
Collaborator

Choose a reason for hiding this comment

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

also seems like we're using different colours here?
image
image

showCopy={true}
blurValue={blur}
/>
{/* Message type info */}
Copy link
Contributor

Choose a reason for hiding this comment

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

this entire section could use a bit of refactoring, I see a lot of multiple jsx with repeated classes, for example the commitment and reveal section has almost the same structure

Comment on lines +213 to +215
useEffect(() => {
getExplorerUrls().catch(() => setExplorerUrls({}));
}, [getExplorerUrls]);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: prefer useEffect to be at the end (right before the return)

Comment on lines +154 to +200
for (let i = 0; i < displayCalls.length; i++) {
const call = displayCalls[i];
urls[`call-${i}`] = await tryGetBlockExplorerAddressUrl(
multiProvider,
destinationDomainId,
call.to,
);
}

// Get URL for owner on origin chain (use derived value for REVEAL messages)
if (displayOwner) {
urls['owner'] = await tryGetBlockExplorerAddressUrl(
multiProvider,
originDomainId,
displayOwner,
);
}

// Get URL for ICA address on destination chain
if (icaAddress) {
urls['ica'] = await tryGetBlockExplorerAddressUrl(
multiProvider,
destinationDomainId,
icaAddress,
);
}

// Get URL for salt address on origin chain (use derived value for REVEAL messages)
const saltAddress = extractAddressFromSalt(displaySalt);
if (saltAddress) {
urls['saltAddress'] = await tryGetBlockExplorerAddressUrl(
multiProvider,
originDomainId,
saltAddress,
);
}

// Get URL for ISM address on destination chain (use derived value for REVEAL messages)
if (displayIsm && displayIsm !== '0x0000000000000000000000000000000000000000') {
urls['ism'] = await tryGetBlockExplorerAddressUrl(
multiProvider,
destinationDomainId,
displayIsm,
);
}

setExplorerUrls(urls);
Copy link
Contributor

Choose a reason for hiding this comment

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

could we maybe do Promise.all for these?

Comment on lines +213 to +215
useEffect(() => {
getExplorerUrls().catch(() => setExplorerUrls({}));
}, [getExplorerUrls]);
Copy link
Contributor

Choose a reason for hiding this comment

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

to prevent status update if the component unmounts, you should cleanup the component

useEffect(() => {
  let cancelled = false;

  getExplorerUrls()
    .then(urls => {
      if (!cancelled) {
        setExplorerUrls(urls);
      }
    })
    .catch(() => {
      if (!cancelled) {
        setExplorerUrls({});
      }
    });

  return () => {
    cancelled = true;
  };
}, [getExplorerUrls]);

Comment on lines +462 to +493
{displayOwner && (
<KeyValueRow
label="Owner:"
labelWidth="w-28 sm:w-36"
display={displayOwner}
displayWidth="flex-1 min-w-0"
link={explorerUrls['owner'] || undefined}
showCopy={true}
blurValue={blur}
/>
)}

{/* Show ICA address when we have owner data */}
{displayOwner && (
<KeyValueRow
label="Account:"
labelWidth="w-28 sm:w-36"
display={
icaAddress
? icaAddress
: isIcaFetching
? 'Computing...'
: isIcaError
? 'Error computing'
: 'Unknown'
}
displayWidth="flex-1 min-w-0"
link={icaAddress ? explorerUrls['ica'] || undefined : undefined}
showCopy={!!icaAddress}
blurValue={blur}
/>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

can be combined and use a react fragment

Comment on lines +621 to +628
let hasValue = false;
let formattedValue = '0';
try {
hasValue = BigNumber.from(call.value).gt(0);
formattedValue = hasValue ? fromWei(call.value, 18) : '0';
} catch {
// Malformed value, use defaults
}
Copy link
Contributor

Choose a reason for hiding this comment

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

you should not just call try catch inside a component, put the try catch into its own separate function

Copy link
Contributor

Choose a reason for hiding this comment

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

my recommendation is to use a memo hook and return hasValue and formattedValue there
const {hasValue, formattedValue } = useMemo( () => { try { } catch {} } , [])

Comment on lines +634 to +651
let borderClass: string;
let labelClass: string;
let statusSuffix = '';

if (isDelivered) {
// All calls succeeded
borderClass = 'border-green-200 bg-green-50';
labelClass = 'text-green-600';
} else if (isFailed) {
// This specific call failed
borderClass = 'border-red-200 bg-red-50';
labelClass = 'text-red-600';
statusSuffix = ' — Failed';
} else {
// Pending (either not checked yet, or after a failed call)
borderClass = 'border-amber-200 bg-amber-50';
labelClass = 'text-amber-600';
}
Copy link
Contributor

Choose a reason for hiding this comment

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

same here, this could also be a function outside the component itself

Comment on lines +564 to +566
const { Mailbox__factory } = await import('@hyperlane-xyz/core');
// eslint-disable-next-line camelcase
const mailboxInterface = Mailbox__factory.createInterface();
Copy link
Contributor

Choose a reason for hiding this comment

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

why lazy load here but not for factory? probably best to keep consistent

return null;
}

const provider = multiProvider.getEthersV5Provider(destinationChainName);
Copy link
Contributor

Choose a reason for hiding this comment

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

does this only works for EVM chains? Maybe should return early in case the chain is not a EVM chain before doing this

try {
const provider = multiProvider.getEthersV5Provider(originDomainId);
return tryFetchIcaAddress(originDomainId, sender, provider);
const provider = multiProvider.getEthersV5Provider(destinationChainName);
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto

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