Skip to content

feat: add real-time validator signature status display#253

Draft
nambrot wants to merge 5 commits intopbio/perf-beta-package-surfacesfrom
nam/validator-signature-status
Draft

feat: add real-time validator signature status display#253
nambrot wants to merge 5 commits intopbio/perf-beta-package-surfacesfrom
nam/validator-signature-status

Conversation

@nambrot
Copy link
Copy Markdown
Contributor

@nambrot nambrot commented Jan 15, 2026

Summary

  • add /api/ism-details for server-side ISM + hook config derivation with best-effort caching
  • vendor a small local metadata builder/type slice for multisig, aggregation, routing, and null ISMs
  • surface validator signature status in the ISM details card, timeline, and pending destination tx path
  • include the small review-followup cleanup for helper extraction and message-stage derivation

Why

The debugger currently makes it hard to tell whether a destination message is still waiting on validator signatures, whether quorum is close, and which validators have already signed. This adds a server-derived ISM details path and renders that state directly in the message UI.

I intentionally removed the direct @hyperlane-xyz/relayer dependency from this PR and mirrored the small read-only metadata slice locally instead. #253 only needs metadata building for validator-status display, but pulling in the published relayer package causes a large lockfile diff today because it drags in service deps and satisfies optional logging peers on the current Hyperlane package line. Keeping the metadata slice local keeps this PR reviewable and avoids mixing metadata UI work with relayer package churn. The full relayer dependency can land in #247, where it is actually needed for self-relay.

Stack

  • base PR: #301 (perf: keep explorer metadata-first and evm-only at runtime)
  • this PR should merge after #301

Validation

  • pnpm run format
  • pnpm run typecheck
  • pnpm run build

@nambrot nambrot requested a review from Xaroz as a code owner January 15, 2026 03:45
@vercel
Copy link
Copy Markdown

vercel bot commented Jan 15, 2026

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

Project Deployment Actions Updated (UTC)
hyperlane-explorer Ready Ready Preview, Comment Apr 16, 2026 10:20pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request layers in ISM (Interchain Security Module) details visualization throughout the message debugger. It adds a backend API endpoint, new types and hooks for fetching ISM configuration data, integrates validator signature status tracking, and refactors UI components to display ISM trees with validator information across the delivery workflow.

Changes

Cohort / File(s) Summary
Dependency Updates
package.json
Updated @hyperlane-xyz/core from 10.0.5 to 10.1.5; added @hyperlane-xyz/sdk PNPM override to preview version.
Type Definitions
src/features/debugger/types.ts, src/features/deliveryStatus/types.ts
Introduced ValidatorStatus and IsmDetails interfaces; updated MessageDebugResult to use IsmDetails type; added optional debugResult field to MessageDeliverySuccessResult.
Schema & Validation
src/features/chains/useChainMetadata.ts
Replaced ChainMetadataArraySchema with z.array(z.any()) to avoid TypeScript recursion issues while preserving runtime validation.
Backend Data Fetching
src/features/debugger/useIsmDetails.ts, src/pages/api/ism-details.ts
Added new hook useIsmDetails for fetching ISM details via React Query; introduced comprehensive API endpoint with caching, parallel ISM/Hook derivation, and validator signature status building.
Delivery Status Integration
src/features/deliveryStatus/fetchDeliveryStatus.ts, src/features/deliveryStatus/useMessageDeliveryStatus.tsx
Enhanced delivery status fetch to include debug/ISM information in parallel; updated hook to propagate debugResult across all delivery status types and adjusted destination resolution logic.
Message Details UI
src/features/messages/MessageDetails.tsx
Integrated useIsmDetails hook; wired ismDetails and debugResult into card components; exported extractValidatorInfo utility for validator data extraction.
ISM Details Visualization
src/features/messages/cards/IsmDetailsCard.tsx
Major refactor: replaced flat rendering with hierarchical ISM tree rendering; added internal components (IsmTreeNode, ValidatorList, ValidatorRow) for displaying ISM structure with expansion state; implemented extractValidatorInfo for traversing multisig/aggregation/routing configurations.
Timeline Enhancement
src/features/messages/cards/TimelineCard.tsx
Added support for debugResult and ismResult props; introduced EnhancedMessageTimeline component with multi-stage visualization (Sent, Finalized, Validated, Relayed), validator quorum tracking, and per-validator status displays.
Transaction Status Rendering
src/features/messages/cards/TransactionCard.tsx
Added validatorInfo prop to DestinationTransactionCard; introduced ValidatorStatusSummary component to conditionally display validator signature progress and quorum status instead of generic loading spinner.

Sequence Diagram

sequenceDiagram
    participant UI as Frontend Component
    participant Hook as useIsmDetails Hook
    participant API as /api/ism-details Endpoint
    participant Registry as Registry & Core
    participant Reader as ISM/Hook Reader
    participant Builder as MetadataBuilder

    UI->>Hook: fetch ISM details (message)
    Hook->>API: POST originTxHash, messageId, originDomain
    
    API->>Registry: get cached or build registry/core (TTL 1min)
    Registry-->>API: registry, core, metadataBuilder
    
    API->>API: derive originChain from originDomain
    API->>Registry: fetch dispatch tx receipt & extract messages
    Registry-->>API: dispatch tx receipt
    
    API->>API: locate target message by messageId
    API->>API: determine destinationChain from message
    
    par ISM and Hook Resolution
        API->>Reader: resolve recipient ISM & Hook addresses
        Reader-->>API: ISM address, Hook address
    end
    
    par ISM and Hook Configuration (with caching)
        API->>Reader: derive ISM config (EvmIsmReader, TTL 30min)
        Reader-->>API: ISM configuration tree
        API->>Reader: derive Hook config (EvmHookReader, TTL 30min)
        Reader-->>API: Hook configuration
    end
    
    API->>Builder: build MetadataBuildResult with validator signatures
    Builder-->>API: ISM details + validator status
    
    API-->>Hook: MetadataBuildResult
    Hook-->>UI: ismDetails (cached 30s, retry 1x)
    
    UI->>UI: extractValidatorInfo(ismDetails)
    UI->>UI: render ISM tree & validator progress
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

Like layers in an ogre, ISM details unfold,
A tree of validators, brave and bold,
Signatures stacking, quorum in sight,
The message debugger shines ever bright! 🌳✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.40% 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
Title check ✅ Passed The title 'feat: add real-time validator signature status display' accurately captures the main objective of this PR, which centers on displaying validator signature statuses in real-time across multiple components.
Description check ✅ Passed The pull request description clearly outlines the changes: adding server-side ISM details API, vendoring metadata builder, displaying validator signature status, and related cleanup. It directly relates to the changeset.

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


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.

@nambrot nambrot force-pushed the nam/validator-signature-status branch from 5378d7a to b13b8f6 Compare January 15, 2026 03:51
Copy link
Copy Markdown

@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: 2

🤖 Fix all issues with AI agents
In `@src/features/debugger/validatorStatus.ts`:
- Around line 154-170: The code in validatorStatus.ts currently returns status:
'pending' together with an error message for missing or unsupported storage (see
the block using parseS3Location and the location check); change the responses so
that error cases return status: 'error' (not 'pending') when you set an error
field, or alternatively remove the error field for true pending states—update
the two return objects in the location-null and !s3Info branches to use status:
'error' when including the error string (and keep any pending-only path free of
an error field) so downstream consumers see a consistent status/value pairing.

In `@src/pages/api/ism-details.ts`:
- Around line 31-47: The ismConfigCache and hookConfigCache are unbounded; add a
MAX_CACHE_SIZE constant and a helper like setCacheWithLimit(cache, key, value)
that enforces the limit by evicting the oldest entry (use Map insertion order or
compare stored timestamp) before inserting, then replace direct cache.set(...)
calls with setCacheWithLimit for ismConfigCache and hookConfigCache; keep
CONFIG_CACHE_TTL logic but ensure eviction uses the same stored
{config,timestamp} shape and reference getIsmCacheKey/getHookCacheKey when
creating keys.
🧹 Nitpick comments (15)
src/features/chains/useChainMetadata.ts (1)

38-38: Type assertion bypasses compile-time safety.

The as ChainMetadata[] cast here tells TypeScript to trust that result.data contains valid ChainMetadata objects, but since the schema is now z.any(), there's no guarantee that's true. If an element has a missing or malformed name property, line 43 and 51 would behave unexpectedly.

package.json (1)

19-23: These aren't preview builds—they're the latest stable releases.

The versions you're using (10.1.5 for core, 21.1.0 for sdk/utils/widgets) are already the current latest on npm. The commit hash suffix is just part of the version metadata, not a red flag. The "preview" tag is how Hyperlane labels these particular builds in their dist-tag system.

Since you're already on the latest stable versions, there's nothing to swap out here. No swamp to worry about.

src/features/deliveryStatus/useMessageDeliveryStatus.tsx (1)

24-29: Query key includes entire message object - might cause unnecessary refetches

Aye, using the whole message object in the query key there at line 25 could be a wee bit troublesome. If any property on that message changes (even ones that don't affect the fetch), it'll trigger a new query. Might want to use only the stable identifiers ye actually need.

Consider something like:

- queryKey: ['messageDeliveryStatus', message, !!multiProvider, registry, chainMetadataOverrides],
+ queryKey: ['messageDeliveryStatus', message.id, message.status, !!multiProvider, registry, chainMetadataOverrides],
src/features/debugger/useIsmDetails.ts (1)

22-46: Consider adding request timeout

The fetch call here doesn't have any timeout configured. If the API endpoint is slow or hangs, this could leave users waiting forever in the swamp. Might want to add an AbortController with a reasonable timeout.

♻️ Suggested improvement
 async function fetchIsmDetails(message: Message): Promise<MetadataBuildResult | null> {
   if (!message.origin?.hash) {
     logger.warn('No origin transaction hash available');
     return null;
   }

+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
+
   try {
     const response = await fetch('/api/ism-details', {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
       },
       body: JSON.stringify({
         originTxHash: message.origin.hash,
         messageId: message.msgId,
         originDomain: message.originDomainId,
       }),
+      signal: controller.signal,
     });
+    clearTimeout(timeoutId);

     if (!response.ok) {
       const errorData = await response.json().catch(() => ({}));
       logger.warn('Failed to fetch ISM details:', errorData);
       return null;
     }

     const data: IsmDetailsResponse = await response.json();
     return data.result;
   } catch (error) {
+    clearTimeout(timeoutId);
+    if (error instanceof Error && error.name === 'AbortError') {
+      logger.warn('ISM details request timed out');
+      return null;
+    }
     logger.error('Error fetching ISM details:', error);
     return null;
   }
 }
src/features/messages/cards/TransactionCard.tsx (2)

350-400: Add defensive guard against empty validators array inside component

While the caller currently checks validators.length > 0 before rendering, the ValidatorStatusSummary component itself doesn't guard against empty arrays. If someone calls it directly without that check, lines 377 and 385 will divide by zero, producing NaN% or Infinity% in the style calculations.

Defense in depth, as they say in my swamp:

