feat: add real-time validator signature status display#253
feat: add real-time validator signature status display#253nambrot wants to merge 5 commits intopbio/perf-beta-package-surfacesfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ 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. Comment |
5378d7a to
b13b8f6
Compare
There was a problem hiding this comment.
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 thatresult.datacontains validChainMetadataobjects, but since the schema is nowz.any(), there's no guarantee that's true. If an element has a missing or malformednameproperty, 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
lateston 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 refetchesAye, using the whole
messageobject 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 timeoutThe 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 componentWhile the caller currently checks
validators.length > 0before rendering, theValidatorStatusSummarycomponent itself doesn't guard against empty arrays. If someone calls it directly without that check, lines 377 and 385 will divide by zero, producingNaN%orInfinity%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 usingclsx()for conditional classNamePer 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.allto 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
originTxHashandmessageIdfirst, thenoriginDomainseparately. 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.messagedirectly. 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
datahere. 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...ofloop processes sub-modules one at a time. For aggregation ISMs with multiple sub-modules, you could usePromise.allfor 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
📒 Files selected for processing (16)
next.config.jspackage.jsonsrc/features/chains/useChainMetadata.tssrc/features/chains/utils.tssrc/features/debugger/debugMessage.tssrc/features/debugger/types.tssrc/features/debugger/useIsmDetails.tssrc/features/debugger/validatorStatus.tssrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/deliveryStatus/types.tssrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/features/messages/MessageDetails.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/TransactionCard.tsxsrc/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()
Useclsx()for conditional className assignment in components
Files:
src/features/deliveryStatus/types.tssrc/features/debugger/types.tssrc/features/chains/useChainMetadata.tssrc/pages/api/ism-details.tssrc/features/messages/cards/TimelineCard.tsxsrc/features/debugger/useIsmDetails.tssrc/features/debugger/validatorStatus.tssrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/chains/utils.tssrc/features/debugger/debugMessage.tssrc/features/messages/MessageDetails.tsxsrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/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.tssrc/features/debugger/types.tssrc/features/chains/useChainMetadata.tssrc/pages/api/ism-details.tssrc/features/messages/cards/TimelineCard.tsxsrc/features/debugger/useIsmDetails.tssrc/features/debugger/validatorStatus.tssrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/chains/utils.tssrc/features/debugger/debugMessage.tssrc/features/messages/MessageDetails.tsxnext.config.jssrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/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.tssrc/features/debugger/types.tssrc/features/chains/useChainMetadata.tssrc/pages/api/ism-details.tssrc/features/messages/cards/TimelineCard.tsxsrc/features/debugger/useIsmDetails.tssrc/features/debugger/validatorStatus.tssrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/chains/utils.tssrc/features/debugger/debugMessage.tssrc/features/messages/MessageDetails.tsxnext.config.jssrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/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.tssrc/features/debugger/types.tssrc/features/chains/useChainMetadata.tssrc/pages/api/ism-details.tssrc/features/debugger/useIsmDetails.tssrc/features/debugger/validatorStatus.tssrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/chains/utils.tssrc/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.tssrc/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.tssrc/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
falseis 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
isServerblock.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 existinggetMailboxAddressapproach, 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
debugResultoptional 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 requireddebugResulton 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
ChainMetadataSchemais correct — it does happen, but downstream instore.ts(lines 156-158), not in this function. ThesafeParseon line 33 only confirms the data is an array; individual chain metadata objects don't get validated until they're processed in the store'sobjFiltercallback.This creates a brief window where invalid metadata can exist in
chainMetadataOverridesstate. Consider clarifying the comment to specify that validation occurs when the overrides are consumed (instore.ts), so future maintainers understand the validation timeline.src/features/deliveryStatus/useMessageDeliveryStatus.tsx (1)
55-56: LGTM - debugResult propagation for delivered messagesGood 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 approachWell 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 importThe
DomainIdtype is declared as a global ambient type insrc/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 completenessThat
eslint-disable-next-linecomment 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 needmessageitself in the deps, or document whymsgIdandorigin.hashare 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 additionGood import of
ValidatorInfotype from the SDK.
178-189: Good defensive check before rendering validator summaryNice work checking
validatorInfo.validators.length > 0before 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 typesNice work on these new interfaces. The
IsmDetailsstructure withsubModulesandselectedModulehandles 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 declaredAll three types (
Address,AddressTo, andHexString) are ambient type declarations insrc/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: MethodgetEthersV5Provideris correct and properly used.This method exists on
MultiProtocolProviderfrom 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,ismConfigorhookConfigcould be undefined when used.If one of the
deriveTasksrejects,Promise.allwill 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 wantPromise.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, butextractValidatorInfois 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
debugResultis 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-ignoreis 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 -
hasQuorumis calculated both here and in the parentEnhancedMessageTimeline. 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/coreand 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
getIsmDetailsfor the heavy lifting while keeping its core responsibility of checking for misconfiguration. The optionalvalidatorContextflows 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.validatorscheck). 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.
There was a problem hiding this comment.
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
ismConfigCacheandhookConfigCacheMaps 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
originDomainis validated separately fromoriginTxHashandmessageId, 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'
responseasanymeans 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 useclsx()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 parameterdebugResult.You've got
debugResultcomin' 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 usingclsx()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' anaria-labelon 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/sdkcould 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: Useclsx()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: Useclsx()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
getTypeBadgeColorfunction duplicates the same type checks thatisMultisigResult,isAggregationResult, andisRoutingResultalready 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 knowsresultisMultisigMetadataBuildResult. 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
📒 Files selected for processing (13)
next.config.jspackage.jsonsrc/features/chains/useChainMetadata.tssrc/features/debugger/types.tssrc/features/debugger/useIsmDetails.tssrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/deliveryStatus/types.tssrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/features/messages/MessageDetails.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/TransactionCard.tsxsrc/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()
Useclsx()for conditional className assignment in components
Files:
src/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/deliveryStatus/types.tssrc/features/messages/cards/TransactionCard.tsxsrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/features/debugger/types.tssrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/debugger/useIsmDetails.tssrc/features/messages/MessageDetails.tsxsrc/pages/api/ism-details.tssrc/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.tssrc/features/deliveryStatus/types.tssrc/features/messages/cards/TransactionCard.tsxsrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/features/debugger/types.tssrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/debugger/useIsmDetails.tssrc/features/messages/MessageDetails.tsxsrc/pages/api/ism-details.tssrc/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.tssrc/features/deliveryStatus/types.tssrc/features/messages/cards/TransactionCard.tsxsrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/features/debugger/types.tssrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/debugger/useIsmDetails.tssrc/features/messages/MessageDetails.tsxsrc/pages/api/ism-details.tssrc/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.tssrc/features/deliveryStatus/types.tssrc/features/debugger/types.tssrc/features/debugger/useIsmDetails.tssrc/pages/api/ism-details.tssrc/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.tsxsrc/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.tsxsrc/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'
debugResultoptional 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 returnsundefinedmeans a hiccup there won't mess up showin' the delivery status. Solid defensive approach.
52-72: Result structure looks good.The
debugResultslots in cleanly with the delivery transaction info. Matches up proper with the type definition changes intypes.ts.src/features/debugger/types.ts (1)
12-18: Lookin' good, these types are solid.The
ValidatorStatusinterface 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
debugResultfor 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
staleTimeof 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
isMessageFoundis 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.lengthis 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.
2687fb1 to
d28495b
Compare
There was a problem hiding this comment.
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.lengthhappens to be 0 (an empty swamp, if you will), those percentage calculations will give youInfinity%. Nobody wants infinity in their width styles.
🧹 Nitpick comments (10)
src/pages/api/ism-details.ts (4)
19-21: Thisanytype is like mud - it'll stick around and cause problems later.The SDK doesn't export
DerivedHookConfigcurrently, but usinganymeans 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.
originDomainis checked separately fromoriginTxHashandmessageId. 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/hookFromCacheand 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: Theanytype on response loses the shape safety.Typing
responseasanymeans 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_debugResultparameter 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, theValidatorDropdowncomponent itself doesn't verify this. If someone later calls it directly with an empty array, ye'd getNaN%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: Usingvalidator.address || indexas a key could cause React reconciliation issues.If
validator.addressis an empty string (falsy), the key falls back toindex, 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
datafrom the hook, butuseIsmDetailsalso returnsisLoading,isError, anderror. 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
isIsmLoadingto components or show a subtle loading state for the ISM card.
138-140: Consider memoizingextractValidatorInforesult 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (13)
next.config.jspackage.jsonsrc/features/chains/useChainMetadata.tssrc/features/debugger/types.tssrc/features/debugger/useIsmDetails.tssrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/deliveryStatus/types.tssrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/features/messages/MessageDetails.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/TransactionCard.tsxsrc/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()
Useclsx()for conditional className assignment in components
Files:
src/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/pages/api/ism-details.tssrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/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.tsxsrc/pages/api/ism-details.tssrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/messages/MessageDetails.tsxnext.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.tsxsrc/pages/api/ism-details.tssrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/messages/MessageDetails.tsxnext.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
falseis the right way to stub out modules that don't play nice with Next.js bundling. Applying these outside theisServerblock means both client and server get consistent behavior, which is what ye want.The
asset/resourcerule for.wasmfiles 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/sdkensures 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
titleattribute gets the job done without overcomplicating things.
238-259: Nice type-to-name mapping with a sensible fallback.The
getIsmTypeNamefunction 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
IsmDetailsCardalready 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.
d28495b to
9ee4d36
Compare
9ee4d36 to
cecc7a3
Compare
There was a problem hiding this comment.
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.lengthis 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
ValidatorStatusSummarycomponent itself doesn't protect againstvalidators.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: Theanytype here is a bit like dumping everything in the swamp and hoping for the best.Using
anyforDerivedHookConfigbypasses type checking. If the SDK doesn't export this type, consider creating a local interface that matches the expected shape, or at least useunknownfor 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 -
originTxHashandmessageIdare checked first, thenoriginDomainis 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 theanytype could be tightened.Using
anyforresponseloses 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_debugResultis sittin' there like a donkey with nothin' to do.The
debugResultprop 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 destructuringdata- consider whetherisLoadingorisErrorstates 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (13)
next.config.jspackage.jsonsrc/features/chains/useChainMetadata.tssrc/features/debugger/types.tssrc/features/debugger/useIsmDetails.tssrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/deliveryStatus/types.tssrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/features/messages/MessageDetails.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/TransactionCard.tsxsrc/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()
Useclsx()for conditional className assignment in components
Files:
src/features/debugger/useIsmDetails.tssrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/messages/MessageDetails.tsxsrc/pages/api/ism-details.tssrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/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.tssrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/messages/MessageDetails.tsxsrc/pages/api/ism-details.tssrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/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.tssrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/messages/MessageDetails.tsxsrc/pages/api/ism-details.tssrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/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.tssrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/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.allapproach here is solid - fetching transaction details and debug info concurrently instead of sequentially. The.catch()wrapper ondebugMessageensures a failed debug lookup doesn't swamp the whole delivery status response.
88-96:DomainIdtype is already available globally, no import needed.
DomainIdis declared globally insrc/global.d.tsasdeclare 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
debugResultas 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
ValidatorInfofrom 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
enabledcondition to prevent unnecessary fetches- Reasonable
staleTimeandretrysettingsThe 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 theextractValidatorInfoutility 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
debugResultandismResultextend 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.Validatedat line 76 relies on MessageStage being a numeric enum, and this assumption is validated across the codebase. The getStageHeader function consistently uses numeric comparisons likecurrentStage >= 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
resultis proper defensive coding. The component cleanly delegates toIsmTreeNodefor 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
useIsmDetailshook andextractValidatorInfoare 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
resultprop correctly maps to the updated component interface.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
cecc7a3 to
011af5a
Compare
There was a problem hiding this comment.
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:IsmDetailsappears to be redundant with SDK'sMetadataBuildResult.Listen, this type sits here like a decoration in my swamp. Looking at how the data actually flows through your components—
MessageDetailsfetches viauseIsmDetailsand passesMetadataBuildResultdirectly toIsmDetailsCard,TimelineCard, andextractValidatorInfo. TheismDetailsfield onMessageDebugResultdoesn't appear to be populated or consumed anywhere meaningful.Consider either removing this interface and the
ismDetailsproperty fromMessageDebugResult, 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 > 0before rendering this, but if this component ever gets reused elsewhere without that guard, you'll end up withInfinity%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.lengthis zero, those percentage calculations will produceInfinity. The threshold marker at line 147 and signed progress at line 155 both divide byvalidators.lengthwithout 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 thisanytype for when the SDK exports the proper type.Using
anyhere is like having an onion with a few layers missing - it works but loses some of the good stuff. When the SDK stabilizes and exportsDerivedHookConfig, 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.allcould 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
originTxHashandmessageIdfirst (line 150), thenoriginDomainseparately (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: LocalIsmModuleTypesvs SDK'sIsmType.You've got this local enum here, but
IsmDetailsCard.tsximports and usesIsmTypefrom@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
ValidatorStatusSummarydirectly without that check, you'll getInfinity%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: UnuseddebugResultparameter.You're accepting
debugResultbut 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 MultisigMetadataBuildResultmultiple times within the same conditional block. Since you already checkedisMultisig, 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (13)
next.config.jspackage.jsonsrc/features/chains/useChainMetadata.tssrc/features/debugger/types.tssrc/features/debugger/useIsmDetails.tssrc/features/deliveryStatus/fetchDeliveryStatus.tssrc/features/deliveryStatus/types.tssrc/features/deliveryStatus/useMessageDeliveryStatus.tsxsrc/features/messages/MessageDetails.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/TransactionCard.tsxsrc/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()
Useclsx()for conditional className assignment in components
Files:
src/pages/api/ism-details.tssrc/features/deliveryStatus/types.tssrc/features/messages/MessageDetails.tsxsrc/features/messages/cards/TransactionCard.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/debugger/types.tssrc/features/messages/cards/TimelineCard.tsxsrc/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.tssrc/features/deliveryStatus/types.tssrc/features/messages/MessageDetails.tsxsrc/features/messages/cards/TransactionCard.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/debugger/types.tsnext.config.jssrc/features/messages/cards/TimelineCard.tsxsrc/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.tssrc/features/deliveryStatus/types.tssrc/features/messages/MessageDetails.tsxsrc/features/messages/cards/TransactionCard.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/debugger/types.tsnext.config.jssrc/features/messages/cards/TimelineCard.tsxsrc/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.tssrc/features/deliveryStatus/types.tssrc/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
falseis the right move for excluding problematic WASM modules from both client and server bundles. Theasset/resourcerule for.wasmfiles 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
falseback 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
hasBackendDestinationcheck ensures we don't overwrite complete backend data with potentially incomplete frontend data.One thing to keep in mind: if
message.destinationexists buthashis an empty string,hasBackendDestinationwill 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
ValidatorInfotype from the SDK sitting right where it belongs.
178-189: Nice defensive pattern here.The guard checking both
validatorInfoexistence andvalidators.length > 0before renderingValidatorStatusSummarykeeps 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
useIsmDetailshook andextractValidatorInfoutility 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
IsmDetailsCardwhen 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
typeimports 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.
getStageHeaderandgetStageOpacityClassdo 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
typeimports keeps the bundle lean.
22-53: Clean component structure.Early return guard, proper external link attributes, and delegation to
IsmTreeNodefor 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:extractValidatorInfois 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.
getIsmTypeNameandgetTypeBadgeColorprovide 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.
There was a problem hiding this comment.
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.messagedirectly 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.lengthwithout a guard. WhileValidatorListis only called when validators exist (line 104 checksvalidators), 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.lengthdirectly at lines 257 and 261. While the parent component guards withvalidators.length > 0at 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,
originTxHashandmessageIdare validated at line 153, butoriginDomainis 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
ismFromCacheand calllogger.infoinline 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'sAddressesMaptype instead ofRecord<string, any>for better type safety.The caching logic is solid. The
addressesMapon line 112 currently usesRecord<string, any>, but the SDK exports anAddressesMaptype ({ [key: string]: Address }) that would give you proper typing instead ofany. Just import it from@hyperlane-xyz/sdkand 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
debugResultprop is destructured as_debugResultwith 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-labelattributes 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
⛔ Files ignored due to path filters (1)
.yarn/install-state.gzis excluded by!**/.yarn/**,!**/*.gz
📒 Files selected for processing (6)
src/features/debugger/useIsmDetails.tssrc/features/messages/MessageDetails.tsxsrc/features/messages/cards/IsmDetailsCard.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/features/messages/cards/TransactionCard.tsxsrc/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()
Useclsx()for conditional className assignment in components
Files:
src/features/messages/cards/TransactionCard.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/pages/api/ism-details.tssrc/features/messages/cards/IsmDetailsCard.tsxsrc/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.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/pages/api/ism-details.tssrc/features/messages/cards/IsmDetailsCard.tsxsrc/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.tsxsrc/features/messages/cards/TimelineCard.tsxsrc/pages/api/ism-details.tssrc/features/messages/cards/IsmDetailsCard.tsxsrc/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
DerivedHookConfigtype workaround withanyis 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
validatorInfoprop 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
useIsmDetailshook 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
isMessageFoundis true prevents unnecessary API calls. The hook handles null gracefully based on the implementation inuseIsmDetails.ts.
138-147: Clean data threading to child components.The validator info extraction and prop passing to
DestinationTransactionCardandTimelineCardis well-structured. Each component gets what it needs without over-fetching.
159-159: Conditional render for IsmDetailsCard is proper.Only rendering when
ismDetailsexists - 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
debugResultandismResultallows 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:
- Delivered status → Relayed stage
- Real-time quorum data → Upgraded to Validated
- 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.
getStageHeaderandgetStageOpacityClassencapsulate the logic nicely. The failing state handling ingetStageHeader(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
resultwith typeMetadataBuildResult | null | undefinedis 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.
getIsmTypeNameandgetTypeBadgeColorprovide 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.
There was a problem hiding this comment.
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:IsmModuleTypesenum looks stale compared to the SDK'sIsmType.This local enum has only 5 members (
UNUSED,ROUTING,AGGREGATION,LEGACY_MULTISIG,MULTISIG), while the SDK'sIsmTypeused inIsmDetailsCard.tsxhas many more (MERKLE_ROOT_MULTISIG,MESSAGE_ID_MULTISIG,STORAGE_*,OP_STACK,CCIP, etc.). TheIsmDetailsinterface 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. IfIsmDetailsstays, this enum should be replaced withIsmTypefrom the SDK.src/features/messages/cards/TransactionCard.tsx (1)
364-367: Considerclsx()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:debugResultis destructured but never used — might as well leave it at the door.
_debugResultis 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 andIsmDetailsCard.tsx.
ValidatorDropdownhere (lines 229-308) andValidatorList/ValidatorRowinIsmDetailsCard.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
ValidatorStatusDisplaycomponent 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: ImportStageTimingsfrom@hyperlane-xyz/widgetsinstead 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
MessageStagefrom widgets, might as well grabStageTimingsfrom 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?.validatorsetc. throughout, instead of casting every time.
304-345:extractValidatorInfoonly 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.
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>
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/features/messages/MessageDetails.tsx (2)
139-148:extractValidatorInfocalled twice with the same data — consider computing onceHere at Line 139 you call
extractValidatorInfo(ismDetails)forDestinationTransactionCard, and thenTimelineCardinternally callsextractValidatorInfo(ismResult)again with the same object (seeTimelineCard.tsxLine 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
useMemoand pass the derivedvalidatorInfoto 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
TimelineCardto accept an optionalvalidatorInfoprop and skip its ownextractValidatorInfocall 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 fetchRight now only
datais 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
isIsmErroras 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/sdkfrom its declared25.1.0down to21.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/sdkreleasegives 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.
There was a problem hiding this comment.
💡 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".
5028b17 to
f3a9d01
Compare
Review SummaryConsolidated review from automated tools and manual inspection. Most prior CodeRabbit/Xaroz feedback has been addressed — this covers remaining issues. High
Medium
Low
Working on fixes now. |
ab71638 to
97c7dca
Compare
2413241 to
65e4452
Compare
- 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>
ec3716d to
2d863c0
Compare
Summary
/api/ism-detailsfor server-side ISM + hook config derivation with best-effort cachingWhy
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/relayerdependency from this PR and mirrored the small read-only metadata slice locally instead.#253only 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
#301(perf: keep explorer metadata-first and evm-only at runtime)#301Validation
pnpm run formatpnpm run typecheckpnpm run build