♻️ Proposed defensive fix
 function ValidatorStatusSummary({
   validators,
   threshold,
 }: {
   validators: ValidatorInfo[];
   threshold: number;
 }) {
+  if (validators.length === 0) {
+    return null;
+  }
+
   const signedCount = validators.filter((v) => v.status === 'signed').length;
   const hasQuorum = signedCount >= threshold && threshold > 0;

364-370: Consider using clsx() for conditional className

Per the coding guidelines, ye should be using clsx() for conditional className assignment instead of template literals. Makes things a bit tidier around these parts.

♻️ Suggested refactor
+import clsx from 'clsx';

// Then in the component:
-          className={`rounded-full px-2 py-0.5 text-xs font-medium ${
-            hasQuorum ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
-          }`}
+          className={clsx(
+            'rounded-full px-2 py-0.5 text-xs font-medium',
+            hasQuorum ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
+          )}

Based on coding guidelines, clsx() should be used for conditional className assignment in components.

src/features/debugger/validatorStatus.ts (1)

53-87: Consider adding a timeout to the fetch, if ye don't mind.

This here fetch could sit in the swamp forever if the network decides to take a nap. Might want to add an AbortController with a reasonable timeout to keep things moving along.

🔧 Optional: Add fetch timeout
 async function fetchS3Checkpoint(
   bucket: string,
   region: string,
   folder: string,
   index: number,
 ): Promise<S3CheckpointWithId | null> {
   const key = folder ? `${folder}/checkpoint_${index}_with_id.json` : `checkpoint_${index}_with_id.json`;
   const url = `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
   
+  const controller = new AbortController();
+  const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
+  
   try {
     const response = await fetch(url, {
       method: 'GET',
       credentials: 'omit',
       mode: 'cors',
+      signal: controller.signal,
     });
+    clearTimeout(timeoutId);
src/pages/api/ism-details.ts (3)

109-119: Sequential fetching of chain addresses could be parallelized.

Right now ye're going through each chain one by one, which might be slower than it needs to be. Could batch these requests up with Promise.all to speed things along when the cache is cold.

⚡ Optional: Parallelize chain address fetching
-  for (const chainName of chainNames) {
-    try {
-      const addresses = await registry.getChainAddresses(chainName);
-      if (addresses) {
-        addressesMap[chainName] = addresses;
-      }
-    } catch (_e) {
-      // Skip chains without addresses
-    }
-  }
+  const addressResults = await Promise.allSettled(
+    chainNames.map(async (chainName) => {
+      const addresses = await registry.getChainAddresses(chainName);
+      return { chainName, addresses };
+    })
+  );
+  
+  for (const result of addressResults) {
+    if (result.status === 'fulfilled' && result.value.addresses) {
+      addressesMap[result.value.chainName] = result.value.addresses;
+    }
+  }

146-164: Consider validating all required fields together.

Ye're checking originTxHash and messageId first, then originDomain separately. Could simplify by validating them all at once - saves a round trip for the caller if they're missing multiple fields.

🧹 Optional: Combine validation
     const {
       originTxHash,
       messageId,
+      originDomain,
     } = req.body;

-    if (!originTxHash || !messageId) {
-      return res.status(400).json({ error: 'Missing required fields: originTxHash, messageId' });
+    if (!originTxHash || !messageId || !originDomain) {
+      return res.status(400).json({ error: 'Missing required fields: originTxHash, messageId, originDomain' });
     }
-
-    const { multiProvider, core, metadataBuilder, fromCache } = await getRegistryAndCore();
-    timer.mark('getRegistryAndCore');
-    logger.info(`[TIMING] getRegistryAndCore took ${timer.getTimings().getRegistryAndCore}ms (fromCache: ${fromCache})`);
-
-    // We need to find which chain this transaction is on
-    // For now, require the origin domain to be passed
-    const { originDomain } = req.body;
-    if (!originDomain) {
-      return res.status(400).json({ error: 'Missing required field: originDomain' });
-    }

297-301: Error messages might leak internal details.

When something goes wrong, ye're returning error.message directly. In production, this could expose internal stack traces or sensitive paths. Might want to sanitize or provide a generic message while logging the full error server-side.

🔒 Sanitize error response
   } catch (error: any) {
     logger.error('Error fetching ISM details:', error);
     logger.error('[TIMING] Error occurred after:', timer.getTimings());
-    return res.status(500).json({ error: error.message || 'Internal server error' });
+    return res.status(500).json({ 
+      error: config.debug ? (error.message || 'Internal server error') : 'Internal server error' 
+    });
   }
src/features/messages/MessageDetails.tsx (2)

79-81: Consider handling loading and error states from useIsmDetails.

Ye're only grabbing the data here. The query could be loading or have errored, and the UI doesn't reflect that. Might want to show a loading state or handle errors gracefully - though I understand if ye want to keep it simple and just not show the card until data arrives.


140-140: This line is a wee bit long - over 100 characters.

Per yer coding guidelines, lines should be 100 characters or less. Consider breaking it up.

📏 Line break suggestion
-        {showTimeline && <TimelineCard message={message} blur={blur} debugResult={debugResult} ismResult={ismDetails} />}
+        {showTimeline && (
+          <TimelineCard
+            message={message}
+            blur={blur}
+            debugResult={debugResult}
+            ismResult={ismDetails}
+          />
+        )}
src/features/messages/cards/TimelineCard.tsx (1)

207-220: Emojis work, but consider consistency with the rest of the UI.

Using emoji for stage icons (✈, 🔒, 🛡, ✉) is quick and works everywhere, but it might look different across devices/browsers. If ye've got SVG icons elsewhere in the project, might want to match that style eventually. Not urgent though - this gets the job done.

src/features/debugger/debugMessage.ts (2)

382-399: Optional: Consider parallel fetching for sub-modules.

The sequential for...of loop processes sub-modules one at a time. For aggregation ISMs with multiple sub-modules, you could use Promise.all for parallel fetching to reduce latency. However, sequential is safer if there are concerns about provider rate limiting or if the recursion depth could get significant.

Given the 30-minute caching mentioned in the PR objectives, this is probably fine as-is since cold cache hits are already acceptable at ~4.2s.

🔧 Optional parallel fetching
-      // Recursively get details for each sub-module
-      for (const subModuleAddr of modules) {
-        const subModuleDetails = await getIsmDetails(subModuleAddr, messageBytes, provider, validatorContext);
-        ismDetails.subModules.push(subModuleDetails);
-
-        // If this sub-module has validators, propagate to the parent for display
-        if (
-          subModuleDetails.validators &&
-          subModuleDetails.validators.length > 0 &&
-          !ismDetails.validators
-        ) {
-          ismDetails.validators = subModuleDetails.validators;
-          // Use the multisig's threshold for validator display
-          if (subModuleDetails.threshold !== undefined) {
-            ismDetails.threshold = subModuleDetails.threshold;
-          }
-        }
-      }
+      // Recursively get details for each sub-module in parallel
+      const subModuleDetailsArray = await Promise.all(
+        modules.map((subModuleAddr) =>
+          getIsmDetails(subModuleAddr, messageBytes, provider, validatorContext)
+        )
+      );
+      ismDetails.subModules = subModuleDetailsArray;
+
+      // Propagate first sub-module's validators to parent for display
+      for (const subModuleDetails of subModuleDetailsArray) {
+        if (
+          subModuleDetails.validators &&
+          subModuleDetails.validators.length > 0 &&
+          !ismDetails.validators
+        ) {
+          ismDetails.validators = subModuleDetails.validators;
+          if (subModuleDetails.threshold !== undefined) {
+            ismDetails.threshold = subModuleDetails.threshold;
+          }
+          break;
+        }
+      }

383-384: Minor: Line length slightly exceeds 100 characters.

Lines 384 and 411 are a bit over the 100-character limit. Not a big deal, but figured I'd mention it since the coding guidelines ask for it. Prettier should sort this out if you run it.

As per coding guidelines, enforce 100 character line width.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between acab6ad and c3ff04f.

📒 Files selected for processing (16)
  • next.config.js
  • package.json
  • src/features/chains/useChainMetadata.ts
  • src/features/chains/utils.ts
  • src/features/debugger/debugMessage.ts
  • src/features/debugger/types.ts
  • src/features/debugger/useIsmDetails.ts
  • src/features/debugger/validatorStatus.ts
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/deliveryStatus/types.ts
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/MessageDetails.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/TransactionCard.tsx
  • src/pages/api/ism-details.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Access state using Zustand hooks: useMultiProvider(), useChainMetadata(), useRegistry()
Use clsx() for conditional className assignment in components

Files:

  • src/features/deliveryStatus/types.ts
  • src/features/debugger/types.ts
  • src/features/chains/useChainMetadata.ts
  • src/pages/api/ism-details.ts
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/debugger/useIsmDetails.ts
  • src/features/debugger/validatorStatus.ts
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/chains/utils.ts
  • src/features/debugger/debugMessage.ts
  • src/features/messages/MessageDetails.tsx
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/cards/TransactionCard.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,ts,jsx,tsx}: Use single quotes for strings in code
Use trailing commas in code
Enforce 100 character line width

Files:

  • src/features/deliveryStatus/types.ts
  • src/features/debugger/types.ts
  • src/features/chains/useChainMetadata.ts
  • src/pages/api/ism-details.ts
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/debugger/useIsmDetails.ts
  • src/features/debugger/validatorStatus.ts
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/chains/utils.ts
  • src/features/debugger/debugMessage.ts
  • src/features/messages/MessageDetails.tsx
  • next.config.js
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/cards/TransactionCard.tsx
**/*.{js,ts,jsx,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier with import organization and Tailwind class sorting

Files:

  • src/features/deliveryStatus/types.ts
  • src/features/debugger/types.ts
  • src/features/chains/useChainMetadata.ts
  • src/pages/api/ism-details.ts
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/debugger/useIsmDetails.ts
  • src/features/debugger/validatorStatus.ts
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/chains/utils.ts
  • src/features/debugger/debugMessage.ts
  • src/features/messages/MessageDetails.tsx
  • next.config.js
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/cards/TransactionCard.tsx
🧠 Learnings (5)
📚 Learning: 2025-12-17T11:59:10.567Z
Learnt from: paulbalaji
Repo: hyperlane-xyz/hyperlane-explorer PR: 242
File: src/utils/yamlParsing.ts:20-41
Timestamp: 2025-12-17T11:59:10.567Z
Learning: Chain names in this repository must be lowercase as defined by the schema. Do not apply any runtime case normalization when parsing or storing chain metadata; instead, validate and rely on lowercase inputs, and ensure downstream logic preserves lowercase to maintain consistency across parsing and storage.

Applied to files:

  • src/features/deliveryStatus/types.ts
  • src/features/debugger/types.ts
  • src/features/chains/useChainMetadata.ts
  • src/pages/api/ism-details.ts
  • src/features/debugger/useIsmDetails.ts
  • src/features/debugger/validatorStatus.ts
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/chains/utils.ts
  • src/features/debugger/debugMessage.ts
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Use Zustand store (`src/store.ts`) to manage global state including chain metadata, MultiProtocolProvider, and warp routes

Applied to files:

  • src/features/chains/useChainMetadata.ts
  • src/features/messages/cards/TransactionCard.tsx
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Applies to **/*.{ts,tsx} : Access state using Zustand hooks: `useMultiProvider()`, `useChainMetadata()`, `useRegistry()`

Applied to files:

  • src/features/chains/useChainMetadata.ts
  • src/features/messages/cards/TransactionCard.tsx
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Applies to src/consts/api.ts : Update API endpoints in `src/consts/api.ts`

Applied to files:

  • src/pages/api/ism-details.ts
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Use Ethers.js v5 for direct blockchain calls

Applied to files:

  • src/features/debugger/debugMessage.ts
🧬 Code graph analysis (8)
src/features/deliveryStatus/types.ts (1)
src/features/debugger/types.ts (1)
  • MessageDebugResult (34-47)
src/pages/api/ism-details.ts (1)
src/utils/logger.ts (1)
  • logger (3-8)
src/features/messages/cards/TimelineCard.tsx (4)
src/types.ts (1)
  • Message (50-57)
src/features/debugger/types.ts (1)
  • MessageDebugResult (34-47)
src/features/messages/cards/IsmDetailsCard.tsx (1)
  • extractValidatorInfo (294-331)
src/components/layout/Card.tsx (1)
  • Card (8-12)
src/features/debugger/useIsmDetails.ts (2)
src/types.ts (1)
  • Message (50-57)
src/utils/logger.ts (1)
  • logger (3-8)
src/features/debugger/validatorStatus.ts (2)
src/utils/logger.ts (1)
  • logger (3-8)
src/features/debugger/types.ts (1)
  • ValidatorStatus (13-18)
src/features/deliveryStatus/fetchDeliveryStatus.ts (2)
src/features/debugger/debugMessage.ts (1)
  • debugMessage (54-163)
src/utils/logger.ts (1)
  • logger (3-8)
src/features/debugger/debugMessage.ts (4)
src/features/chains/utils.ts (2)
  • getValidatorAnnounceAddress (20-26)
  • getMerkleTreeHookAddress (28-34)
src/features/debugger/types.ts (2)
  • IsmDetails (21-32)
  • ValidatorStatus (13-18)
src/utils/logger.ts (1)
  • logger (3-8)
src/features/debugger/validatorStatus.ts (1)
  • getValidatorSignatureStatus (118-219)
src/features/messages/MessageDetails.tsx (3)
src/features/debugger/useIsmDetails.ts (1)
  • useIsmDetails (49-59)
src/features/messages/cards/IsmDetailsCard.tsx (2)
  • extractValidatorInfo (294-331)
  • IsmDetailsCard (22-53)
src/features/messages/cards/TimelineCard.tsx (1)
  • TimelineCard (17-39)
🔇 Additional comments (40)
next.config.js (1)

66-79: WASM mocking looks good, though placement could be tidied up.

These changes properly stub out the Aleo SDK packages and handle WASM files as assets. The approach of setting aliases to false is the right way to exclude problematic modules from the bundle.

One small thing to consider: the WASM aliases are applied unconditionally (client + server) while the pino mock is server-only. If that's intentional, all good — just making sure it wasn't meant to be inside the isServer block.

src/features/chains/utils.ts (1)

20-34: These helpers are nice and simple — following the existing pattern.

Both functions are clean wrappers around registry.getChainAddresses. They're consistent with the existing getMailboxAddress approach, just without the override support.

If you find yourself needing both addresses in the same spot, might be worth a combined helper to avoid two registry calls — but that's something for another day, not this swamp.

package.json (1)

96-97: Good call on the SDK override.

This ensures all transitive dependencies use the same preview SDK version, avoiding potential version mismatches. Consistent versioning across the dependency tree prevents some nasty surprises.

src/features/deliveryStatus/types.ts (1)

8-12: Clean type extension — optional debug info for successful deliveries.

Making debugResult optional for success cases is the right call. Delivered messages don't always need the full debug payload, but when you want to show validator signatures and ISM details, the data's there. This plays nicely with the existing required debugResult on failing/pending results.

src/features/chains/useChainMetadata.ts (1)

13-15: The comment is technically accurate but misleading about where validation occurs.

The claim that runtime validation happens via ChainMetadataSchema is correct — it does happen, but downstream in store.ts (lines 156-158), not in this function. The safeParse on line 33 only confirms the data is an array; individual chain metadata objects don't get validated until they're processed in the store's objFilter callback.

This creates a brief window where invalid metadata can exist in chainMetadataOverrides state. Consider clarifying the comment to specify that validation occurs when the overrides are consumed (in store.ts), so future maintainers understand the validation timeline.

src/features/deliveryStatus/useMessageDeliveryStatus.tsx (1)

55-56: LGTM - debugResult propagation for delivered messages

Good on ye for adding the debug result to delivered messages too. This allows the validator info display to work even after delivery, which is exactly what the swamp ordered.

src/features/deliveryStatus/fetchDeliveryStatus.ts (2)

43-50: Nice parallel fetching approach

Well done parallelizing these fetches with Promise.all. Like layers on an onion, ye get both the transaction details and debug info at the same time instead of waiting for one after the other. The resilient .catch() handler ensures the main flow doesn't get bogged down if debug info fails.


88-96: DomainId is globally available and requires no import

The DomainId type is declared as a global ambient type in src/global.d.ts, making it available throughout the codebase without explicit imports. TypeScript will have no issue with the function signature at line 90. No changes are needed.

Likely an incorrect or invalid review comment.

src/features/debugger/useIsmDetails.ts (1)

49-58: ESLint disable comment - verify query key completeness

That eslint-disable-next-line comment at line 52 is like ignoring the "Keep Out" sign at the entrance. The exhaustive-deps rule is there for good reason. Make sure ye really don't need message itself in the deps, or document why msgId and origin.hash are sufficient for cache invalidation.

The hook otherwise looks solid - enabled only when there's a valid message with origin hash, and sensible caching config.

src/features/messages/cards/TransactionCard.tsx (2)

4-4: LGTM - Import addition

Good import of ValidatorInfo type from the SDK.


178-189: Good defensive check before rendering validator summary

Nice work checking validatorInfo.validators.length > 0 before rendering the summary component. This prevents the division-by-zero issue that could occur in the progress bar calculations.

src/features/debugger/types.ts (2)

12-32: Well-structured recursive ISM types

Nice work on these new interfaces. The IsmDetails structure with subModules and selectedModule handles the recursive nature of aggregation and routing ISMs quite elegantly - like layers on an onion, if ye will.


13-18: No action needed — types are globally declared

All three types (Address, AddressTo, and HexString) are ambient type declarations in src/global.d.ts, so they're available throughout the codebase without explicit imports. The code's working as intended.

Likely an incorrect or invalid review comment.

src/features/debugger/validatorStatus.ts (5)

1-16: Looks good to me, donkey!

The module setup is clean - nice docstring explaining what this swamp of code does, proper imports from the Hyperlane ecosystem, and a sensible constant for the S3 prefix. The GCS comment shows ye've got plans for the future without cluttering things up now.


18-29: Interface structure looks proper.

The snake_case field names match what's coming from S3 storage - that's the way it should be done when mapping external JSON structures.


34-46: Parsing logic is solid.

Simple and effective - ye get what ye need from the S3 URL without over-complicating things. The null return for invalid inputs keeps things safe downstream.


173-219: The checkpoint validation and parallel execution look proper.

Nice work with the parallel fetching - no need to wait around in line like at Far Far Away. The checkpoint field matching is thorough, and the error handling keeps things from blowing up.


92-113: Method getEthersV5Provider is correct and properly used.

This method exists on MultiProtocolProvider from the Hyperlane SDK and is already used consistently across the codebase in multiple places—messages utils, delivery status, debugger, you name it. The way ye're using it here is spot on. Gets ye an Ethers v5 provider, connects to the contract, and fetches what ye need. The logic for grabbing the most recent storage location is sound as well. All layers work together nicely.

src/pages/api/ism-details.ts (1)

262-279: If derivation fails, ismConfig or hookConfig could be undefined when used.

If one of the deriveTasks rejects, Promise.all will throw, but that's caught by the outer try-catch. However, if ye want finer-grained error handling (like continuing with partial data), ye might want Promise.allSettled. Currently it's fine because the outer catch handles it, but just making sure ye know what's lurking in these waters.

src/features/messages/MessageDetails.tsx (3)

14-18: Imports look proper.

Good separation of concerns - pulling in the hook and the utility function from their respective homes.


138-139: This works, but extractValidatorInfo is called on every render.

Not a big deal since it's a synchronous traversal, but if ye find performance issues later, could wrap in useMemo. For now, it's fine - no need to over-engineer it.


152-154: Clean conditional rendering.

Only showing the ISM card when ye've got something to show - that's the way to do it. No empty states cluttering up the place.

src/features/messages/cards/TimelineCard.tsx (5)

1-15: Imports and props look good.

Nice type definitions. I notice debugResult is in the props but not used in the current implementation (prefixed with _). Keeping it for API compatibility makes sense if other components might pass it.


17-39: Component structure is clean.

Good delegation pattern - extracting the validator info and passing it down. The @ts-ignore is a known tech debt item that's noted with a TODO.


57-171: EnhancedMessageTimeline is well-structured.

Like layers in an onion - each stage is clearly separated. The fix to show Relayed when delivered is a good catch. The expandable validator section is a nice touch for when folks want more details.


238-313: ValidatorDropdown looks proper.

Good progress visualization with the threshold marker. One small thing - hasQuorum is calculated both here and in the parent EnhancedMessageTimeline. Could pass it as a prop to avoid the duplication, but it's a tiny calculation so not a big deal.


315-350: Helper functions are clean and purposeful.

The stage header logic handles edge cases well (no timing for Sent, special handling for failures). The opacity classes give good visual feedback on progress.

src/features/messages/cards/IsmDetailsCard.tsx (6)

1-20: Imports and Props are well-organized.

Clean separation of type imports and value imports. The Props interface is minimal - just what ye need.


22-53: IsmDetailsCard component is clean and well-structured.

Early return for null result keeps things tidy. The layout with header, description, and tree makes the information digestible.


55-133: IsmTreeNode recursion is handled well.

The depth-based indentation creates a nice visual hierarchy - makes it easy to see what's nested where. Auto-expanding the first two levels is a good UX choice.


135-208: ValidatorList and ValidatorRow are well-implemented.

Good progress visualization with the layered bars showing threshold vs signed count. The error tooltip on hover (title attribute) is a nice touch for debugging without cluttering the UI.


210-288: Helper functions are comprehensive and well-organized.

The type guards cover all the ISM variants, and the color coding makes it easy to distinguish different ISM types at a glance. Like sorting onions by color - makes things easier to find.


290-331: extractValidatorInfo is a solid utility function.

The recursive traversal handles the nested ISM structures properly. Returning the first found validator set makes sense - in most cases there's only one multisig in the tree anyway.

src/features/debugger/debugMessage.ts (7)

5-47: LGTM!

The new imports are well-organized and all appear to be used by the new ISM inspection logic. The factory imports from @hyperlane-xyz/core and utility imports from local modules are properly structured.


111-130: LGTM!

The defensive approach here is spot on, like building a sturdy swamp hut. Only constructing the validator context when all the pieces are available prevents downstream errors. The inline comment explaining that nonce equals merkle tree index is a nice touch for future travelers through this code.


260-269: LGTM!

The interface is well-defined with all necessary fields for validator status checking. Clean and straightforward.


272-307: LGTM!

The function now delegates to the recursive getIsmDetails for the heavy lifting while keeping its core responsibility of checking for misconfiguration. The optional validatorContext flows through nicely without breaking existing behavior when it's not available.


309-372: LGTM!

The multisig handling here is solid as an onion's layers. The fallback to 'pending' status when validator signature fetching fails ensures the UI always has something to show rather than breaking entirely. Good defensive approach.


374-404: Consider whether propagating only the first sub-module's validators is intentional.

The current logic propagates validators from only the first sub-module that has them (due to the !ismDetails.validators check). If an aggregation ISM has multiple multisig sub-modules, only the first one's validators will bubble up to the parent for display.

If the intent is to show all validators across multiple multisig sub-modules, you'd need to aggregate them. But if the goal is just to surface "some" validator info for the UI, this approach works fine. Just wanted to make sure this matches what you had in mind, like knowing exactly what's in your swamp before you settle in.


406-429: LGTM!

The routing ISM handling properly resolves the actual routed ISM based on the message and recursively fetches its details. The fallback for other ISM types keeps things from falling apart when encountering unexpected module types.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/features/debugger/validatorStatus.ts Outdated
Comment thread src/pages/api/ism-details.ts
Copy link
Copy Markdown

@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: 3

🤖 Fix all issues with AI agents
In `@src/features/chains/useChainMetadata.ts`:
- Around line 13-15: The comment is misleading because using z.array(z.any())
with ChainMetadataArraySchema means safeParse will accept any items and the cast
on line 38 doesn't validate them; update the useEffect to perform per-item
runtime validation by iterating result.data after safeParse success and calling
ChainMetadataSchema.safeParse on each item (log warnings via logger.warn for
failures) and only keep items that pass, then cast the filtered array to
ChainMetadata; keep ChainMetadataArraySchema to avoid TS recursion but ensure
runtime validation happens per-item using ChainMetadataSchema and the existing
safeParse flow.

In `@src/features/debugger/types.ts`:
- Around line 20-32: Remove the dead IsmDetails type and switch
MessageDebugResult to use the SDK's MetadataBuildResult (or drop the unused
ismDetails field entirely if MessageDebugResult already carries
ismResult/MetadataBuildResult); update any references to IsmDetails (including
the ismDetails property on MessageDebugResult) to reference MetadataBuildResult
or ismResult, and remove now-unused imports/exports related to IsmDetails to
keep types consistent and avoid duplication.

In `@src/features/messages/cards/IsmDetailsCard.tsx`:
- Around line 143-158: Guard against division-by-zero in IsmDetailsCard: when
computing the style widths that use (threshold / validators.length) and
(signedCount / validators.length) ensure validators.length is checked first and
return 0% when it's 0. Update the two inline style expressions (the threshold
marker and the signed progress divs) to use a conditional like validators.length
> 0 ? (threshold / validators.length) * 100 : 0 and validators.length > 0 ?
(signedCount / validators.length) * 100 : 0 respectively so widths become "0%"
instead of "Infinity%".
♻️ Duplicate comments (1)
src/pages/api/ism-details.ts (1)

31-37: Unbounded cache growth concern previously noted.

The cache size management issue was flagged in a prior review. The ismConfigCache and hookConfigCache Maps can grow without bounds over time.

🧹 Nitpick comments (16)
src/pages/api/ism-details.ts (3)

109-118: This loop's movin' slower than a swamp snail.

Fetchin' chain addresses one-by-one in a sequential loop could be sped up considerably. These are independent network calls that could run in parallel.

⚡ Suggested parallel approach
-  for (const chainName of chainNames) {
-    try {
-      const addresses = await registry.getChainAddresses(chainName);
-      if (addresses) {
-        addressesMap[chainName] = addresses;
-      }
-    } catch (_e) {
-      // Skip chains without addresses
-    }
-  }
+  await Promise.all(
+    chainNames.map(async (chainName) => {
+      try {
+        const addresses = await registry.getChainAddresses(chainName);
+        if (addresses) {
+          addressesMap[chainName] = addresses;
+        }
+      } catch (_e) {
+        // Skip chains without addresses
+      }
+    })
+  );

146-164: Why make two trips when one'll do?

The originDomain is validated separately from originTxHash and messageId, resultin' in two potential 400 responses for missin' fields. Might as well check 'em all together.

🧅 Suggested consolidation
   try {
     const {
       originTxHash,
       messageId,
+      originDomain,
     } = req.body;

-    if (!originTxHash || !messageId) {
-      return res.status(400).json({ error: 'Missing required fields: originTxHash, messageId' });
+    if (!originTxHash || !messageId || !originDomain) {
+      return res.status(400).json({ error: 'Missing required fields: originTxHash, messageId, originDomain' });
     }

     const { multiProvider, core, metadataBuilder, fromCache } = await getRegistryAndCore();
     timer.mark('getRegistryAndCore');
     logger.info(`[TIMING] getRegistryAndCore took ${timer.getTimings().getRegistryAndCore}ms (fromCache: ${fromCache})`);

-    // We need to find which chain this transaction is on
-    // For now, require the origin domain to be passed
-    const { originDomain } = req.body;
-    if (!originDomain) {
-      return res.status(400).json({ error: 'Missing required field: originDomain' });
-    }
-
     const originChain = multiProvider.tryGetChainName(originDomain);

286-294: Consider givin' this response some proper layers.

Typin' response as any means you lose all the type checkin' benefits. A proper interface would keep things tidy.

💡 Suggested type definition
interface IsmDetailsResponse {
  result: MetadataBuildResult;
  _timings?: Record<string, number>;
  _cache?: {
    registry: boolean;
    ismConfig: boolean;
    hookConfig: boolean;
  };
}

// Then use it:
const response: IsmDetailsResponse = { result };
if (config.debug) {
  response._timings = finalTimings;
  response._cache = {
    registry: fromCache,
    ismConfig: ismFromCache,
    hookConfig: hookFromCache,
  };
}
src/features/messages/cards/TransactionCard.tsx (1)

364-370: Could use clsx() for these conditional class names.

Per the codin' guidelines, conditional className assignment should use clsx(). There's a few spots in this component with template literal conditionals.

💡 Example refactor
+import clsx from 'clsx';

// Then in the component:
         <span
-          className={`rounded-full px-2 py-0.5 text-xs font-medium ${
-            hasQuorum ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
-          }`}
+          className={clsx(
+            'rounded-full px-2 py-0.5 text-xs font-medium',
+            hasQuorum ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
+          )}
         >

Same pattern applies to lines 381-384.

Based on coding guidelines, conditional className assignment should use clsx().

src/features/debugger/useIsmDetails.ts (1)

35-39: Consider surfacing error details to users.

Right now you're fetchin' that error data but only loggin' it to the console. For most folks wanderin' through your swamp, that information just disappears into the void. Might want to extract a message from the error response for better observability, though it's not critical since this gracefully returns null.

🔧 Optional: Extract error message for logging
     if (!response.ok) {
       const errorData = await response.json().catch(() => ({}));
-      logger.warn('Failed to fetch ISM details:', errorData);
+      const errorMessage = errorData?.error || errorData?.message || 'Unknown error';
+      logger.warn('Failed to fetch ISM details:', errorMessage, errorData);
       return null;
     }
src/features/messages/MessageDetails.tsx (1)

140-140: Line exceeds the 100 character limit.

Per the coding guidelines, we should keep lines under 100 characters. This one's gotten a bit... ogre-sized.

🔧 Split the line for readability
-        {showTimeline && <TimelineCard message={message} blur={blur} debugResult={debugResult} ismResult={ismDetails} />}
+        {showTimeline && (
+          <TimelineCard
+            message={message}
+            blur={blur}
+            debugResult={debugResult}
+            ismResult={ismDetails}
+          />
+        )}
src/features/messages/cards/TimelineCard.tsx (3)

17-17: Unused parameter debugResult.

You've got debugResult comin' in but it's just sittin' there collectin' dust as _debugResult. If it's not needed in this component, might want to remove it from the props interface to keep things honest. Otherwise, if there's future plans for it, a TODO comment would help.

🔧 Remove unused parameter or document intent

If not needed:

 interface Props {
   message: Message;
   blur?: boolean;
-  debugResult?: MessageDebugResult;
   ismResult?: MetadataBuildResult | null;
 }

-export function TimelineCard({ message, blur, debugResult: _debugResult, ismResult }: Props) {
+export function TimelineCard({ message, blur, ismResult }: Props) {

Or if planned for future use:

-export function TimelineCard({ message, blur, debugResult: _debugResult, ismResult }: Props) {
+export function TimelineCard({ message, blur, debugResult, ismResult }: Props) {
+  // TODO: Use debugResult for additional error state display

28-28: Consider using clsx() for conditional className.

Per coding guidelines, clsx() is preferred for conditional class assignment. This applies to several places in this file with template literal conditionals.

🔧 Example using clsx
+import clsx from 'clsx';
+
 // In TimelineCard:
-      <div className={`-mx-2 -my-2 font-light sm:mx-0 ${blur && 'blur-xs'}`}>
+      <div className={clsx('-mx-2 -my-2 font-light sm:mx-0', blur && 'blur-xs')}>

207-220: Emoji icons may have accessibility concerns.

These emoji icons look nice in the swamp, but screen readers might announce them in unexpected ways. Consider adding aria-hidden="true" to the emoji container and providin' an aria-label on the parent for better accessibility.

♿ Improve accessibility
 function StageIcon({ stage }: { stage: MessageStage }) {
   const iconMap: Partial<Record<MessageStage, string>> = {
     [MessageStage.Sent]: '✈',
     [MessageStage.Finalized]: '🔒',
     [MessageStage.Validated]: '🛡',
     [MessageStage.Relayed]: '✉',
   };

+  const labelMap: Partial<Record<MessageStage, string>> = {
+    [MessageStage.Sent]: 'Sent',
+    [MessageStage.Finalized]: 'Finalized',
+    [MessageStage.Validated]: 'Validated',
+    [MessageStage.Relayed]: 'Relayed',
+  };
+
   return (
-    <div className="flex h-9 w-9 items-center justify-center rounded-full bg-blue-500 text-white">
-      {iconMap[stage] || '•'}
+    <div
+      className="flex h-9 w-9 items-center justify-center rounded-full bg-blue-500 text-white"
+      role="img"
+      aria-label={labelMap[stage] || 'Stage'}
+    >
+      <span aria-hidden="true">{iconMap[stage] || '•'}</span>
     </div>
   );
 }
src/features/messages/cards/IsmDetailsCard.tsx (7)

2-9: Consider consolidating SDK imports, if ye don't mind.

These two import statements from @hyperlane-xyz/sdk could be merged into one. It's a small thing, like layers on an onion, but keeps things tidy.

🧅 Proposed consolidation
-import type {
-  MetadataBuildResult,
-  MultisigMetadataBuildResult,
-  AggregationMetadataBuildResult,
-  RoutingMetadataBuildResult,
-  ValidatorInfo,
-} from '@hyperlane-xyz/sdk';
-import { IsmType } from '@hyperlane-xyz/sdk';
+import {
+  IsmType,
+  type MetadataBuildResult,
+  type MultisigMetadataBuildResult,
+  type AggregationMetadataBuildResult,
+  type RoutingMetadataBuildResult,
+  type ValidatorInfo,
+} from '@hyperlane-xyz/sdk';

48-50: Use clsx() for conditional classNames, as per the rules of the swamp.

Per coding guidelines, conditional className assignment should use clsx() rather than template literals. Makes things cleaner when more conditions pile up like donkeys in me home.

🧅 Proposed fix

Add the import at the top of the file:

import clsx from 'clsx';

Then apply this change:

-      <div className={`space-y-2 ${blur ? 'blur-xs' : ''}`}>
+      <div className={clsx('space-y-2', blur && 'blur-xs')}>

67-70: Use clsx() here too, and mind the line lengths.

This section has conditional classNames that'd benefit from clsx(). Also, line 69 is gettin' a bit long for a swamp dwelling.

🧅 Proposed improvement
+import clsx from 'clsx';
+
 // Then in the component:
-    <div className={`${depth > 0 ? 'ml-4 border-l-2 border-gray-200 pl-3' : ''}`}>
+    <div className={clsx(depth > 0 && 'ml-4 border-l-2 border-gray-200 pl-3')}>
       <div
-        className={`flex items-center space-x-2 py-1 ${hasChildren || isMultisig ? 'cursor-pointer' : ''}`}
+        className={clsx(
+          'flex items-center space-x-2 py-1',
+          (hasChildren || isMultisig) && 'cursor-pointer',
+        )}
         onClick={() => (hasChildren || isMultisig) && setExpanded(!expanded)}
       >

88-92: This line's longer than the walk from me swamp to Far Far Away.

Line 90 exceeds the 100 character limit specified in coding guidelines. Consider breaking it up for readability.

🧅 Proposed fix
         {/* Validator count for multisig */}
-        {isMultisig && (result as MultisigMetadataBuildResult).validators && (
-          <span className="text-xs text-gray-500">
-            ({getSignedCount(result as MultisigMetadataBuildResult)}/{(result as MultisigMetadataBuildResult).validators.length} signed, {(result as MultisigMetadataBuildResult).threshold} required)
-          </span>
-        )}
+        {isMultisig && (result as MultisigMetadataBuildResult).validators && (() => {
+          const msResult = result as MultisigMetadataBuildResult;
+          return (
+            <span className="text-xs text-gray-500">
+              ({getSignedCount(msResult)}/{msResult.validators.length} signed,{' '}
+              {msResult.threshold} required)
+            </span>
+          );
+        })()}

Or better yet, extract the cast once at the start of the multisig block.


178-186: Nested ternaries can get as tangled as onion layers.

The status icon and color logic works, but could be cleaner with a lookup object. Just a suggestion for when things get more complex, not strictly necessary.

🧅 Optional cleanup
 function ValidatorRow({ validator }: { validator: ValidatorInfo }) {
-  const statusIcon =
-    validator.status === 'signed' ? '✓' : validator.status === 'error' ? '✗' : '•';
-  const statusColor =
-    validator.status === 'signed'
-      ? 'text-green-600'
-      : validator.status === 'error'
-        ? 'text-red-500'
-        : 'text-gray-400';
+  const statusConfig: Record<string, { icon: string; color: string }> = {
+    signed: { icon: '✓', color: 'text-green-600' },
+    error: { icon: '✗', color: 'text-red-500' },
+  };
+  const { icon: statusIcon, color: statusColor } = statusConfig[validator.status] ?? {
+    icon: '•',
+    color: 'text-gray-400',
+  };

261-288: Could reuse those type guards ye already built.

The getTypeBadgeColor function duplicates the same type checks that isMultisigResult, isAggregationResult, and isRoutingResult already handle. A bit of onion-peeling to reduce repetition.

🧅 Proposed DRY improvement
 function getTypeBadgeColor(type: IsmType): string {
-  if (
-    type === IsmType.AGGREGATION ||
-    type === IsmType.STORAGE_AGGREGATION
-  ) {
+  // Create a minimal object to pass to type guards
+  const mockResult = { type } as MetadataBuildResult;
+  
+  if (isAggregationResult(mockResult)) {
     return 'bg-purple-100 text-purple-700';
   }
-  if (
-    type === IsmType.ROUTING ||
-    type === IsmType.FALLBACK_ROUTING ||
-    type === IsmType.AMOUNT_ROUTING ||
-    type === IsmType.INTERCHAIN_ACCOUNT_ROUTING
-  ) {
+  if (isRoutingResult(mockResult)) {
     return 'bg-orange-100 text-orange-700';
   }
-  if (
-    type === IsmType.MERKLE_ROOT_MULTISIG ||
-    type === IsmType.MESSAGE_ID_MULTISIG ||
-    type === IsmType.STORAGE_MERKLE_ROOT_MULTISIG ||
-    type === IsmType.STORAGE_MESSAGE_ID_MULTISIG
-  ) {
+  if (isMultisigResult(mockResult)) {
     return 'bg-blue-100 text-blue-700';
   }
   if (type === IsmType.OP_STACK) {
     return 'bg-red-100 text-red-700';
   }
   return 'bg-gray-100 text-gray-700';
 }

300-309: These casts are redundant after the type guard, like putting on boots after ye're already in the mud.

After isMultisigResult(result) returns true, TypeScript already knows result is MultisigMetadataBuildResult. The explicit cast on line 302 isn't needed. Same applies to lines 313 and 324.

🧅 Cleaner version
   // Check if this is a multisig result
   if (isMultisigResult(result)) {
-    const multisigResult = result as MultisigMetadataBuildResult;
-    if (multisigResult.validators && multisigResult.validators.length > 0) {
+    if (result.validators && result.validators.length > 0) {
       return {
-        validators: multisigResult.validators,
-        threshold: multisigResult.threshold,
+        validators: result.validators,
+        threshold: result.threshold,
       };
     }
   }

   // Check aggregation sub-modules
   if (isAggregationResult(result)) {
-    const aggResult = result as AggregationMetadataBuildResult;
-    if (aggResult.modules) {
-      for (const subModule of aggResult.modules) {
+    if (result.modules) {
+      for (const subModule of result.modules) {
         const info = extractValidatorInfo(subModule);
         if (info) return info;
       }
     }
   }

   // Check routing selected ISM
   if (isRoutingResult(result)) {
-    const routingResult = result as RoutingMetadataBuildResult;
-    if (routingResult.selectedIsm) {
-      return extractValidatorInfo(routingResult.selectedIsm);
+    if (result.selectedIsm) {
+      return extractValidatorInfo(result.selectedIsm);
     }
   }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3ff04f and b13b8f6.

📒 Files selected for processing (13)
  • next.config.js
  • package.json
  • src/features/chains/useChainMetadata.ts
  • src/features/debugger/types.ts
  • src/features/debugger/useIsmDetails.ts
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/deliveryStatus/types.ts
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/MessageDetails.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/TransactionCard.tsx
  • src/pages/api/ism-details.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • package.json
  • next.config.js
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Access state using Zustand hooks: useMultiProvider(), useChainMetadata(), useRegistry()
Use clsx() for conditional className assignment in components

Files:

  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/deliveryStatus/types.ts
  • src/features/messages/cards/TransactionCard.tsx
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/debugger/types.ts
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/debugger/useIsmDetails.ts
  • src/features/messages/MessageDetails.tsx
  • src/pages/api/ism-details.ts
  • src/features/chains/useChainMetadata.ts
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,ts,jsx,tsx}: Use single quotes for strings in code
Use trailing commas in code
Enforce 100 character line width

Files:

  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/deliveryStatus/types.ts
  • src/features/messages/cards/TransactionCard.tsx
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/debugger/types.ts
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/debugger/useIsmDetails.ts
  • src/features/messages/MessageDetails.tsx
  • src/pages/api/ism-details.ts
  • src/features/chains/useChainMetadata.ts
**/*.{js,ts,jsx,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier with import organization and Tailwind class sorting

Files:

  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/deliveryStatus/types.ts
  • src/features/messages/cards/TransactionCard.tsx
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/debugger/types.ts
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/debugger/useIsmDetails.ts
  • src/features/messages/MessageDetails.tsx
  • src/pages/api/ism-details.ts
  • src/features/chains/useChainMetadata.ts
🧠 Learnings (4)
📚 Learning: 2025-12-17T11:59:10.567Z
Learnt from: paulbalaji
Repo: hyperlane-xyz/hyperlane-explorer PR: 242
File: src/utils/yamlParsing.ts:20-41
Timestamp: 2025-12-17T11:59:10.567Z
Learning: Chain names in this repository must be lowercase as defined by the schema. Do not apply any runtime case normalization when parsing or storing chain metadata; instead, validate and rely on lowercase inputs, and ensure downstream logic preserves lowercase to maintain consistency across parsing and storage.

Applied to files:

  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/deliveryStatus/types.ts
  • src/features/debugger/types.ts
  • src/features/debugger/useIsmDetails.ts
  • src/pages/api/ism-details.ts
  • src/features/chains/useChainMetadata.ts
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Applies to **/*.{ts,tsx} : Access state using Zustand hooks: `useMultiProvider()`, `useChainMetadata()`, `useRegistry()`

Applied to files:

  • src/features/messages/cards/TransactionCard.tsx
  • src/features/chains/useChainMetadata.ts
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Use Zustand store (`src/store.ts`) to manage global state including chain metadata, MultiProtocolProvider, and warp routes

Applied to files:

  • src/features/messages/cards/TransactionCard.tsx
  • src/features/chains/useChainMetadata.ts
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Applies to src/consts/api.ts : Update API endpoints in `src/consts/api.ts`

Applied to files:

  • src/pages/api/ism-details.ts
🧬 Code graph analysis (7)
src/features/deliveryStatus/fetchDeliveryStatus.ts (2)
src/features/debugger/debugMessage.ts (1)
  • debugMessage (43-130)
src/utils/logger.ts (1)
  • logger (3-8)
src/features/deliveryStatus/types.ts (1)
src/features/debugger/types.ts (1)
  • MessageDebugResult (34-47)
src/features/messages/cards/TimelineCard.tsx (4)
src/types.ts (1)
  • Message (50-57)
src/features/debugger/types.ts (1)
  • MessageDebugResult (34-47)
src/features/messages/cards/IsmDetailsCard.tsx (1)
  • extractValidatorInfo (294-331)
src/components/layout/Card.tsx (1)
  • Card (8-12)
src/features/messages/cards/IsmDetailsCard.tsx (1)
src/components/layout/Card.tsx (1)
  • Card (8-12)
src/features/debugger/useIsmDetails.ts (2)
src/types.ts (1)
  • Message (50-57)
src/utils/logger.ts (1)
  • logger (3-8)
src/features/messages/MessageDetails.tsx (3)
src/features/debugger/useIsmDetails.ts (1)
  • useIsmDetails (49-59)
src/features/messages/cards/IsmDetailsCard.tsx (2)
  • extractValidatorInfo (294-331)
  • IsmDetailsCard (22-53)
src/features/messages/cards/TimelineCard.tsx (1)
  • TimelineCard (17-39)
src/pages/api/ism-details.ts (1)
src/utils/logger.ts (1)
  • logger (3-8)
🔇 Additional comments (14)
src/features/messages/cards/TransactionCard.tsx (2)

350-400: Nice work on this validator status display!

The progress bar with threshold marker is a solid touch - gives folks a clear picture of where things stand. The quorum logic's spot on, and the conditional stylin' between waitin' and success states makes it easy to see what's happenin' at a glance.


178-189: The loadin' state handling looks proper.

Good call showin' the spinner while validator info is bein' fetched, then switchin' to the summary once the data arrives. Keeps things smooth for the user.

src/features/deliveryStatus/types.ts (1)

8-12: This type extension fits right in.

Makin' debugResult optional for the success case is sensible - when a message is already delivered, debug info is nice-to-have rather than essential. The required fields for failing/pending states make sense since you'd actually need that info to figure out what's goin' on.

src/features/deliveryStatus/fetchDeliveryStatus.ts (2)

43-50: Good move runnin' these in parallel!

Fetchin' the transaction details and debug info at the same time instead of waitin' around - that's the smart way to do it. And wrappin' the debug call in a .catch() that returns undefined means a hiccup there won't mess up showin' the delivery status. Solid defensive approach.


52-72: Result structure looks good.

The debugResult slots in cleanly with the delivery transaction info. Matches up proper with the type definition changes in types.ts.

src/features/debugger/types.ts (1)

12-18: Lookin' good, these types are solid.

The ValidatorStatus interface is well-defined with proper optional fields for alias and error. The status union type covers the expected states nicely.

src/features/deliveryStatus/useMessageDeliveryStatus.tsx (2)

27-29: Nice simplification of the guard condition.

By removin' the delivered status check from the early return, you're now allowin' the fetch to happen for delivered messages too, which makes sense given you want that debugResult for validator info display. Layers, like an onion.


48-57: Good addition of debugResult for delivered messages.

The comment explains the intent well. Now delivered messages get the same treatment as pending/failing ones when it comes to validator info. Consistent structure across all the status branches.

src/features/debugger/useIsmDetails.ts (1)

49-58: Hook implementation looks solid.

The query key properly uses stable identifiers (msgId and origin hash) rather than the full message object, which is the right call for cache key stability. The eslint-disable is justified here. The staleTime of 30 seconds aligns with the PR's caching strategy.

One small thing - the coding guidelines mention using single quotes, and this file uses them consistently. Good donkey, good donkey.

src/features/messages/MessageDetails.tsx (2)

79-80: Clean integration of the ISM details hook.

Passin' the message only when isMessageFound is true prevents unnecessary API calls. The data flow from hook → extractValidatorInfo → child components is well organized.


152-154: Conditional rendering is the right approach.

Only showin' the ISM card when there's actual data to display. No need to clutter the UI with empty cards - keep things tidy in the swamp.

src/features/messages/cards/TimelineCard.tsx (2)

262-273: Progress bar logic is safe but could be clearer.

The division by validators.length is guarded by the parent conditional at line 162, so no division by zero here. But documentin' this assumption would help future travelers through this code. The threshold marker positioning is a nice touch for visualizin' the requirement.


315-339: Stage header logic handles all cases well.

Good coverage of all message stages with appropriate past/present tense labels. The timing display and failure state handling look correct.

src/features/messages/cards/IsmDetailsCard.tsx (1)

2-9: No action needed — the preview SDK and these types are already integrated throughout the codebase. MetadataBuildResult, ValidatorInfo, and the like are working just fine in multiple components (useIsmDetails.ts, ism-details.ts, TransactionCard.tsx, etc.), so you're in the clear on this one.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/features/chains/useChainMetadata.ts Outdated
Comment thread src/features/debugger/types.ts Outdated
Comment thread src/features/messages/cards/IsmDetailsCard.tsx
@nambrot nambrot force-pushed the nam/validator-signature-status branch from 2687fb1 to d28495b Compare January 15, 2026 04:00
Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In `@package.json`:
- Around line 19-23: The package.json pins several Hyperlane packages to
pre-release CI builds (`@hyperlane-xyz/core`, `@hyperlane-xyz/sdk`,
`@hyperlane-xyz/utils`, `@hyperlane-xyz/widgets`); either replace those preview
versions with the corresponding stable release versions (or semver ranges like
^<stable-version>) before promoting to production, or explicitly document and
gate the use of these previews by adding a comment in package.json/README and a
follow-up task in this PR to swap to stable when published; ensure the change
targets the exact keys "@hyperlane-xyz/core", "@hyperlane-xyz/sdk",
"@hyperlane-xyz/utils", and "@hyperlane-xyz/widgets" so the project is not
accidentally locked to CI-built artifacts long-term.
♻️ Duplicate comments (2)
src/pages/api/ism-details.ts (1)

34-40: The cache size issue was noted before - just making sure it's on the radar.

These Maps will grow indefinitely with unique chain/address combos. This was flagged in a previous review, so I won't belabor the point, but LRU eviction or a max size would prevent memory from creeping up over time.

src/features/messages/cards/IsmDetailsCard.tsx (1)

143-158: Division by zero concern - ogres protect their swamps, you protect your calculations.

If validators.length happens to be 0 (an empty swamp, if you will), those percentage calculations will give you Infinity%. Nobody wants infinity in their width styles.

🧹 Nitpick comments (10)
src/pages/api/ism-details.ts (4)

19-21: This any type is like mud - it'll stick around and cause problems later.

The SDK doesn't export DerivedHookConfig currently, but using any means ye lose all type safety downstream. Consider adding a more specific inline type or at least a TODO with a tracking issue.

💡 Suggested improvement
-// DerivedHookConfig is WithAddress<Exclude<HookConfig, Address>>
-// Using any for now until SDK exports this type
-type DerivedHookConfig = any;
+// TODO: Replace with proper type once SDK exports DerivedHookConfig
+// DerivedHookConfig is WithAddress<Exclude<HookConfig, Address>>
+type DerivedHookConfig = {
+  type: string;
+  address: string;
+  [key: string]: unknown;
+};

145-163: The required field validation is a bit scattered across the swamp.

originDomain is checked separately from originTxHash and messageId. Would be cleaner to validate all required fields in one spot.

♻️ Consolidate validation
     const {
       originTxHash,
       messageId,
+      originDomain,
     } = req.body;

-    if (!originTxHash || !messageId) {
-      return res.status(400).json({ error: 'Missing required fields: originTxHash, messageId' });
+    if (!originTxHash || !messageId || !originDomain) {
+      return res.status(400).json({ error: 'Missing required fields: originTxHash, messageId, originDomain' });
     }

-    const { multiProvider, core, metadataBuilder, fromCache } = await getRegistryAndCore();
-    timer.mark('getRegistryAndCore');
-    logger.info(`[TIMING] getRegistryAndCore took ${timer.getTimings().getRegistryAndCore}ms (fromCache: ${fromCache})`);
-
-    // We need to find which chain this transaction is on
-    // For now, require the origin domain to be passed
-    const { originDomain } = req.body;
-    if (!originDomain) {
-      return res.status(400).json({ error: 'Missing required field: originDomain' });
-    }

220-245: Side effects hiding in ternary expressions - that's sneakier than Donkey in a quiet moment.

The comma operator with assignments and logger calls inside ternaries works, but it's hard to follow. The logic that sets ismFromCache/hookFromCache and logs is buried in the condition branch.

♻️ Clearer structure
+    let ismFromCache = false;
+    let hookFromCache = false;
+
+    let ismConfigPromise: Promise<DerivedIsmConfig>;
+    let hookConfigPromise: Promise<DerivedHookConfig>;
+
+    if (ismCacheValid) {
+      ismFromCache = true;
+      logger.info(`[TIMING] ISM config cache HIT for ${ismCacheKey}`);
+      ismConfigPromise = Promise.resolve(cachedIsmConfig!.config);
+    } else {
+      logger.info(`[TIMING] ISM config cache MISS for ${ismCacheKey}, will derive...`);
+      ismConfigPromise = (async () => {
+        const ismConfigStart = Date.now();
+        const evmIsmReader = new EvmIsmReader(multiProvider, destinationChain, undefined, message);
+        const result = await evmIsmReader.deriveIsmConfig(recipientIsm);
+        logger.info(`[TIMING] deriveIsmConfig completed in ${Date.now() - ismConfigStart}ms`);
+        ismConfigCache.set(ismCacheKey, { config: result, timestamp: now });
+        return result;
+      })();
+    }
+
+    if (hookCacheValid) {
+      hookFromCache = true;
+      logger.info(`[TIMING] Hook config cache HIT for ${hookCacheKey}`);
+      hookConfigPromise = Promise.resolve(cachedHookConfig!.config);
+    } else {
+      logger.info(`[TIMING] Hook config cache MISS for ${hookCacheKey}, will derive...`);
+      hookConfigPromise = (async () => {
+        const hookConfigStart = Date.now();
+        const evmHookReader = new EvmHookReader(multiProvider, originChain, undefined, message);
+        const result = await evmHookReader.deriveHookConfig(senderHook);
+        logger.info(`[TIMING] deriveHookConfig completed in ${Date.now() - hookConfigStart}ms`);
+        hookConfigCache.set(hookCacheKey, { config: result, timestamp: now });
+        return result;
+      })();
+    }
-
-    let ismFromCache = false;
-    let hookFromCache = false;
-
-    // Derive configs in parallel if needed, use cache if available
-    const ismConfigPromise: Promise<DerivedIsmConfig> = ismCacheValid
-      ? (ismFromCache = true, logger.info(`[TIMING] ISM config cache HIT for ${ismCacheKey}`), Promise.resolve(cachedIsmConfig!.config))
-      : (async () => {
-          // ... existing async code
-        })();
-
-    const hookConfigPromise: Promise<DerivedHookConfig> = hookCacheValid
-      ? (hookFromCache = true, logger.info(`[TIMING] Hook config cache HIT for ${hookCacheKey}`), Promise.resolve(cachedHookConfig!.config))
-      : (async () => {
-          // ... existing async code
-        })();

268-279: The any type on response loses the shape safety.

Typing response as any means typos in property names won't be caught. Since ye control the shape, a simple inline type would help.

💡 Add inline type
-    const response: any = { result };
+    const response: { 
+      result: MetadataBuildResult; 
+      _timings?: Record<string, number>; 
+      _cache?: { registry: boolean; ismConfig: boolean; hookConfig: boolean };
+    } = { result };
src/features/messages/cards/TimelineCard.tsx (3)

17-17: The unused _debugResult parameter might confuse future visitors.

If it's intentionally unused (perhaps for future use or prop drilling), a brief comment would help. Otherwise, consider removing it from the destructure.


262-273: Careful with that division - empty arrays might sneak through.

While line 162 guards with validators.length > 0, the ValidatorDropdown component itself doesn't verify this. If someone later calls it directly with an empty array, ye'd get NaN% widths.

💡 Defensive check
       {/* Progress bar */}
       <div className="relative mb-3 h-2 w-full rounded-full bg-gray-200">
         <div
           className="absolute left-0 top-0 h-full rounded-full bg-blue-500 transition-all duration-300"
-          style={{ width: `${(signedCount / validators.length) * 100}%` }}
+          style={{ width: `${validators.length > 0 ? (signedCount / validators.length) * 100 : 0}%` }}
         />
         <div
           className="absolute top-0 h-full w-0.5 bg-gray-600"
-          style={{ left: `${(threshold / validators.length) * 100}%` }}
+          style={{ left: `${validators.length > 0 ? (threshold / validators.length) * 100 : 0}%` }}
           title={`Threshold: ${threshold}`}
         />
       </div>

277-279: Using validator.address || index as a key could cause React reconciliation issues.

If validator.address is an empty string (falsy), the key falls back to index, which can lead to incorrect component reuse when the list reorders. Consider a more robust key.

💡 Safer key pattern
-          key={validator.address || index}
+          key={`${validator.address}-${index}`}
src/features/messages/cards/IsmDetailsCard.tsx (1)

88-92: Line exceeds 100 character limit, best break it up a bit.

This line is runnin' longer than a donkey's monologue. Per the coding guidelines, we need to keep it under 100 characters.

✨ Suggested formatting
         {/* Validator count for multisig */}
         {isMultisig && (result as MultisigMetadataBuildResult).validators && (
           <span className="text-xs text-gray-500">
-            ({getSignedCount(result as MultisigMetadataBuildResult)}/{(result as MultisigMetadataBuildResult).validators.length} signed, {(result as MultisigMetadataBuildResult).threshold} required)
+            ({getSignedCount(result as MultisigMetadataBuildResult)}/
+            {(result as MultisigMetadataBuildResult).validators.length} signed,{' '}
+            {(result as MultisigMetadataBuildResult).threshold} required)
           </span>
         )}
src/features/messages/MessageDetails.tsx (2)

79-80: Consider handling loading/error states from useIsmDetails.

Right now you're only grabbin' the data from the hook, but useIsmDetails also returns isLoading, isError, and error. For ISM details that can take ~4.2s on cold cache, showing a loading indicator or handling errors gracefully could improve the user experience. Not a swamp-stopper, just somethin' to think about.

💡 Example of handling loading state
-  const { data: ismDetails } = useIsmDetails(isMessageFound ? message : null);
+  const { data: ismDetails, isLoading: isIsmLoading } = useIsmDetails(isMessageFound ? message : null);

Then you could pass isIsmLoading to components or show a subtle loading state for the ISM card.


138-140: Consider memoizing extractValidatorInfo result and wrapping that long line.

Calling extractValidatorInfo(ismDetails) inline means it runs on every render. Since it recursively traverses the ISM tree, memorizin' it would be a kindness to the render cycle. Also, line 140 is stretchin' past the 100-character guideline.

✨ Suggested improvement
+  // Memoize validator info extraction
+  const validatorInfo = useMemo(
+    () => (ismDetails ? extractValidatorInfo(ismDetails) : null),
+    [ismDetails],
+  );
+
   // ... in the JSX:
-          validatorInfo={ismDetails ? extractValidatorInfo(ismDetails) : null}
+          validatorInfo={validatorInfo}
         />
-        {showTimeline && <TimelineCard message={message} blur={blur} debugResult={debugResult} ismResult={ismDetails} />}
+        {showTimeline && (
+          <TimelineCard
+            message={message}
+            blur={blur}
+            debugResult={debugResult}
+            ismResult={ismDetails}
+          />
+        )}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b13b8f6 and d28495b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • next.config.js
  • package.json
  • src/features/chains/useChainMetadata.ts
  • src/features/debugger/types.ts
  • src/features/debugger/useIsmDetails.ts
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/deliveryStatus/types.ts
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/MessageDetails.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/TransactionCard.tsx
  • src/pages/api/ism-details.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/features/debugger/useIsmDetails.ts
  • src/features/debugger/types.ts
  • src/features/chains/useChainMetadata.ts
  • src/features/deliveryStatus/types.ts
  • src/features/messages/cards/TransactionCard.tsx
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Access state using Zustand hooks: useMultiProvider(), useChainMetadata(), useRegistry()
Use clsx() for conditional className assignment in components

Files:

  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/pages/api/ism-details.ts
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/MessageDetails.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,ts,jsx,tsx}: Use single quotes for strings in code
Use trailing commas in code
Enforce 100 character line width

Files:

  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/pages/api/ism-details.ts
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/MessageDetails.tsx
  • next.config.js
**/*.{js,ts,jsx,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier with import organization and Tailwind class sorting

Files:

  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/pages/api/ism-details.ts
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/MessageDetails.tsx
  • next.config.js
🧠 Learnings (2)
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Applies to src/consts/api.ts : Update API endpoints in `src/consts/api.ts`

Applied to files:

  • src/pages/api/ism-details.ts
📚 Learning: 2025-12-17T11:59:10.567Z
Learnt from: paulbalaji
Repo: hyperlane-xyz/hyperlane-explorer PR: 242
File: src/utils/yamlParsing.ts:20-41
Timestamp: 2025-12-17T11:59:10.567Z
Learning: Chain names in this repository must be lowercase as defined by the schema. Do not apply any runtime case normalization when parsing or storing chain metadata; instead, validate and rely on lowercase inputs, and ensure downstream logic preserves lowercase to maintain consistency across parsing and storage.

Applied to files:

  • src/pages/api/ism-details.ts
🧬 Code graph analysis (4)
src/pages/api/ism-details.ts (1)
src/utils/logger.ts (1)
  • logger (3-8)
src/features/messages/cards/TimelineCard.tsx (4)
src/types.ts (1)
  • Message (50-57)
src/features/debugger/types.ts (1)
  • MessageDebugResult (34-47)
src/features/messages/cards/IsmDetailsCard.tsx (1)
  • extractValidatorInfo (294-331)
src/components/layout/Card.tsx (1)
  • Card (8-12)
src/features/messages/cards/IsmDetailsCard.tsx (1)
src/components/layout/Card.tsx (1)
  • Card (8-12)
src/features/messages/MessageDetails.tsx (3)
src/features/debugger/useIsmDetails.ts (1)
  • useIsmDetails (49-59)
src/features/messages/cards/IsmDetailsCard.tsx (2)
  • extractValidatorInfo (294-331)
  • IsmDetailsCard (22-53)
src/features/messages/cards/TimelineCard.tsx (1)
  • TimelineCard (17-39)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (13)
next.config.js (1)

66-79: These WASM mocks look proper, like layers on an onion.

The approach of aliasing Aleo-related modules to false is the right way to stub out modules that don't play nice with Next.js bundling. Applying these outside the isServer block means both client and server get consistent behavior, which is what ye want.

The asset/resource rule for .wasm files is also sound - it prevents webpack from trying to parse the binary content.

src/features/deliveryStatus/useMessageDeliveryStatus.tsx (1)

48-57: Good thinking including debug results for delivered messages too.

Previously, delivered messages wouldn't get the validator info since the debug data was only returned for failing/pending states. Now the UI can show who signed even after the message lands safely at its destination - makes perfect sense for this feature.

package.json (1)

96-97: The SDK override is the right call here.

Adding the explicit pnpm override for @hyperlane-xyz/sdk ensures all nested dependencies resolve to the same preview version. Without this, ye might end up with version mismatches in the node_modules swamp.

src/pages/api/ism-details.ts (1)

71-135: The registry initialization is solid work.

Good use of timing instrumentation for performance debugging. The parallel address fetching with graceful skipping for chains without addresses is sensible. The cache layer with TTL keeps things snappy on subsequent requests.

src/features/messages/cards/TimelineCard.tsx (2)

57-171: The EnhancedMessageTimeline is well-structured overall.

The stage progression logic, validator quorum display, and expandable details work together nicely. The visual hierarchy with icons, bars, and chevrons gives users a clear sense of where their message stands in the journey. The quorum badge (signedCount/total (threshold req)) is informative.


238-313: ValidatorDropdown component handles the signature status display well.

Good accessibility considerations with the status indicators (✓, ✗, ○) paired with text labels. The progress bar with threshold marker is a nice touch for visualizing quorum progress.

src/features/messages/cards/IsmDetailsCard.tsx (5)

1-20: LGTM!

Imports are properly organized and the Props interface is clean with appropriate typing. Using preview SDK version types for MetadataBuildResult and friends.


22-53: LGTM!

The main component has a proper guard clause at the start, and the layout is well-structured with the Card wrapper. The blur prop is applied correctly to the tree container.


178-208: LGTM!

ValidatorRow handles the three states nicely with appropriate visual feedback. The error tooltip via the title attribute gets the job done without overcomplicating things.


238-259: Nice type-to-name mapping with a sensible fallback.

The getIsmTypeName function handles unknown types gracefully by returning the raw type value. That's good defensive coding - like layers, it's got you covered.


290-331: Well-documented recursive traversal, returns the first multisig found.

The JSDoc clearly states this returns the first multisig validator list. For complex aggregation ISMs with multiple multisig modules, only one will be surfaced. That's probably fine for timeline display purposes, but just somethin' to keep in mind if downstream needs change.

src/features/messages/MessageDetails.tsx (2)

14-21: LGTM!

Imports are properly organized. The new hook and helper function are pulled in from their respective homes.


152-154: LGTM!

The conditional wrapper is a bit redundant since IsmDetailsCard already guards against null, but it makes the render intent crystal clear. No harm done.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread package.json Outdated
Copy link
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In `@src/features/messages/cards/TimelineCard.tsx`:
- Around line 271-282: The progress bar width and threshold position
calculations divide by validators.length, which will produce Infinity when
validators.length is 0; update TimelineCard (the JSX block using signedCount,
validators, and threshold) to guard against zero-length validators by computing
a safe denominator (e.g., denom = validators.length || 1) or conditionally
rendering/setting percentage to 0 when validators.length === 0, and use that
safe percentage for the style.width and style.left to avoid broken styling.
♻️ Duplicate comments (2)
src/pages/api/ism-details.ts (1)

34-40: Cache size management was flagged before - still worth keeping an eye on.

The ISM and Hook config caches remain unbounded. A previous review already suggested adding a max size with LRU eviction. Just a reminder this is still pending.

src/features/messages/cards/IsmDetailsCard.tsx (1)

143-158: Seen this one before - division by zero still lurkin' like Lord Farquaad's guards.

The percentage calculations on lines 147 and 155 can result in division by zero if validators.length is 0. This was flagged in a previous review and should be addressed with guards.

🧹 Nitpick comments (11)
src/features/messages/cards/TransactionCard.tsx (1)

373-386: Division by zero lurking in the component if validators array is empty.

Even though the parent guards against rendering with empty validators, the ValidatorStatusSummary component itself doesn't protect against validators.length === 0. If this component gets reused elsewhere without the same guard, you'll get NaN or Infinity in the styles.

♻️ Defensive fix
 function ValidatorStatusSummary({
   validators,
   threshold,
 }: {
   validators: ValidatorInfo[];
   threshold: number;
 }) {
+  if (validators.length === 0) return null;
+
   const signedCount = validators.filter((v) => v.status === 'signed').length;
   const hasQuorum = signedCount >= threshold && threshold > 0;
+  const progressPercent = (signedCount / validators.length) * 100;
+  const thresholdPercent = (threshold / validators.length) * 100;

   return (
     <div className="mt-4 w-full max-w-xs">
       {/* ... */}
       <div className="relative h-2 w-full rounded-full bg-gray-200">
         {/* Threshold marker */}
         <div
           className="absolute top-0 h-full w-0.5 bg-gray-600"
-          style={{ left: `${(threshold / validators.length) * 100}%` }}
+          style={{ left: `${thresholdPercent}%` }}
           title={`Threshold: ${threshold}`}
         />
         {/* Signed progress */}
         <div
           className={`absolute left-0 top-0 h-full rounded-full transition-all duration-300 ${
             hasQuorum ? 'bg-green-500' : 'bg-blue-500'
           }`}
-          style={{ width: `${(signedCount / validators.length) * 100}%` }}
+          style={{ width: `${progressPercent}%` }}
         />
       </div>
src/pages/api/ism-details.ts (5)

19-21: The any type here is a bit like dumping everything in the swamp and hoping for the best.

Using any for DerivedHookConfig bypasses type checking. If the SDK doesn't export this type, consider creating a local interface that matches the expected shape, or at least use unknown for safer handling.


108-117: Sequential fetching of chain addresses - could slow things down on cold starts.

This loop fetches addresses one chain at a time. With many chains, this becomes a bottleneck. Consider batching or parallelizing these calls.

♻️ Parallel approach
-  for (const chainName of chainNames) {
-    try {
-      const addresses = await registry.getChainAddresses(chainName);
-      if (addresses) {
-        addressesMap[chainName] = addresses;
-      }
-    } catch (_e) {
-      // Skip chains without addresses
-    }
-  }
+  const addressResults = await Promise.allSettled(
+    chainNames.map(async (chainName) => {
+      const addresses = await registry.getChainAddresses(chainName);
+      return { chainName, addresses };
+    })
+  );
+  for (const result of addressResults) {
+    if (result.status === 'fulfilled' && result.value.addresses) {
+      addressesMap[result.value.chainName] = result.value.addresses;
+    }
+  }

220-232: This comma expression in the ternary is doing a lot of work in one line.

The pattern (ismFromCache = true, logger.info(...), Promise.resolve(...)) is clever but makes ogres squint. Consider extracting this into a helper function or using a clearer conditional structure.

♻️ Clearer alternative
-    const ismConfigPromise: Promise<DerivedIsmConfig> = ismCacheValid
-      ? (ismFromCache = true, logger.info(`[TIMING] ISM config cache HIT for ${ismCacheKey}`), Promise.resolve(cachedIsmConfig!.config))
-      : (async () => {
+    let ismConfigPromise: Promise<DerivedIsmConfig>;
+    if (ismCacheValid) {
+      ismFromCache = true;
+      logger.info(`[TIMING] ISM config cache HIT for ${ismCacheKey}`);
+      ismConfigPromise = Promise.resolve(cachedIsmConfig!.config);
+    } else {
+      ismConfigPromise = (async () => {
           logger.info(`[TIMING] ISM config cache MISS for ${ismCacheKey}, will derive...`);
           // ... rest of the async function
         })();
+    }

145-152: Validation could be done upfront in one go.

The required field validation is split - originTxHash and messageId are checked first, then originDomain is checked later at line 161. Consolidating these checks would make the validation logic clearer.

♻️ Consolidated validation
     const {
       originTxHash,
       messageId,
+      originDomain,
     } = req.body;

-    if (!originTxHash || !messageId) {
-      return res.status(400).json({ error: 'Missing required fields: originTxHash, messageId' });
+    if (!originTxHash || !messageId || !originDomain) {
+      return res.status(400).json({ 
+        error: 'Missing required fields: originTxHash, messageId, originDomain' 
+      });
     }
-
-    const { multiProvider, core, metadataBuilder, fromCache } = await getRegistryAndCore();
-    timer.mark('getRegistryAndCore');
-    logger.info(`[TIMING] getRegistryAndCore took ${timer.getTimings().getRegistryAndCore}ms (fromCache: ${fromCache})`);
-
-    // We need to find which chain this transaction is on
-    // For now, require the origin domain to be passed
-    const { originDomain } = req.body;
-    if (!originDomain) {
-      return res.status(400).json({ error: 'Missing required field: originDomain' });
-    }

268-277: Debug info exposure looks fine, but the any type could be tightened.

Using any for response loses type safety. Consider defining a proper response type.

interface IsmDetailsApiResponse {
  result: MetadataBuildResult;
  _timings?: Record<string, number>;
  _cache?: {
    registry: boolean;
    ismConfig: boolean;
    hookConfig: boolean;
  };
}
src/features/messages/cards/TimelineCard.tsx (2)

17-17: That _debugResult is sittin' there like a donkey with nothin' to do.

The debugResult prop is destructured with an underscore prefix indicating it's unused. If this was left for future use, consider documenting the intent. Otherwise, it could be removed from the Props interface to avoid confusion.


216-229: Emoji icons might look different in different kingdoms.

Emojis render inconsistently across browsers and operating systems. For a polished, consistent look, consider using SVG icons instead. That said, if this is good enough for now, it works.

src/features/messages/cards/IsmDetailsCard.tsx (1)

88-92: That line's stretchin' longer than Farquaad's ego.

Line 90 exceeds the 100-character line width guideline. Consider breaking it up for readability.

♻️ Proposed formatting
         {isMultisig && (result as MultisigMetadataBuildResult).validators && (
           <span className="text-xs text-gray-500">
-            ({getSignedCount(result as MultisigMetadataBuildResult)}/{(result as MultisigMetadataBuildResult).validators.length} signed, {(result as MultisigMetadataBuildResult).threshold} required)
+            ({getSignedCount(result as MultisigMetadataBuildResult)}/
+            {(result as MultisigMetadataBuildResult).validators.length} signed,{' '}
+            {(result as MultisigMetadataBuildResult).threshold} required)
           </span>
         )}
src/features/messages/MessageDetails.tsx (2)

79-80: The hook usage is fine, but there might be somethin' in me swamp water...

The query is correctly gated by isMessageFound. However, you're only destructuring data - consider whether isLoading or isError states from the hook should be used to show loading indicators or error states for the ISM section specifically.


140-140: That line's got more packed in than me swamp on a busy day.

Line 140 exceeds the 100-character guideline. Consider breaking the props across multiple lines.

♻️ Suggested formatting
-        {showTimeline && <TimelineCard message={message} blur={blur} debugResult={debugResult} ismResult={ismDetails} />}
+        {showTimeline && (
+          <TimelineCard
+            message={message}
+            blur={blur}
+            debugResult={debugResult}
+            ismResult={ismDetails}
+          />
+        )}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d28495b and cecc7a3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • next.config.js
  • package.json
  • src/features/chains/useChainMetadata.ts
  • src/features/debugger/types.ts
  • src/features/debugger/useIsmDetails.ts
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/deliveryStatus/types.ts
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/MessageDetails.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/TransactionCard.tsx
  • src/pages/api/ism-details.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/features/chains/useChainMetadata.ts
  • package.json
  • src/features/debugger/types.ts
  • next.config.js
  • src/features/deliveryStatus/types.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Access state using Zustand hooks: useMultiProvider(), useChainMetadata(), useRegistry()
Use clsx() for conditional className assignment in components

Files:

  • src/features/debugger/useIsmDetails.ts
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/MessageDetails.tsx
  • src/pages/api/ism-details.ts
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/cards/TransactionCard.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,ts,jsx,tsx}: Use single quotes for strings in code
Use trailing commas in code
Enforce 100 character line width

Files:

  • src/features/debugger/useIsmDetails.ts
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/MessageDetails.tsx
  • src/pages/api/ism-details.ts
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/cards/TransactionCard.tsx
**/*.{js,ts,jsx,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier with import organization and Tailwind class sorting

Files:

  • src/features/debugger/useIsmDetails.ts
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/MessageDetails.tsx
  • src/pages/api/ism-details.ts
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/cards/TransactionCard.tsx
🧠 Learnings (4)
📚 Learning: 2025-12-17T11:59:10.567Z
Learnt from: paulbalaji
Repo: hyperlane-xyz/hyperlane-explorer PR: 242
File: src/utils/yamlParsing.ts:20-41
Timestamp: 2025-12-17T11:59:10.567Z
Learning: Chain names in this repository must be lowercase as defined by the schema. Do not apply any runtime case normalization when parsing or storing chain metadata; instead, validate and rely on lowercase inputs, and ensure downstream logic preserves lowercase to maintain consistency across parsing and storage.

Applied to files:

  • src/features/debugger/useIsmDetails.ts
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/pages/api/ism-details.ts
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Applies to src/consts/api.ts : Update API endpoints in `src/consts/api.ts`

Applied to files:

  • src/pages/api/ism-details.ts
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Applies to **/*.{ts,tsx} : Access state using Zustand hooks: `useMultiProvider()`, `useChainMetadata()`, `useRegistry()`

Applied to files:

  • src/features/messages/cards/TransactionCard.tsx
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Use Zustand store (`src/store.ts`) to manage global state including chain metadata, MultiProtocolProvider, and warp routes

Applied to files:

  • src/features/messages/cards/TransactionCard.tsx
🧬 Code graph analysis (6)
src/features/debugger/useIsmDetails.ts (2)
src/types.ts (1)
  • Message (50-57)
src/utils/logger.ts (1)
  • logger (3-8)
src/features/deliveryStatus/fetchDeliveryStatus.ts (2)
src/features/debugger/debugMessage.ts (1)
  • debugMessage (43-130)
src/utils/logger.ts (1)
  • logger (3-8)
src/features/messages/cards/TimelineCard.tsx (3)
src/types.ts (1)
  • Message (50-57)
src/features/debugger/types.ts (1)
  • MessageDebugResult (34-47)
src/features/messages/cards/IsmDetailsCard.tsx (1)
  • extractValidatorInfo (294-331)
src/features/messages/cards/IsmDetailsCard.tsx (1)
src/components/layout/Card.tsx (1)
  • Card (8-12)
src/features/messages/MessageDetails.tsx (3)
src/features/debugger/useIsmDetails.ts (1)
  • useIsmDetails (49-59)
src/features/messages/cards/IsmDetailsCard.tsx (2)
  • extractValidatorInfo (294-331)
  • IsmDetailsCard (22-53)
src/features/messages/cards/TimelineCard.tsx (1)
  • TimelineCard (17-39)
src/pages/api/ism-details.ts (1)
src/utils/logger.ts (1)
  • logger (3-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (18)
src/features/deliveryStatus/fetchDeliveryStatus.ts (2)

43-50: Nicely done with the parallel fetch, like doing two things at once in me swamp.

The Promise.all approach here is solid - fetching transaction details and debug info concurrently instead of sequentially. The .catch() wrapper on debugMessage ensures a failed debug lookup doesn't swamp the whole delivery status response.


88-96: DomainId type is already available globally, no import needed.

DomainId is declared globally in src/global.d.ts as declare type DomainId = number, so it's accessible throughout the codebase without needing to import it. The code compiles without any TypeScript errors.

Likely an incorrect or invalid review comment.

src/features/deliveryStatus/useMessageDeliveryStatus.tsx (2)

48-57: Good to see the delivered path getting the same treatment as the others.

Now all delivery status branches (Delivered, Pending, Failing) consistently expose debugResult. This makes the downstream consumer code cleaner since they don't have to handle different shapes. Like having a consistent recipe for all the onion layers.


87-91: Public API expanded - make sure downstream swamp dwellers know about it.

The return object now includes debugResult as a new field. Any components consuming this hook will have access to the debug information for ISM/validator display.

src/features/messages/cards/TransactionCard.tsx (2)

1-4: Type import looks proper.

Importing ValidatorInfo from the SDK to type the validator data. Clean and correct.


178-189: Validator status display - nice addition for pending messages.

The conditional rendering properly guards against empty validators array before showing the summary, and falls back to a spinner when validator data isn't available yet. Good user experience, like knowing when to wait for the onions to caramelize.

src/features/debugger/useIsmDetails.ts (2)

49-58: Hook implementation looks proper, like a well-organized swamp.

Good use of React Query with:

  • Appropriate query key including message ID and origin hash
  • Proper enabled condition to prevent unnecessary fetches
  • Reasonable staleTime and retry settings

The eslint-disable comment suggests you've considered the deps - just make sure the query key truly captures all the reactive values that should trigger a refetch.


35-39: Null handling is already properly taken care of here.

The consuming components already check for null (e.g., line 138 uses ismDetails ? extractValidatorInfo(ismDetails) : null) and the extractValidatorInfo utility gracefully handles null/undefined values. When the API call fails, errors are logged at line 37, and the UI simply doesn't render the IsmDetailsCard—the message details still display, just without the ISM-specific bits. Not much left to worry about on this one.

Likely an incorrect or invalid review comment.

src/features/messages/cards/TimelineCard.tsx (4)

1-15: Imports and Props look good, donkey.

The imports are properly organized, and the new props debugResult and ismResult extend the component's capability to handle ISM validation data nicely. The use of optional types keeps backwards compatibility intact.


182-214: StageBar's put together nicely, like a well-built swamp cottage.

The component properly handles first/last stage styling and delegates opacity calculation to a helper. The absolute positioning for icons and chevrons is clean.


324-359: Helper functions are solid as a boulder blockin' my swamp entrance.

The stage header logic properly handles timing display and failure states. The opacity class function cleanly determines visual feedback based on progress.


66-79: Stage override logic is solid and the numeric enum assumption checks out.

The comparison _stage < MessageStage.Validated at line 76 relies on MessageStage being a numeric enum, and this assumption is validated across the codebase. The getStageHeader function consistently uses numeric comparisons like currentStage >= targetStage, which would only work if MessageStage is numeric. Since MessageStage comes from @hyperlane-xyz/widgets, the enum ordering is fixed upstream and your logic safely assumes it increases with progression.

src/features/messages/cards/IsmDetailsCard.tsx (4)

22-53: IsmDetailsCard's got good structure, like me swamp after a spring cleanin'.

The early return for null result is proper defensive coding. The component cleanly delegates to IsmTreeNode for recursive rendering.


178-208: ValidatorRow's tidy enough for me likin'.

Clean status icon mapping and conditional badge rendering. The component handles all validator states properly.


210-288: These helper functions know what they're doin', like Donkey knows how to talk too much.

Type guards are comprehensive, covering all multisig, aggregation, and routing ISM variants. The type name and badge color mappings provide good coverage for the ISM ecosystem.


290-331: extractValidatorInfo does its job well, traversin' the tree like I traverse me swamp.

The recursive traversal correctly finds the first multisig validator list through aggregation and routing structures. The early return pattern is efficient.

src/features/messages/MessageDetails.tsx (2)

14-14: Good import additions.

The useIsmDetails hook and extractValidatorInfo are properly imported for the new ISM integration.


152-154: IsmDetailsCard integration looks proper.

Conditional rendering ensures the card only appears when ISM data is available. The result prop correctly maps to the updated component interface.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/features/messages/cards/TimelineCard.tsx
Copy link
Copy Markdown

@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: 2

🤖 Fix all issues with AI agents
In `@src/features/debugger/types.ts`:
- Around line 12-18: The ValidatorStatus interface references the Address type
but no import exists; add a type-only import for Address (e.g. import type {
Address } from '@hyperlane-xyz/utils') at the top of the file so the
ValidatorStatus interface (and fields like address: Address) compile correctly.

In `@src/pages/api/ism-details.ts`:
- Around line 280-284: In the catch block that currently does "catch (error:
any) { ... return res.status(500).json({ error: error.message || 'Internal
server error' }); }", stop returning raw error.message to clients; instead
return a generic message (e.g. 'Internal server error') and keep the full error
and timings in logger.error for diagnostics. If you need richer client feedback
in non-production, conditionally include a sanitized error detail only when
NODE_ENV === 'development' (avoid sending stacks or third‑party text). Update
the code that builds the response in the catch block (the error variable and the
res.status(500).json call) to implement this sanitized/conditional behavior.
♻️ Duplicate comments (5)
package.json (1)

96-97: The SDK override ensures all nested dependencies use the same preview version.

This keeps things consistent in the swamp, so there's no version mismatches lurking beneath the surface. Just remember to remove this override when swapping to stable releases.

src/pages/api/ism-details.ts (1)

38-48: Cache key design looks proper - addresses are lowercased consistently.

The ISM cache key correctly includes both destination and origin chains (routing ISMs need origin context), and addresses are normalized. Good thinking ahead there.

The unbounded cache growth concern was already noted in a previous review - that still applies.

src/features/debugger/types.ts (1)

20-32: IsmDetails appears to be redundant with SDK's MetadataBuildResult.

Listen, this type sits here like a decoration in my swamp. Looking at how the data actually flows through your components—MessageDetails fetches via useIsmDetails and passes MetadataBuildResult directly to IsmDetailsCard, TimelineCard, and extractValidatorInfo. The ismDetails field on MessageDebugResult doesn't appear to be populated or consumed anywhere meaningful.

Consider either removing this interface and the ismDetails property from MessageDebugResult, or actually wiring it up if there's a use case I'm not seeing.

src/features/messages/cards/TimelineCard.tsx (1)

271-282: Division by zero still unguarded internally.

I know the parent checks validators.length > 0 before rendering this, but if this component ever gets reused elsewhere without that guard, you'll end up with Infinity% widths. Add an internal check to keep things safe—better to be cautious than to step in something unpleasant.

🧅 Proposed safeguard
       {/* Progress bar */}
       <div className="relative mb-3 h-2 w-full rounded-full bg-gray-200">
-        <div
-          className="absolute left-0 top-0 h-full rounded-full bg-blue-500 transition-all duration-300"
-          style={{ width: `${(signedCount / validators.length) * 100}%` }}
-        />
-        <div
-          className="absolute top-0 h-full w-0.5 bg-gray-600"
-          style={{ left: `${(threshold / validators.length) * 100}%` }}
-          title={`Threshold: ${threshold}`}
-        />
+        {validators.length > 0 && (
+          <>
+            <div
+              className="absolute left-0 top-0 h-full rounded-full bg-blue-500 transition-all duration-300"
+              style={{ width: `${(signedCount / validators.length) * 100}%` }}
+            />
+            <div
+              className="absolute top-0 h-full w-0.5 bg-gray-600"
+              style={{ left: `${(threshold / validators.length) * 100}%` }}
+              title={`Threshold: ${threshold}`}
+            />
+          </>
+        )}
       </div>
src/features/messages/cards/IsmDetailsCard.tsx (1)

143-158: Division by zero in progress bar calculations.

Same issue I mentioned before—if validators.length is zero, those percentage calculations will produce Infinity. The threshold marker at line 147 and signed progress at line 155 both divide by validators.length without checking first. Add a guard to keep things from blowing up.

🧅 Proposed safeguard
       <div className="flex items-center space-x-2">
         <div className="relative h-2 flex-1 rounded-full bg-gray-200">
           {/* Threshold marker */}
-          <div
-            className="absolute left-0 top-0 h-full rounded-full bg-blue-200"
-            style={{ width: `${(threshold / validators.length) * 100}%` }}
-          />
+          {validators.length > 0 && (
+            <div
+              className="absolute left-0 top-0 h-full rounded-full bg-blue-200"
+              style={{ width: `${(threshold / validators.length) * 100}%` }}
+            />
+          )}
           {/* Signed progress */}
-          {signedCount > 0 && (
+          {signedCount > 0 && validators.length > 0 && (
             <div
               className={`absolute left-0 top-0 h-full rounded-full transition-all duration-300 ${
                 hasQuorum ? 'bg-green-500' : 'bg-blue-500'
               }`}
               style={{ width: `${(signedCount / validators.length) * 100}%` }}
             />
           )}
         </div>
🧹 Nitpick comments (10)
src/pages/api/ism-details.ts (4)

19-21: Track this any type for when the SDK exports the proper type.

Using any here is like having an onion with a few layers missing - it works but loses some of the good stuff. When the SDK stabilizes and exports DerivedHookConfig, swap this out for proper type safety.


108-117: Consider parallelizing chain address fetches if cold start time becomes a problem.

This loop runs sequentially for all chains, which adds up on cache misses. Since these are independent requests, Promise.all could speed things up:

♻️ Optional parallel approach
-  for (const chainName of chainNames) {
-    try {
-      const addresses = await registry.getChainAddresses(chainName);
-      if (addresses) {
-        addressesMap[chainName] = addresses;
-      }
-    } catch (_e) {
-      // Skip chains without addresses
-    }
-  }
+  await Promise.all(
+    chainNames.map(async (chainName) => {
+      try {
+        const addresses = await registry.getChainAddresses(chainName);
+        if (addresses) {
+          addressesMap[chainName] = addresses;
+        }
+      } catch (_e) {
+        // Skip chains without addresses
+      }
+    })
+  );

That said, with a 1-minute cache this only matters for cold starts, so it's not urgent - just something to consider if startup latency becomes a bother.


220-232: The comma operator trick here is clever but might confuse folks later.

Using (ismFromCache = true, logger.info(...), Promise.resolve(...)) works fine, but it's a bit like finding an ogre living in a swamp - unexpected. A more readable approach would be explicit if/else blocks, but this isn't wrong, just unconventional.


145-152: Input validation could happen all at once instead of in stages.

The handler checks originTxHash and messageId first (line 150), then originDomain separately (line 161). Combining these into a single validation block would give cleaner error messages and fail faster:

♻️ Consolidated validation
    const {
      originTxHash,
      messageId,
+     originDomain,
    } = req.body;

-   if (!originTxHash || !messageId) {
-     return res.status(400).json({ error: 'Missing required fields: originTxHash, messageId' });
+   if (!originTxHash || !messageId || !originDomain) {
+     return res.status(400).json({ 
+       error: 'Missing required fields: originTxHash, messageId, originDomain' 
+     });
    }

    const { multiProvider, core, metadataBuilder, fromCache } = await getRegistryAndCore();
    timer.mark('getRegistryAndCore');
    logger.info(`[TIMING] getRegistryAndCore took ${timer.getTimings().getRegistryAndCore}ms (fromCache: ${fromCache})`);

-   // We need to find which chain this transaction is on
-   // For now, require the origin domain to be passed
-   const { originDomain } = req.body;
-   if (!originDomain) {
-     return res.status(400).json({ error: 'Missing required field: originDomain' });
-   }
src/features/debugger/types.ts (1)

54-60: Local IsmModuleTypes vs SDK's IsmType.

You've got this local enum here, but IsmDetailsCard.tsx imports and uses IsmType from @hyperlane-xyz/sdk. If these serve the same purpose, might want to consolidate to avoid maintaining two separate type definitions. Less layers, like a proper onion.

src/features/messages/cards/TransactionCard.tsx (1)

373-387: Consider an internal guard against empty validators.

Right now, the parent component guards against empty validators before rendering this. But if someone else calls ValidatorStatusSummary directly without that check, you'll get Infinity% styles. A little defensive check wouldn't hurt—better than getting burned by dragon fire later.

🧅 Suggested safeguard
       {/* Progress bar */}
       <div className="relative h-2 w-full rounded-full bg-gray-200">
+        {validators.length > 0 && (
+          <>
-        {/* Threshold marker */}
-        <div
-          className="absolute top-0 h-full w-0.5 bg-gray-600"
-          style={{ left: `${(threshold / validators.length) * 100}%` }}
-          title={`Threshold: ${threshold}`}
-        />
-        {/* Signed progress */}
-        <div
-          className={`absolute left-0 top-0 h-full rounded-full transition-all duration-300 ${
-            hasQuorum ? 'bg-green-500' : 'bg-blue-500'
-          }`}
-          style={{ width: `${(signedCount / validators.length) * 100}%` }}
-        />
+            {/* Threshold marker */}
+            <div
+              className="absolute top-0 h-full w-0.5 bg-gray-600"
+              style={{ left: `${(threshold / validators.length) * 100}%` }}
+              title={`Threshold: ${threshold}`}
+            />
+            {/* Signed progress */}
+            <div
+              className={`absolute left-0 top-0 h-full rounded-full transition-all duration-300 ${
+                hasQuorum ? 'bg-green-500' : 'bg-blue-500'
+              }`}
+              style={{ width: `${(signedCount / validators.length) * 100}%` }}
+            />
+          </>
+        )}
       </div>
src/features/messages/MessageDetails.tsx (1)

140-140: Line exceeds 100 character limit.

This line's got more characters than a village full of fairy tale creatures. Per coding guidelines, break it up to stay under 100 characters.

🧅 Suggested formatting
-        {showTimeline && <TimelineCard message={message} blur={blur} debugResult={debugResult} ismResult={ismDetails} />}
+        {showTimeline && (
+          <TimelineCard
+            message={message}
+            blur={blur}
+            debugResult={debugResult}
+            ismResult={ismDetails}
+          />
+        )}
src/features/messages/cards/TimelineCard.tsx (2)

17-17: Unused debugResult parameter.

You're accepting debugResult but not using it (hence the underscore). If it's meant for future use, that's fine—maybe add a comment. Otherwise, consider removing it from the props to keep the interface clean.


216-229: Emoji icons may render inconsistently.

Those emoji characters (, 🔒, 🛡, ) might look different across browsers and operating systems. If consistent visuals matter, consider using SVG icons instead. But hey, sometimes a simple emoji gets the job done without overthinking it.

src/features/messages/cards/IsmDetailsCard.tsx (1)

88-92: Repeated type casting is a bit verbose.

You're casting result as MultisigMetadataBuildResult multiple times within the same conditional block. Since you already checked isMultisig, consider extracting to a typed variable once at the start of the block. Less repetition, cleaner code—like peeling an onion once instead of multiple times.

🧅 Suggested cleanup
+        {/* Validator count for multisig */}
+        {isMultisig && (() => {
+          const msResult = result as MultisigMetadataBuildResult;
+          return msResult.validators && (
+            <span className="text-xs text-gray-500">
+              ({getSignedCount(msResult)}/{msResult.validators.length} signed, {msResult.threshold} required)
+            </span>
+          );
+        })()}
-        {/* Validator count for multisig */}
-        {isMultisig && (result as MultisigMetadataBuildResult).validators && (
-          <span className="text-xs text-gray-500">
-            ({getSignedCount(result as MultisigMetadataBuildResult)}/{(result as MultisigMetadataBuildResult).validators.length} signed, {(result as MultisigMetadataBuildResult).threshold} required)
-          </span>
-        )}

Or extract the multisig-specific rendering to a helper function.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cecc7a3 and 011af5a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • next.config.js
  • package.json
  • src/features/chains/useChainMetadata.ts
  • src/features/debugger/types.ts
  • src/features/debugger/useIsmDetails.ts
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
  • src/features/deliveryStatus/types.ts
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
  • src/features/messages/MessageDetails.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/TransactionCard.tsx
  • src/pages/api/ism-details.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/features/debugger/useIsmDetails.ts
  • src/features/chains/useChainMetadata.ts
  • src/features/deliveryStatus/fetchDeliveryStatus.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Access state using Zustand hooks: useMultiProvider(), useChainMetadata(), useRegistry()
Use clsx() for conditional className assignment in components

Files:

  • src/pages/api/ism-details.ts
  • src/features/deliveryStatus/types.ts
  • src/features/messages/MessageDetails.tsx
  • src/features/messages/cards/TransactionCard.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/debugger/types.ts
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,ts,jsx,tsx}: Use single quotes for strings in code
Use trailing commas in code
Enforce 100 character line width

Files:

  • src/pages/api/ism-details.ts
  • src/features/deliveryStatus/types.ts
  • src/features/messages/MessageDetails.tsx
  • src/features/messages/cards/TransactionCard.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/debugger/types.ts
  • next.config.js
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
**/*.{js,ts,jsx,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier with import organization and Tailwind class sorting

Files:

  • src/pages/api/ism-details.ts
  • src/features/deliveryStatus/types.ts
  • src/features/messages/MessageDetails.tsx
  • src/features/messages/cards/TransactionCard.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/debugger/types.ts
  • next.config.js
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/deliveryStatus/useMessageDeliveryStatus.tsx
🧠 Learnings (4)
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Applies to src/consts/api.ts : Update API endpoints in `src/consts/api.ts`

Applied to files:

  • src/pages/api/ism-details.ts
📚 Learning: 2025-12-17T11:59:10.567Z
Learnt from: paulbalaji
Repo: hyperlane-xyz/hyperlane-explorer PR: 242
File: src/utils/yamlParsing.ts:20-41
Timestamp: 2025-12-17T11:59:10.567Z
Learning: Chain names in this repository must be lowercase as defined by the schema. Do not apply any runtime case normalization when parsing or storing chain metadata; instead, validate and rely on lowercase inputs, and ensure downstream logic preserves lowercase to maintain consistency across parsing and storage.

Applied to files:

  • src/pages/api/ism-details.ts
  • src/features/deliveryStatus/types.ts
  • src/features/debugger/types.ts
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Applies to **/*.{ts,tsx} : Access state using Zustand hooks: `useMultiProvider()`, `useChainMetadata()`, `useRegistry()`

Applied to files:

  • src/features/messages/cards/TransactionCard.tsx
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Use Zustand store (`src/store.ts`) to manage global state including chain metadata, MultiProtocolProvider, and warp routes

Applied to files:

  • src/features/messages/cards/TransactionCard.tsx
🧬 Code graph analysis (4)
src/features/deliveryStatus/types.ts (1)
src/features/debugger/types.ts (1)
  • MessageDebugResult (34-47)
src/features/messages/MessageDetails.tsx (3)
src/features/debugger/useIsmDetails.ts (1)
  • useIsmDetails (49-59)
src/features/messages/cards/IsmDetailsCard.tsx (2)
  • extractValidatorInfo (294-331)
  • IsmDetailsCard (22-53)
src/features/messages/cards/TimelineCard.tsx (1)
  • TimelineCard (17-39)
src/features/messages/cards/IsmDetailsCard.tsx (1)
src/components/layout/Card.tsx (1)
  • Card (8-12)
src/features/messages/cards/TimelineCard.tsx (3)
src/types.ts (1)
  • Message (50-57)
src/features/debugger/types.ts (1)
  • MessageDebugResult (34-47)
src/features/messages/cards/IsmDetailsCard.tsx (1)
  • extractValidatorInfo (294-331)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: lint
🔇 Additional comments (19)
next.config.js (1)

67-79: LGTM - Solid approach to keepin' those pesky WASM modules out of the swamp.

The aliasing to false is the right move for excluding problematic WASM modules from both client and server bundles. The asset/resource rule for .wasm files ensures they won't cause bundling headaches.

One small thing worth knowing: if any code actually tries to import these modules at runtime (not just build time), it'll get false back and likely throw. Make sure any code paths that touch Aleo are properly guarded or dead-code eliminated.

src/pages/api/ism-details.ts (2)

50-69: Nice little timing helper - simple and gets the job done.


287-291: LGTM - 60 seconds gives enough breathing room for registry loading on cold starts.

The comment about the Pro plan requirement is helpful context.

src/features/deliveryStatus/types.ts (1)

8-12: LGTM - Making debugResult optional for successful deliveries makes sense.

Delivered messages might not always have (or need) the full debug details, unlike failing or pending ones where it's essential for understanding what's going wrong. Good type design here.

src/features/deliveryStatus/useMessageDeliveryStatus.tsx (2)

48-62: Nice improvement to the destination data handling.

The logic now properly prefers backend data (which has indexed tx details) over frontend-fetched data. The hasBackendDestination check ensures we don't overwrite complete backend data with potentially incomplete frontend data.

One thing to keep in mind: if message.destination exists but hash is an empty string, hasBackendDestination will be falsy and you'll use the frontend data. That's probably fine, but worth being aware of.


92-96: Return value now includes debugResult for all statuses - good for the validator info display.

This enables downstream components to show validator signature status regardless of message delivery state, which is exactly what the PR is after.

src/features/messages/cards/TransactionCard.tsx (2)

1-4: LGTM on imports.

Clean import structure with the new ValidatorInfo type from the SDK sitting right where it belongs.


178-189: Nice defensive pattern here.

The guard checking both validatorInfo existence and validators.length > 0 before rendering ValidatorStatusSummary keeps things safe. The fallback spinner for the loading state is a good touch too.

src/features/messages/MessageDetails.tsx (3)

14-18: Clean hook and utility imports.

The new useIsmDetails hook and extractValidatorInfo utility are imported properly. Good modular approach—keeping things organized like layers in an onion.


79-80: Smart conditional fetching.

Only fetching ISM details when the message is actually found. Keeps the swamp—I mean, the network—nice and quiet when there's nothing to fetch.


152-154: Good conditional rendering.

Only showing IsmDetailsCard when we actually have data. No point showing an empty card—that's just wasting swamp real estate.

src/features/messages/cards/TimelineCard.tsx (3)

1-8: Imports look solid.

Good mix of SDK types and local utilities. Using type imports for types is the right approach.


69-79: Good stage determination logic.

The priority order makes sense: delivered status overrides everything, then quorum status can upgrade the stage. Well-documented too—I appreciate code that explains itself without needing a fairy godmother.


324-358: Helper functions are clean and focused.

getStageHeader and getStageOpacityClass do their jobs without unnecessary complexity. The failure state handling and the pulse animation for the current stage are nice touches.

src/features/messages/cards/IsmDetailsCard.tsx (5)

1-15: Solid SDK type imports.

Good selection of types from the SDK. Using type imports keeps the bundle lean.


22-53: Clean component structure.

Early return guard, proper external link attributes, and delegation to IsmTreeNode for the heavy lifting. Nice and organized.


212-232: Type guards are well-implemented.

Proper TypeScript type predicates with comprehensive coverage of ISM types. These make the rest of the code much cleaner than sprinkling type checks everywhere.


290-331: extractValidatorInfo is solid.

Good recursive traversal that handles all the nested ISM structures—multisig, aggregation, routing. The "first match wins" approach is sensible for the UI use case. JSDoc comment explains the intent clearly.


238-288: Nice type-to-display mappings.

getIsmTypeName and getTypeBadgeColor provide good UX with human-readable labels and category-based coloring. The fallback handling for unknown types keeps things from breaking if new ISM types get added to the SDK.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/features/debugger/types.ts Outdated
Comment thread src/pages/api/ism-details.ts
Copy link
Copy Markdown

@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: 2

🤖 Fix all issues with AI agents
In `@src/features/messages/cards/IsmDetailsCard.tsx`:
- Around line 225-247: The three type-guard functions isMultisigResult,
isAggregationResult, and isRoutingResult reference IsmType members that don't
exist and omit real enum members; update each guard to remove non-existent keys
(STORAGE_MERKLE_ROOT_MULTISIG, STORAGE_MESSAGE_ID_MULTISIG, STORAGE_AGGREGATION,
FALLBACK_ROUTING, AMOUNT_ROUTING, INTERCHAIN_ACCOUNT_ROUTING) and instead
include the actual IsmType values from the SDK such as LEGACY_MULTISIG,
WEIGHTED_MERKLE_ROOT_MULTISIG, WEIGHTED_MESSAGE_ID_MULTISIG for multisig, the
correct aggregation types (e.g., AGGREGATION if present), and include routing
variants like ROUTING, CCIP_READ, ARB_L2_TO_L1, OP_L2_TO_L1, NULL/UNUSED
handling as appropriate; adjust the type predicates' OR-clauses in
isMultisigResult, isAggregationResult, and isRoutingResult to exactly match the
SDK's IsmType enum so the runtime checks and TypeScript narrowing align with the
real enum.

In `@src/features/messages/cards/TransactionCard.tsx`:
- Around line 350-398: ValidatorStatusSummary can divide by validators.length
and produce Infinity% when validators is empty; fix by adding a defensive total
variable (e.g., const total = validators.length || 1) or conditional logic so
the threshold marker and signed progress use a safe denominator (or render
width/left as "0%") when validators.length === 0; update the calculations used
in the style props for left and width (the elements computing `${(threshold /
validators.length) * 100}%` and `${(signedCount / validators.length) * 100}%`)
to use that safe total or an explicit zero case so the component never renders
Infinity.
♻️ Duplicate comments (4)
src/pages/api/ism-details.ts (2)

34-48: Unbounded cache concern already noted.

This was flagged in a previous review - the caches can grow without bounds. Won't repeat meself like a broken record.


294-298: Error message exposure already flagged.

This was noted in a prior review - returning error.message directly could leak internals. Not gonna repeat what's already been said.

src/features/messages/cards/IsmDetailsCard.tsx (1)

156-171: Division by zero still hiding in this swamp too.

Same issue as the past review flagged - lines 160 and 168 divide by validators.length without a guard. While ValidatorList is only called when validators exist (line 104 checks validators), the array could theoretically be empty.

🧅 Defensive fix
       <div className="flex items-center space-x-2">
         <div className="relative h-2 flex-1 rounded-full bg-gray-200">
-          {/* Threshold marker */}
-          <div
-            className="absolute left-0 top-0 h-full rounded-full bg-blue-200"
-            style={{ width: `${(threshold / validators.length) * 100}%` }}
-          />
-          {/* Signed progress */}
-          {signedCount > 0 && (
-            <div
-              className={`absolute left-0 top-0 h-full rounded-full transition-all duration-300 ${
-                hasQuorum ? 'bg-green-500' : 'bg-blue-500'
-              }`}
-              style={{ width: `${(signedCount / validators.length) * 100}%` }}
-            />
-          )}
+          {validators.length > 0 && (
+            <>
+              {/* Threshold marker */}
+              <div
+                className="absolute left-0 top-0 h-full rounded-full bg-blue-200"
+                style={{ width: `${(threshold / validators.length) * 100}%` }}
+              />
+              {/* Signed progress */}
+              {signedCount > 0 && (
+                <div
+                  className={`absolute left-0 top-0 h-full rounded-full transition-all duration-300 ${
+                    hasQuorum ? 'bg-green-500' : 'bg-blue-500'
+                  }`}
+                  style={{ width: `${(signedCount / validators.length) * 100}%` }}
+                />
+              )}
+            </>
+          )}
         </div>
src/features/messages/cards/TimelineCard.tsx (1)

253-264: Division by zero still lurking in the swamp.

The past review comment was marked as addressed in commit 011af5a, but the code still divides by validators.length directly at lines 257 and 261. While the parent component guards with validators.length > 0 at line 153, defensive coding here would prevent breakage if that guard changes.

🧅 Safeguard against division by zero
       {/* Progress bar */}
       <div className="relative mb-3 h-2 w-full rounded-full bg-gray-200">
-        <div
-          className="absolute left-0 top-0 h-full rounded-full bg-blue-500 transition-all duration-300"
-          style={{ width: `${(signedCount / validators.length) * 100}%` }}
-        />
-        <div
-          className="absolute top-0 h-full w-0.5 bg-gray-600"
-          style={{ left: `${(threshold / validators.length) * 100}%` }}
-          title={`Threshold: ${threshold}`}
-        />
+        {validators.length > 0 && (
+          <>
+            <div
+              className="absolute left-0 top-0 h-full rounded-full bg-blue-500 transition-all duration-300"
+              style={{ width: `${(signedCount / validators.length) * 100}%` }}
+            />
+            <div
+              className="absolute top-0 h-full w-0.5 bg-gray-600"
+              style={{ left: `${(threshold / validators.length) * 100}%` }}
+              title={`Threshold: ${threshold}`}
+            />
+          </>
+        )}
       </div>
🧹 Nitpick comments (5)
src/pages/api/ism-details.ts (3)

151-168: Consider consolidating request validation into a single block.

Right now, originTxHash and messageId are validated at line 153, but originDomain is validated separately at line 166. Would be cleaner to destructure and validate all required fields together - like layers of an onion, all in one place.

🧅 Suggested consolidation
  try {
-   const { originTxHash, messageId } = req.body;
-
-   if (!originTxHash || !messageId) {
-     return res.status(400).json({ error: 'Missing required fields: originTxHash, messageId' });
-   }
+   const { originTxHash, messageId, originDomain } = req.body;
+
+   if (!originTxHash || !messageId || !originDomain) {
+     return res.status(400).json({ 
+       error: 'Missing required fields: originTxHash, messageId, originDomain' 
+     });
+   }

    const { multiProvider, core, metadataBuilder, fromCache } = await getRegistryAndCore();
    timer.mark('getRegistryAndCore');
    logger.info(
      `[TIMING] getRegistryAndCore took ${timer.getTimings().getRegistryAndCore}ms (fromCache: ${fromCache})`,
    );

-   // We need to find which chain this transaction is on
-   // For now, require the origin domain to be passed
-   const { originDomain } = req.body;
-   if (!originDomain) {
-     return res.status(400).json({ error: 'Missing required field: originDomain' });
-   }
-
    const originChain = multiProvider.tryGetChainName(originDomain);

225-244: Comma operator side-effects in promise construction - a bit swampy.

The comma operator usage here to set ismFromCache and call logger.info inline is clever but reads like an ogre wrote it in the dark. Consider extracting to a helper or using a more explicit pattern for clarity.

🧅 Cleaner approach
+   // Helper to handle cache lookup with logging
+   async function getIsmConfig(): Promise<DerivedIsmConfig> {
+     if (ismCacheValid) {
+       ismFromCache = true;
+       logger.info(`[TIMING] ISM config cache HIT for ${ismCacheKey}`);
+       return cachedIsmConfig!.config;
+     }
+     logger.info(`[TIMING] ISM config cache MISS for ${ismCacheKey}, will derive...`);
+     const ismConfigStart = Date.now();
+     const evmIsmReader = new EvmIsmReader(
+       multiProvider,
+       destinationChain,
+       undefined,
+       message,
+     );
+     const result = await evmIsmReader.deriveIsmConfig(recipientIsm);
+     logger.info(`[TIMING] deriveIsmConfig completed in ${Date.now() - ismConfigStart}ms`);
+     ismConfigCache.set(ismCacheKey, { config: result, timestamp: now });
+     return result;
+   }
+
-   const ismConfigPromise: Promise<DerivedIsmConfig> = ismCacheValid
-     ? ((ismFromCache = true),
-       logger.info(`[TIMING] ISM config cache HIT for ${ismCacheKey}`),
-       Promise.resolve(cachedIsmConfig!.config))
-     : (async () => {
-         // ... existing IIFE ...
-       })();
+   const ismConfigPromise = getIsmConfig();

71-141: Consider using the SDK's AddressesMap type instead of Record<string, any> for better type safety.

The caching logic is solid. The addressesMap on line 112 currently uses Record<string, any>, but the SDK exports an AddressesMap type ({ [key: string]: Address }) that would give you proper typing instead of any. Just import it from @hyperlane-xyz/sdk and swap it in—cleaner and catches any address-related type issues down the road.

src/features/messages/cards/TimelineCard.tsx (2)

17-17: Unused parameter _debugResult - intentional?

The debugResult prop is destructured as _debugResult with the underscore convention suggesting it's intentionally unused. If this is reserved for future use, consider adding a brief comment. Otherwise, it might be worth removing from the props until needed.


198-211: Emoji icons work, but consider accessibility.

The emoji icons are visually clear, but screen readers might not convey their meaning well. Consider adding aria-label attributes for better accessibility.

🧅 Add aria labels
 function StageIcon({ stage }: { stage: MessageStage }) {
   const iconMap: Partial<Record<MessageStage, string>> = {
     [MessageStage.Sent]: '✈',
     [MessageStage.Finalized]: '🔒',
     [MessageStage.Validated]: '🛡',
     [MessageStage.Relayed]: '✉',
   };

+  const labelMap: Partial<Record<MessageStage, string>> = {
+    [MessageStage.Sent]: 'Sent',
+    [MessageStage.Finalized]: 'Finalized',
+    [MessageStage.Validated]: 'Validated',
+    [MessageStage.Relayed]: 'Relayed',
+  };

   return (
-    <div className="flex h-9 w-9 items-center justify-center rounded-full bg-blue-500 text-white">
+    <div 
+      className="flex h-9 w-9 items-center justify-center rounded-full bg-blue-500 text-white"
+      aria-label={labelMap[stage]}
+      role="img"
+    >
       {iconMap[stage] || '•'}
     </div>
   );
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 011af5a and 167e6ed.

⛔ Files ignored due to path filters (1)
  • .yarn/install-state.gz is excluded by !**/.yarn/**, !**/*.gz
📒 Files selected for processing (6)
  • src/features/debugger/useIsmDetails.ts
  • src/features/messages/MessageDetails.tsx
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/features/messages/cards/TransactionCard.tsx
  • src/pages/api/ism-details.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/features/debugger/useIsmDetails.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Access state using Zustand hooks: useMultiProvider(), useChainMetadata(), useRegistry()
Use clsx() for conditional className assignment in components

Files:

  • src/features/messages/cards/TransactionCard.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/pages/api/ism-details.ts
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/MessageDetails.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,ts,jsx,tsx}: Use single quotes for strings in code
Use trailing commas in code
Enforce 100 character line width

Files:

  • src/features/messages/cards/TransactionCard.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/pages/api/ism-details.ts
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/MessageDetails.tsx
**/*.{js,ts,jsx,tsx,css}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Prettier with import organization and Tailwind class sorting

Files:

  • src/features/messages/cards/TransactionCard.tsx
  • src/features/messages/cards/TimelineCard.tsx
  • src/pages/api/ism-details.ts
  • src/features/messages/cards/IsmDetailsCard.tsx
  • src/features/messages/MessageDetails.tsx
🧠 Learnings (3)
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Use Zustand store (`src/store.ts`) to manage global state including chain metadata, MultiProtocolProvider, and warp routes

Applied to files:

  • src/features/messages/cards/TransactionCard.tsx
📚 Learning: 2025-12-17T22:05:40.656Z
Learnt from: CR
Repo: hyperlane-xyz/hyperlane-explorer PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-17T22:05:40.656Z
Learning: Applies to src/consts/api.ts : Update API endpoints in `src/consts/api.ts`

Applied to files:

  • src/pages/api/ism-details.ts
📚 Learning: 2025-12-17T11:59:10.567Z
Learnt from: paulbalaji
Repo: hyperlane-xyz/hyperlane-explorer PR: 242
File: src/utils/yamlParsing.ts:20-41
Timestamp: 2025-12-17T11:59:10.567Z
Learning: Chain names in this repository must be lowercase as defined by the schema. Do not apply any runtime case normalization when parsing or storing chain metadata; instead, validate and rely on lowercase inputs, and ensure downstream logic preserves lowercase to maintain consistency across parsing and storage.

Applied to files:

  • src/pages/api/ism-details.ts
🧬 Code graph analysis (4)
src/features/messages/cards/TimelineCard.tsx (4)
src/types.ts (1)
  • Message (50-57)
src/features/debugger/types.ts (1)
  • MessageDebugResult (34-47)
src/features/messages/cards/IsmDetailsCard.tsx (1)
  • extractValidatorInfo (306-343)
src/components/layout/Card.tsx (1)
  • Card (8-12)
src/pages/api/ism-details.ts (1)
src/utils/logger.ts (1)
  • logger (3-8)
src/features/messages/cards/IsmDetailsCard.tsx (1)
src/components/layout/Card.tsx (1)
  • Card (8-12)
src/features/messages/MessageDetails.tsx (3)
src/features/debugger/useIsmDetails.ts (1)
  • useIsmDetails (49-59)
src/features/messages/cards/IsmDetailsCard.tsx (2)
  • extractValidatorInfo (306-343)
  • IsmDetailsCard (22-53)
src/features/messages/cards/TimelineCard.tsx (1)
  • TimelineCard (17-39)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: lint
🔇 Additional comments (20)
src/pages/api/ism-details.ts (3)

1-24: LGTM on the imports and type workaround.

The module structure looks solid. The DerivedHookConfig type workaround with any is acknowledged with a clear TODO comment - that's fair enough until the SDK exports it properly.


50-69: Neat timing helper, no issues here.

The timer utility is clean and functional. Does what it says on the tin.


301-305: Serverless config looks proper.

The 60-second timeout for cold starts is sensible given the registry loading overhead. Good call.

src/features/messages/cards/TransactionCard.tsx (3)

1-1: Good addition of the ValidatorInfo type import.

Clean import from the SDK for the new validator functionality.


59-72: Props extension looks proper.

The validatorInfo prop is well-typed with the optional shape { validators: ValidatorInfo[]; threshold: number } | null. Nice and clear.


178-189: Smart conditional rendering for validator status.

The guard at line 179 (validatorInfo && validatorInfo.validators.length > 0) ensures we only show the summary when there's actual data to display. Falls back to the spinner otherwise - that's good UX.

src/features/messages/MessageDetails.tsx (5)

13-13: Hook import looks good.

Clean addition of the useIsmDetails hook for fetching ISM data from the new API.


18-18: Good import update for extractValidatorInfo.

Pulling in the utility function alongside the card component keeps related concerns together.


79-80: Solid conditional hook usage.

Only fetching ISM details when isMessageFound is true prevents unnecessary API calls. The hook handles null gracefully based on the implementation in useIsmDetails.ts.


138-147: Clean data threading to child components.

The validator info extraction and prop passing to DestinationTransactionCard and TimelineCard is well-structured. Each component gets what it needs without over-fetching.


159-159: Conditional render for IsmDetailsCard is proper.

Only rendering when ismDetails exists - no wasted render cycles. Nice and simple.

src/features/messages/cards/TimelineCard.tsx (4)

1-8: Imports look comprehensive and appropriate.

Good set of imports for the enhanced timeline functionality. The SDK types and utilities are well-chosen.


10-15: Props extension is proper.

Adding debugResult and ismResult allows the timeline to leverage the new ISM data for richer visualization.


57-79: Stage determination logic is well thought out.

The effective stage calculation correctly prioritizes:

  1. Delivered status → Relayed stage
  2. Real-time quorum data → Upgraded to Validated
  3. Fallback to widget's stage

This ensures the UI reflects the most accurate state available. Good work.


306-341: Helper functions are clean and readable.

getStageHeader and getStageOpacityClass encapsulate the logic nicely. The failing state handling in getStageHeader (lines 324-327) is particularly thoughtful.

src/features/messages/cards/IsmDetailsCard.tsx (5)

1-15: Imports are well-organized and comprehensive.

Good use of type-only imports for SDK types. The separation of concerns is clear.


17-23: Props change from ismDetails to result is cleaner.

The new prop name result with type MetadataBuildResult | null | undefined is more accurate for what's being passed. The early return pattern at line 23 is clean.


55-140: IsmTreeNode is a lovely piece of recursive rendering.

The depth-based indentation, expand/collapse logic, and conditional rendering for different ISM types (multisig, aggregation, routing) is well-structured. The default expansion for shallow nodes (depth < 2) provides a good initial view.


253-300: Helper functions are well-implemented.

getIsmTypeName and getTypeBadgeColor provide good coverage of ISM types with sensible defaults. The color coding by ISM category (purple for aggregation, orange for routing, blue for multisig) aids visual recognition.


302-343: extractValidatorInfo traversal is correct.

The recursive traversal through multisig, aggregation sub-modules, and routing selected ISMs correctly finds the first validator list. Returning early on the first match is the right behavior for the current use case.

One consideration: if an ISM tree has multiple multisig modules (e.g., in an aggregation), this only returns the first one found. That's fine if intentional, but worth noting.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/features/messages/cards/IsmDetailsCard.tsx Outdated
Comment thread src/features/messages/cards/TransactionCard.tsx
Copy link
Copy Markdown

@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: 2

🤖 Fix all issues with AI agents
In `@src/features/debugger/types.ts`:
- Around line 14-34: Remove the unused ValidatorStatus type and update
IsmDetails to reflect the actual shape returned by checkMultisigIsmEmpty (retain
only ismAddress: Address and moduleType: IsmModuleTypes, removing threshold,
validators, checkpointIndex, subModules, selectedModule) OR alternatively
populate those fields where details.ismDetails is created; also add an explicit
async return type annotation to checkMultisigIsmEmpty (e.g.
Promise<MessageDebugResult>) so the compiler catches mismatches when assigning
to details.ismDetails.

In `@src/features/messages/cards/IsmDetailsCard.tsx`:
- Around line 148-150: The quorum check in ValidatorList computes hasQuorum as
signedCount >= threshold which returns true when threshold === 0; update the
logic that defines hasQuorum (in the ValidatorList component where signedCount
is computed from validators) to require threshold > 0 as well (i.e., hasQuorum
should be signedCount >= threshold && threshold > 0) so it matches the behavior
used in ValidatorStatusSummary/EnhancedMessageTimeline and avoids showing a
quorum reached indicator when no signatures are required.
🧹 Nitpick comments (7)
src/features/debugger/types.ts (1)

56-63: IsmModuleTypes enum looks stale compared to the SDK's IsmType.

This local enum has only 5 members (UNUSED, ROUTING, AGGREGATION, LEGACY_MULTISIG, MULTISIG), while the SDK's IsmType used in IsmDetailsCard.tsx has many more (MERKLE_ROOT_MULTISIG, MESSAGE_ID_MULTISIG, STORAGE_*, OP_STACK, CCIP, etc.). The IsmDetails interface on line 25 references this local enum, but if anyone actually reads that field they'll get a much narrower type than the real ISM landscape. If IsmDetails stays, this enum should be replaced with IsmType from the SDK.

src/features/messages/cards/TransactionCard.tsx (1)

364-367: Consider clsx() for these conditional class compositions.

Lines 365-366 and 384-385 use template literal string interpolation for conditional classes. The project guidelines say to use clsx() for conditional classNames. Not a huge deal — the current approach works — but worth aligning with the rest of the swamp.

Example with clsx
+import clsx from 'clsx';
 // ...
         <span
-          className={`rounded-full px-2 py-0.5 text-xs font-medium ${
-            hasQuorum ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
-          }`}
+          className={clsx(
+            'rounded-full px-2 py-0.5 text-xs font-medium',
+            hasQuorum ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800',
+          )}
         >

As per coding guidelines: "Use clsx() for conditional classNames instead of manual concatenation."

src/features/messages/cards/TimelineCard.tsx (3)

17-17: debugResult is destructured but never used — might as well leave it at the door.

_debugResult is accepted via props and immediately underscore-prefixed but never referenced in the component body. If it's there for future use, a comment would help. Otherwise it's just clutter.

If not needed yet
-export function TimelineCard({ message, blur, debugResult: _debugResult, ismResult }: Props) {
+export function TimelineCard({ message, blur, ismResult }: Props) {

And if you still want it in the Props interface for future use, that's fine — just don't destructure it.


229-308: Validator display logic is duplicated between here and IsmDetailsCard.tsx.

ValidatorDropdown here (lines 229-308) and ValidatorList/ValidatorRow in IsmDetailsCard.tsx (lines 142-223) both render essentially the same thing: a progress bar with threshold marker, signed count badge, and per-validator status rows with icons and colors. The styling differs slightly but the data flow and logic are identical.

Worth extracting a shared ValidatorStatusDisplay component that both can use — saves you from fixin' the same bug twice in the future (like the division-by-zero guard that had to be applied in both places).

Also applies to: 142-191


43-47: Import StageTimings from @hyperlane-xyz/widgets instead of redeclaring it locally.

This type already exists in the widgets package and matches your local definition exactly. Right now you've got it defined twice, which means if the widgets library updates its timing shape down the road, you won't catch it unless you remember to update this spot too. Since you're already importing MessageStage from widgets, might as well grab StageTimings from there as well.

import type { StageTimings, MessageStage } from '@hyperlane-xyz/widgets';
src/features/messages/cards/IsmDetailsCard.tsx (2)

55-70: Repeated type assertions — consider narrowing once at the top.

Throughout IsmTreeNode, you've got (result as MultisigMetadataBuildResult) cast six times (lines 80, 81, 88, 90, 91, 92, 104, 107, 108). Once the type guard confirms the type, you could assign to a narrowed local variable to reduce the noise.

Example cleanup
   const isMultisig = isMultisigResult(result);
+  const multisigResult = isMultisig ? (result as MultisigMetadataBuildResult) : null;
   const isAggregation = isAggregationResult(result);
+  const aggResult = isAggregation ? (result as AggregationMetadataBuildResult) : null;
   const isRouting = isRoutingResult(result);
+  const routingResult = isRouting ? (result as RoutingMetadataBuildResult) : null;

Then use multisigResult?.validators etc. throughout, instead of casting every time.


304-345: extractValidatorInfo only returns the first multisig found in the tree.

This depth-first traversal returns the first multisig it encounters. For aggregation ISMs that combine multiple multisigs (e.g., "3-of-5 multisig AND 2-of-3 multisig"), only the first one's validators and threshold will surface in the timeline and transaction cards.

If that's the intended behavior, a brief comment like // Returns first multisig found (depth-first) would make the intent crystal clear for whoever wanders into this code next.

Comment thread src/features/debugger/types.ts Outdated
Comment thread src/features/messages/cards/IsmDetailsCard.tsx Outdated
paulbalaji added a commit that referenced this pull request Feb 10, 2026
Integrate validator signature status feature (PR #253) with
ui-rebrand-more changes. Keeps SectionCard wrapper, updated dep
versions, transpilePackages config, and Color.primaryDark styling
from our branch while adding ISM tree view, validator progress
bars, and ValidatorStatusSummary from the incoming branch.

pnpm-lock.yaml accepted from ours -- needs pnpm install to regenerate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread .yarn/install-state.gz Outdated
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (3)
src/features/messages/MessageDetails.tsx (2)

139-148: extractValidatorInfo called twice with the same data — consider computing once

Here at Line 139 you call extractValidatorInfo(ismDetails) for DestinationTransactionCard, and then TimelineCard internally calls extractValidatorInfo(ismResult) again with the same object (see TimelineCard.tsx Line 22). That's a recursive tree traversal happening twice every render for no good reason — like trudging through the same swamp twice when there's a perfectly good bridge.

You could compute it once with useMemo and pass the derived validatorInfo to both consumers:

♻️ Suggested refactor
+ const validatorInfo = useMemo(
+   () => (ismDetails ? extractValidatorInfo(ismDetails) : null),
+   [ismDetails],
+ );
+
  <DestinationTransactionCard
    ...
-   validatorInfo={ismDetails ? extractValidatorInfo(ismDetails) : null}
+   validatorInfo={validatorInfo}
  />
  {showTimeline && (
    <TimelineCard
      message={message}
      blur={blur}
      debugResult={debugResult}
-     ismResult={ismDetails}
+     ismResult={ismDetails}
+     validatorInfo={validatorInfo}
    />
  )}

This would also require updating TimelineCard to accept an optional validatorInfo prop and skip its own extractValidatorInfo call when provided.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/messages/MessageDetails.tsx` around lines 139 - 148, Compute
validatorInfo once with useMemo from ismDetails/ismResult (e.g., const
validatorInfo = useMemo(() => extractValidatorInfo(ismDetails || ismResult),
[ismDetails, ismResult])) and pass that validatorInfo into
DestinationTransactionCard and TimelineCard instead of calling
extractValidatorInfo twice; update TimelineCard's props to accept an optional
validatorInfo parameter and change its internal logic to skip calling
extractValidatorInfo when validatorInfo is provided (keep fallback to
extractValidatorInfo if the prop is undefined).

80-82: No error feedback from ISM details fetch

Right now only data is destructured from the query. If the fetch fails, the user gets no indication — the ISM card and validator info just silently don't appear. That's fine for now since the cards handle nulls gracefully, but you might want to surface a toast or log on error eventually, especially since this hits a backend API that could time out.

Something like:

const { data: ismDetails, isError: isIsmError } = useIsmDetails(isMessageFound ? message : null);

…and then handle isIsmError as you see fit. Not a swamp-level emergency, just worth keepin' in mind.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/messages/MessageDetails.tsx` around lines 80 - 82, The fetch of
ISM details currently only destructures data from the useIsmDetails hook (const
{ data: ismDetails } = useIsmDetails(...)) so failures are silent; update the
call to also destructure the error state (e.g., isError or error) from
useIsmDetails (keep the same arguments: useIsmDetails(isMessageFound ? message :
null)), then add a minimal error handling path in MessageDetails (for example:
show a toast, set a local error state, or log via console.error/processLogger
when isIsmError or error is present) so users/developers get feedback when the
backend fetch fails; reference the useIsmDetails hook and the ismDetails
variable and implement handling where the ISM card and validator info are
rendered.
package.json (1)

94-95: SDK override downgrade is intentional, but clarify the discrepancy.

Lines 94–95 override @hyperlane-xyz/sdk from its declared 25.1.0 down to 21.1.0-preview.9c72f6fb…. The pnpm-lock confirms the preview version is what actually resolves. There's no ambiguity there—overrides win.

The good news: this explorer repo doesn't directly import from @hyperlane-xyz/sdk, so you won't hit API drift from the version gap. The override affects transitive dependencies, not the explorer code itself.

The thing worth tidying up: the version mismatch between declared (top-level) and overridden (bottom-level) makes the intent murky for the next developer. Since this is blocked on the monorepo PR #7807, add a comment above the override explaining it's temporary. Something like // Temporary: waiting for stable @hyperlane-xyz/sdk release gives future maintainers the context they need to know why 25.1.0 is declared but 21.1.0-preview is actually installed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 94 - 95, The override for "@hyperlane-xyz/sdk"
pins a preview v21.1.0 while the top-level declaration lists 25.1.0, which is
confusing; update package.json by adding a concise comment immediately above the
"@hyperlane-xyz/sdk" override (the override entry shown in package.json)
explaining this is a temporary downgrade pending the monorepo PR `#7807` (e.g.,
"Temporary override: using 21.1.0-preview until monorepo PR `#7807` is merged"),
so future maintainers understand the mismatch between the declared dependency
and the resolved override.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@package.json`:
- Around line 94-95: The override for "@hyperlane-xyz/sdk" pins a preview
v21.1.0 while the top-level declaration lists 25.1.0, which is confusing; update
package.json by adding a concise comment immediately above the
"@hyperlane-xyz/sdk" override (the override entry shown in package.json)
explaining this is a temporary downgrade pending the monorepo PR `#7807` (e.g.,
"Temporary override: using 21.1.0-preview until monorepo PR `#7807` is merged"),
so future maintainers understand the mismatch between the declared dependency
and the resolved override.

In `@src/features/messages/MessageDetails.tsx`:
- Around line 139-148: Compute validatorInfo once with useMemo from
ismDetails/ismResult (e.g., const validatorInfo = useMemo(() =>
extractValidatorInfo(ismDetails || ismResult), [ismDetails, ismResult])) and
pass that validatorInfo into DestinationTransactionCard and TimelineCard instead
of calling extractValidatorInfo twice; update TimelineCard's props to accept an
optional validatorInfo parameter and change its internal logic to skip calling
extractValidatorInfo when validatorInfo is provided (keep fallback to
extractValidatorInfo if the prop is undefined).
- Around line 80-82: The fetch of ISM details currently only destructures data
from the useIsmDetails hook (const { data: ismDetails } = useIsmDetails(...)) so
failures are silent; update the call to also destructure the error state (e.g.,
isError or error) from useIsmDetails (keep the same arguments:
useIsmDetails(isMessageFound ? message : null)), then add a minimal error
handling path in MessageDetails (for example: show a toast, set a local error
state, or log via console.error/processLogger when isIsmError or error is
present) so users/developers get feedback when the backend fetch fails;
reference the useIsmDetails hook and the ismDetails variable and implement
handling where the ISM card and validator info are rendered.

Comment thread src/pages/api/ism-details.ts Outdated
@paulbalaji
Copy link
Copy Markdown
Collaborator

@codex review
@cursor review

Comment thread src/features/messages/cards/IsmDetailsCard.tsx Outdated
Comment thread src/features/messages/cards/IsmDetailsCard.tsx Outdated
Comment thread src/features/messages/cards/IsmDetailsCard.tsx
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5028b17827

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/pages/api/ism-details.ts Outdated
Comment thread src/features/messages/cards/TimelineCard.tsx Outdated
Comment thread src/features/chains/useChainMetadata.ts Outdated
Comment thread src/features/messages/cards/TimelineCard.tsx Outdated
Comment thread src/features/messages/cards/TimelineCard.tsx Outdated
Comment thread src/features/messages/cards/TimelineCard.tsx Outdated
Comment thread package.json Outdated
Comment thread src/features/debugger/types.ts Outdated
@paulbalaji
Copy link
Copy Markdown
Collaborator

Review Summary

Consolidated review from automated tools and manual inspection. Most prior CodeRabbit/Xaroz feedback has been addressed — this covers remaining issues.

High

# Issue File
1 Hook cache key missing destination — routing/fallback hooks resolve differently per destination, so ${originChain}:${address} can serve a poisoned cache entry for a different destination ism-details.ts:48
2 extractValidatorInfo returns only first multisig branch — for aggregation/routing ISMs with multiple multisig children, one branch reaching quorum ≠ full ISM buildable. UI can show "Validated" / "quorum reached" too early ismHelpers.ts:93

Medium

# Issue File
3 z.any() removes runtime validation of user-controlled chains query param. Malformed links ([null], ["x"]) now pass safeParse and throw at c.name dereference useChainMetadata.ts:14
4 No EVM gate on useIsmDetails — called for any found message including PI/non-EVM. The API assumes EVM receipt shape (getTransactionReceipt, getDispatchedMessages), so non-EVM detail pages issue a failing API request + server error on every load MessageDetailsInner.tsx:105
5 Missing SDK exportsMetadataBuildResult, BaseMetadataBuilder, ValidatorInfo, etc. don't exist in SDK 31.1.0. CI typecheck/build fails multiple files

Low

# Issue File
6 hasQuorum inconsistencyValidatorList uses signedCount >= threshold without threshold > 0 guard (other components have it) IsmDetailsCard.tsx:155
7 Sequential getChainAddresses — awaits each chain serially in a loop, impacts cold-start time ism-details.ts:116-125
8 originDomain validated late — after originTxHash/messageId already processed ism-details.ts:167-179
9 Format failures on TimelineCard.tsx and ism-details.ts CI

Working on fixes now.

paulbalaji and others added 5 commits April 16, 2026 23:17
- Added new API endpoint /api/ism-details for server-side ISM config
  derivation with 30-minute TTL caching for ISM and hook configs
- Enhanced IsmDetailsCard with ISM tree visualization showing module
  hierarchy and validator details
- Enhanced TimelineCard with validator signature status visualization
- Added validator signature status to DestinationTransactionCard pending state
- Extracted ISM helper functions into ismHelpers.ts (per review feedback)
- Used z.any() in useChainMetadata to avoid TS infinite recursion

Review feedback addressed:
- Don't expose raw error.message in API responses (security)
- Added comment about Vercel serverless caching being best-effort
- Extracted isExpandable variable in IsmTreeNode
- Extracted helper functions into ismHelpers.ts
- No re-exports; direct imports from ismHelpers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract deriveMessageStage utility function (Xaroz: no let mutation in render)
- Replace emoji icons with AirplaneIcon/LockIcon/ShieldIcon/EnvelopeIcon from widgets
- Replace CSS chevrons with WideChevronIcon from widgets package

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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