feat(explorer): phase 2 enhancements#1527
Conversation
Edge middleware inlines process.env at build time, so BACKEND_URL was hardcoded to the Docker build default. Replace with a catch-all API route handler (/api/[...path]) that runs in Node.js runtime and reads BACKEND_URL from process.env on each request. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ting, and consensus journey Add 5 feature enhancements to the bundled studio explorer: - Wei-to-GEN value formatting across all pages (transactions, contracts, validators) - Global search modal (Cmd+K) with debounced parallel search for transactions and contracts - Shiki-powered syntax highlighting for contract source code with theme support - Transaction data decode/encode toggle for base64-encoded fields - Consensus journey step indicator with timestamp tooltips and failure states Additional improvements: - Rename /state route to /contracts for clarity - Convert contracts page from card grid to sortable table layout - Add datetime range filter with calendar picker on transactions page - Filter contracts list to only show deployed contracts (not EOA accounts) - Fix contract count in stats bar to count only deployed contracts Co-Authored-By: Claude Opus 4.6 <[email protected]>
The route handler fix from PR #1523 is not needed since PR #1522's middleware approach already resolved the issue. Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Count deployed contracts from CurrentState + deploy tx join (consistent with /contracts) - Fix "Back to states" → "Back to contracts" label - Add biome-ignore for trusted Shiki dangerouslySetInnerHTML - Make CodeBlock fallback theme-aware (bg-muted instead of hard-coded dark) - Use last consensus round monitoring instead of first for appealed txs - Make journey step circles focusable buttons for keyboard accessibility - Add AbortController to GlobalSearch to prevent stale search results - Include timezone offset in DateTimePicker format for tz-aware DB columns Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Remove unused `parse` import from DateTimePicker - Add aria-labels to hour/minute inputs for accessibility - Show platform-appropriate keyboard shortcut (⌘K on Mac, Ctrl+K otherwise) Co-Authored-By: Claude Opus 4.6 <[email protected]>
Changes delete button label to "Disconnect Wallet" for external accounts and enables the delete/disconnect action for all account types. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…unt views Adds /address/[addr] page that resolves address type and shows the appropriate detail view. Replaces the old contract detail page with a redirect to /address. Updates all address links to use /address/ routes. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Adds average TPS (24h) stat card, a sparkline chart component, and a 14-day transaction volume visualization to the dashboard. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ltering Adds All/In Progress/Completed/Failed tabs for transaction filtering, replacing the single-status dropdown. Supports comma-separated multi-status filtering on the backend. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Adds validator search results, quick links panel when search is empty, and navigates contract results to /address/ routes. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…o /contracts
Removes transaction delete and status update endpoints (and all frontend
UI for them). Renames /api/explorer/state to /api/explorer/contracts and
removes the redundant /contracts/{id} detail endpoint. Adds address
lookup, creator info, TPS/volume stats, and multi-status filtering to
the backend queries.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
36 tests covering all explorer query functions: stats, paginated transactions, transaction relations, contracts/state listing, contract detail with code extraction, unified address resolution, validators, and providers. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
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:
📝 WalkthroughWalkthroughAdds address/validator resolution and richer stats to the explorer backend, removes transaction mutation endpoints, and converts many explorer frontend pages to async server components with address/contract/validators UIs, sorting, tabs, and date-range filtering. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant Router
participant Queries
participant DB
Client->>Router: GET /api/explorer/address/{addr}
Router->>Queries: get_address_info(session, address)
Queries->>DB: Query Validators by address
DB-->>Queries: validator or null
Queries->>DB: Query CurrentState by address
DB-->>Queries: state or null
Queries->>DB: Query Transactions (sender/receiver = address) with optional date-range
DB-->>Queries: transactions[]
alt address resolves to VALIDATOR
Queries-->>Router: { type: "VALIDATOR", validator, tx_count, ... }
else address resolves to CONTRACT
Queries-->>Router: { type: "CONTRACT", state, contract_code, creator_info, tx_count, ... }
else
Queries-->>Router: { type: "ACCOUNT", balance, tx_count, ... }
end
Router-->>Client: AddressInfo JSON
Client->>Client: Render AccountView / ContractView / ValidatorView
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
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. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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 |
…iew tab Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
The chart now always shows 14 days ending today, filling days without transactions with 0 instead of stopping at the last transaction date. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (8)
frontend/src/components/Simulator/AccountItem.vue (1)
91-100: Minor UX inconsistency: confirmation tooltip doesn't match disconnect terminology.The confirmation tooltip on line 95 says "Confirm deletion" for all accounts, but for external wallets the initial tooltip says "Disconnect Wallet". Consider making this consistent.
✨ Suggested fix for consistency
<button v-else data-testid="account-item-confirm-delete" `@click.stop`="deleteAddress" - v-tooltip="'Confirm deletion'" + v-tooltip="isExternal ? 'Confirm disconnect' : 'Confirm deletion'" >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/Simulator/AccountItem.vue` around lines 91 - 100, The confirm button's v-tooltip text is always "Confirm deletion" which conflicts with the original "Disconnect Wallet" wording for external wallets; update the tooltip to be conditional (e.g., use a computed property like confirmTooltip or a small inline ternary) so data-testid="account-item-confirm-delete" shows "Disconnect Wallet" for external wallets and "Confirm deletion" for normal accounts, and reference the existing deleteAddress handler to keep behavior unchanged.explorer/src/components/SparklineChart.tsx (1)
16-16: Consider handling single data point.When
data.length === 1, the chart renders a move command without any visible line. You may want to either returnnullor render a dot for this edge case.💡 Optional: handle single-point data
- if (data.length === 0) return null; + if (data.length <= 1) return null;Or alternatively, render a centered dot for single-point data.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/components/SparklineChart.tsx` at line 16, The current early-exit in SparklineChart (the if (data.length === 0) return null;) doesn't handle the single-point case where data.length === 1, which results in a stray move command and no visible mark; update SparklineChart to detect data.length === 1 and either return null or draw a centered dot for that point (e.g., compute its x/y via the existing xScale/yScale and render a circle), preserving existing behavior for data.length === 0 and the multi-point path rendering in the existing render/path logic.tests/db-sqlalchemy/explorer_queries_test.py (1)
246-254: Remove unused variabletx.The variable
txis assigned but never used. The hash is accessed viaunique_hashdirectly. As per coding guidelines, apply Black formatter for Python code formatting.♻️ Suggested fix
def test_search_by_hash(self, session: Session): unique_hash = "0xABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890" - tx = _make_tx(session, hash=unique_hash) + _make_tx(session, hash=unique_hash) _make_tx(session) session.commit()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/db-sqlalchemy/explorer_queries_test.py` around lines 246 - 254, In test_search_by_hash remove the unused local variable assignment to tx (created via _make_tx(session, hash=unique_hash)) since the test uses unique_hash directly; either delete the tx assignment or use it in an assertion, and then run Black to format the file; target the test_search_by_hash function and _make_tx invocation and ensure queries.get_all_transactions_paginated call and assertions remain unchanged.explorer/src/components/GlobalSearch.tsx (1)
50-54: Add AbortController cleanup on unmount.The debounce cleanup effect only clears the timeout, but doesn't abort any in-flight request. If a search is in progress when the component unmounts,
setResultsandsetLoadingwill be called on an unmounted component.♻️ Suggested fix
useEffect(() => { return () => { if (debounceRef.current) clearTimeout(debounceRef.current); + if (abortRef.current) abortRef.current.abort(); }; }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/components/GlobalSearch.tsx` around lines 50 - 54, The cleanup effect currently only clears debounceRef but must also abort any in-flight search request: introduce an AbortController ref (e.g., abortRef) used by the search function to attach signal to the fetch/axios request, set abortRef.current = new AbortController() when starting a request, and clear it when the request completes; in the useEffect cleanup return handler call abortRef.current?.abort() in addition to clearTimeout(debounceRef.current) to prevent setResults/setLoading on unmounted component and ensure the request error handling ignores AbortError so state updates are skipped when aborted.backend/protocol_rpc/explorer/queries.py (1)
299-310: Consider returning an error for invalid date formats instead of silently ignoring.Currently, invalid
from_dateorto_datevalues are silently ignored (lines 303-304, 309-310), while invalidstatusreturns empty results. This inconsistency could confuse API consumers who might not realize their date filter is being ignored.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/protocol_rpc/explorer/queries.py` around lines 299 - 310, The date parsing block for from_date and to_date currently swallows ValueError and silently ignores invalid formats; update the function that builds filters to validate datetime.fromisoformat for from_date and to_date and return a clear error (e.g., raise a validation/HTTP 400 error or propagate a specific exception) when parsing fails so behavior matches the invalid status handling; locate the parsing code around datetime.fromisoformat, the variables from_date/to_date, and the filters.append(Transactions.created_at ...) calls and replace the silent except-pass with an explicit error response including which parameter is invalid.explorer/src/app/transactions/[hash]/page.tsx (1)
71-88: Consider adding AbortController for in-flight polling requests.If the component unmounts while a polling fetch is in-flight (after the timeout fires but before the response arrives),
setData(updated)may be called on an unmounted component. While React handles this gracefully, adding an AbortController would be cleaner.♻️ Suggested improvement
useEffect(() => { if (!data || isTerminalStatus(data.transaction.status)) return; + const controller = new AbortController(); const timer = setTimeout(async () => { try { - const res = await fetch(`/api/transactions/${hash}`); + const res = await fetch(`/api/transactions/${hash}`, { signal: controller.signal }); if (res.ok) { const updated = await res.json(); setData(updated); } } catch { // silently ignore polling errors } }, 5000); - return () => clearTimeout(timer); + return () => { + clearTimeout(timer); + controller.abort(); + }; }, [data, hash]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/app/transactions/`[hash]/page.tsx around lines 71 - 88, The polling useEffect should cancel any in-flight fetch when the component unmounts or when dependencies change: create an AbortController inside the timeout handler, pass its signal to the fetch call in the useEffect where you currently call fetch(`/api/transactions/${hash}`), and in the cleanup return (or before calling setData) call controller.abort() so that aborted requests don't resolve to setData; ensure the catch still silently ignores abort errors but avoid calling setData(updated) if the controller.signal.aborted; reference the useEffect, timer, fetch, setData, hash, and isTerminalStatus identifiers when making these changes.explorer/src/app/contracts/[id]/page.tsx (1)
1-6: Use a permanent redirect for this retired route.This page is now a permanent alias of
/address/[addr];redirect()sends a temporary 307, so caches and crawlers keep treating/contracts/[id]as canonical.permanentRedirect()better matches the route migration.♻️ Suggested change
-import { redirect } from 'next/navigation'; +import { permanentRedirect } from 'next/navigation'; export default async function ContractDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; - redirect(`/address/${id}`); + permanentRedirect(`/address/${id}`); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/app/contracts/`[id]/page.tsx around lines 1 - 6, The page handler ContractDetailPage currently calls redirect(`/address/${id}`) which issues a temporary 307; replace that call with Next's permanentRedirect to send a 308/301-style permanent redirect so caches and crawlers treat /contracts/[id] as an alias of /address/[addr]. Update the import to use permanentRedirect (or import both and swap) and call permanentRedirect(`/address/${id}`) inside ContractDetailPage after resolving params.explorer/src/app/contracts/page.tsx (1)
145-145: Consider removingcursor-pointerclass from TableRow.The row itself has
cursor-pointer hover:bg-accent/50but clicking the row doesn't navigate anywhere—only the address link does. This could confuse users expecting the entire row to be clickable.💡 Suggested fix
- <TableRow key={state.id} className="cursor-pointer hover:bg-accent/50"> + <TableRow key={state.id} className="hover:bg-accent/50">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/app/contracts/page.tsx` at line 145, The TableRow element rendering rows uses className="cursor-pointer hover:bg-accent/50" which falsely implies the whole row is clickable; remove the "cursor-pointer" token from the TableRow className (keep hover:bg-accent/50 if desired) and instead ensure the actual clickable element (the address link component/rendering function) retains or gains the "cursor-pointer" class so only the link shows the pointer affordance; update references to TableRow and the address link component accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@explorer/src/app/address/`[addr]/page.tsx:
- Line 177: The Transactions StatItem is showing transactions.length (capped at
50) which is misleading; update the value to use the server-provided total when
available (data?.tx_count) and fall back to the array length with a “(recent)”
hint otherwise — i.e. change the StatItem value prop in page.tsx to check for
data?.tx_count (use String(data.tx_count)) and if missing use
String(transactions.length) + " (recent)"; reference the StatItem component and
the transactions and data variables used in this file (consistent with
AccountView/get_state_with_transactions).
In `@explorer/src/app/page.tsx`:
- Around line 211-242: The txVolume14d array may omit dates with zero
transactions, so normalize stats.txVolume14d into a fixed 14-day daily series
before rendering: compute the 14-day date range (e.g., last 14 calendar days or
from stats.txVolume14d[0] to last), build a map by date from stats.txVolume14d,
then produce an ordered array of 14 objects (each with date and count, using 0
when missing) and replace usages of stats.txVolume14d.map(...) and the
reduce/label accesses with this normalizedSeries (used in SparklineChart data,
the two date labels, and the Total calculation) so the sparkline shows exactly
14 daily buckets including zero-volume days.
In `@explorer/src/components/ConsensusJourney.tsx`:
- Around line 103-140: The circle UI is always created as a <button> in
ConsensusJourney (variable circle), causing inert tab stops when there is no
interaction (no timestamp/Tooltip); change circle to render a non-interactive
element in the non-interactive path: when timestamp is falsy, create circle as a
<span> or <div> (not a button) with role="img" and
aria-label={`${phase.label}${isReached ? ' — reached' : ''}`} and tabIndex={-1}
(or otherwise remove it from tab order); keep the same visual classes and child
content logic (using isFailedStep, isReached, isCurrent, idx) and continue to
use TooltipTrigger asChild only in the timestamp branch so keyboard focus is
only present when the Tooltip/interaction exists.
In `@explorer/src/components/DateTimePicker.tsx`:
- Around line 20-23: The code in DateTimePicker creates a Date from value
without validating it, so an invalid date (new Date(value) that's NaN) will
still be truthy and cause hours/minutes to become NaN and format(date, ...) to
throw; update the parsing to check date.getTime() !== date.getTime() or
Number.isNaN(date.getTime()) (i.e. validate via getTime()) and treat the date as
undefined when invalid, then only compute hours, minutes and call format(date,
...) when the date is valid (falling back to '00' for hours/minutes and an
empty/placeholder formatted string otherwise); change the variables named date,
hours, minutes and the format(...) usage in DateTimePicker accordingly so
invalid inputs are safely ignored.
---
Nitpick comments:
In `@backend/protocol_rpc/explorer/queries.py`:
- Around line 299-310: The date parsing block for from_date and to_date
currently swallows ValueError and silently ignores invalid formats; update the
function that builds filters to validate datetime.fromisoformat for from_date
and to_date and return a clear error (e.g., raise a validation/HTTP 400 error or
propagate a specific exception) when parsing fails so behavior matches the
invalid status handling; locate the parsing code around datetime.fromisoformat,
the variables from_date/to_date, and the filters.append(Transactions.created_at
...) calls and replace the silent except-pass with an explicit error response
including which parameter is invalid.
In `@explorer/src/app/contracts/`[id]/page.tsx:
- Around line 1-6: The page handler ContractDetailPage currently calls
redirect(`/address/${id}`) which issues a temporary 307; replace that call with
Next's permanentRedirect to send a 308/301-style permanent redirect so caches
and crawlers treat /contracts/[id] as an alias of /address/[addr]. Update the
import to use permanentRedirect (or import both and swap) and call
permanentRedirect(`/address/${id}`) inside ContractDetailPage after resolving
params.
In `@explorer/src/app/contracts/page.tsx`:
- Line 145: The TableRow element rendering rows uses className="cursor-pointer
hover:bg-accent/50" which falsely implies the whole row is clickable; remove the
"cursor-pointer" token from the TableRow className (keep hover:bg-accent/50 if
desired) and instead ensure the actual clickable element (the address link
component/rendering function) retains or gains the "cursor-pointer" class so
only the link shows the pointer affordance; update references to TableRow and
the address link component accordingly.
In `@explorer/src/app/transactions/`[hash]/page.tsx:
- Around line 71-88: The polling useEffect should cancel any in-flight fetch
when the component unmounts or when dependencies change: create an
AbortController inside the timeout handler, pass its signal to the fetch call in
the useEffect where you currently call fetch(`/api/transactions/${hash}`), and
in the cleanup return (or before calling setData) call controller.abort() so
that aborted requests don't resolve to setData; ensure the catch still silently
ignores abort errors but avoid calling setData(updated) if the
controller.signal.aborted; reference the useEffect, timer, fetch, setData, hash,
and isTerminalStatus identifiers when making these changes.
In `@explorer/src/components/GlobalSearch.tsx`:
- Around line 50-54: The cleanup effect currently only clears debounceRef but
must also abort any in-flight search request: introduce an AbortController ref
(e.g., abortRef) used by the search function to attach signal to the fetch/axios
request, set abortRef.current = new AbortController() when starting a request,
and clear it when the request completes; in the useEffect cleanup return handler
call abortRef.current?.abort() in addition to clearTimeout(debounceRef.current)
to prevent setResults/setLoading on unmounted component and ensure the request
error handling ignores AbortError so state updates are skipped when aborted.
In `@explorer/src/components/SparklineChart.tsx`:
- Line 16: The current early-exit in SparklineChart (the if (data.length === 0)
return null;) doesn't handle the single-point case where data.length === 1,
which results in a stray move command and no visible mark; update SparklineChart
to detect data.length === 1 and either return null or draw a centered dot for
that point (e.g., compute its x/y via the existing xScale/yScale and render a
circle), preserving existing behavior for data.length === 0 and the multi-point
path rendering in the existing render/path logic.
In `@frontend/src/components/Simulator/AccountItem.vue`:
- Around line 91-100: The confirm button's v-tooltip text is always "Confirm
deletion" which conflicts with the original "Disconnect Wallet" wording for
external wallets; update the tooltip to be conditional (e.g., use a computed
property like confirmTooltip or a small inline ternary) so
data-testid="account-item-confirm-delete" shows "Disconnect Wallet" for external
wallets and "Confirm deletion" for normal accounts, and reference the existing
deleteAddress handler to keep behavior unchanged.
In `@tests/db-sqlalchemy/explorer_queries_test.py`:
- Around line 246-254: In test_search_by_hash remove the unused local variable
assignment to tx (created via _make_tx(session, hash=unique_hash)) since the
test uses unique_hash directly; either delete the tx assignment or use it in an
assertion, and then run Black to format the file; target the test_search_by_hash
function and _make_tx invocation and ensure
queries.get_all_transactions_paginated call and assertions remain unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8ea1054b-c36f-4a6e-8e7b-62dff09f5fcd
📒 Files selected for processing (32)
backend/protocol_rpc/explorer/queries.pybackend/protocol_rpc/explorer/router.pyexplorer/package.jsonexplorer/src/app/address/[addr]/page.tsxexplorer/src/app/contracts/[id]/page.tsxexplorer/src/app/contracts/page.tsxexplorer/src/app/page.tsxexplorer/src/app/state/[id]/page.tsxexplorer/src/app/transactions/[hash]/components/DataTab.tsxexplorer/src/app/transactions/[hash]/components/MonitoringTab.tsxexplorer/src/app/transactions/[hash]/components/OverviewTab.tsxexplorer/src/app/transactions/[hash]/page.tsxexplorer/src/app/transactions/page.tsxexplorer/src/app/validators/page.tsxexplorer/src/components/AddressTransactionTable.tsxexplorer/src/components/CodeBlock.tsxexplorer/src/components/ConsensusJourney.tsxexplorer/src/components/DataDecodePanel.tsxexplorer/src/components/DateTimePicker.tsxexplorer/src/components/GlobalSearch.tsxexplorer/src/components/Navigation.tsxexplorer/src/components/SparklineChart.tsxexplorer/src/components/TransactionTable.tsxexplorer/src/components/index.tsexplorer/src/components/ui/calendar.tsxexplorer/src/components/ui/popover.tsxexplorer/src/lib/constants.tsexplorer/src/lib/formatters.tsexplorer/src/lib/types.tsfrontend/src/components/Simulator/AccountItem.vuefrontend/src/components/Simulator/AccountSelect.vuetests/db-sqlalchemy/explorer_queries_test.py
💤 Files with no reviewable changes (1)
- explorer/src/app/state/[id]/page.tsx
| {state && ( | ||
| <> | ||
| <StatItem icon={<Wallet className="w-5 h-5 text-green-600 dark:text-green-400" />} iconBg="bg-green-100 dark:bg-green-950" label="Balance" value={formatGenValue(state.balance)} /> | ||
| <StatItem icon={<ArrowRightLeft className="w-5 h-5 text-blue-600 dark:text-blue-400" />} iconBg="bg-blue-100 dark:bg-blue-950" label="Transactions" value={String(transactions.length)} /> |
There was a problem hiding this comment.
Transaction count shows array length, not total count.
The transactions.length displays the number of transactions in the returned array, which is capped at 50 by the backend (limit(50) in get_state_with_transactions). For contracts with more than 50 transactions, this could be misleading as it shows "50" instead of the actual total.
Consider either:
- Using
data.tx_countif available (for consistency with AccountView) - Adding "(recent)" suffix to clarify the count is limited
💡 Suggested clarification
- <StatItem icon={<ArrowRightLeft className="w-5 h-5 text-blue-600 dark:text-blue-400" />} iconBg="bg-blue-100 dark:bg-blue-950" label="Transactions" value={String(transactions.length)} />
+ <StatItem icon={<ArrowRightLeft className="w-5 h-5 text-blue-600 dark:text-blue-400" />} iconBg="bg-blue-100 dark:bg-blue-950" label="Recent Transactions" value={String(transactions.length)} />📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <StatItem icon={<ArrowRightLeft className="w-5 h-5 text-blue-600 dark:text-blue-400" />} iconBg="bg-blue-100 dark:bg-blue-950" label="Transactions" value={String(transactions.length)} /> | |
| <StatItem icon={<ArrowRightLeft className="w-5 h-5 text-blue-600 dark:text-blue-400" />} iconBg="bg-blue-100 dark:bg-blue-950" label="Recent Transactions" value={String(transactions.length)} /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@explorer/src/app/address/`[addr]/page.tsx at line 177, The Transactions
StatItem is showing transactions.length (capped at 50) which is misleading;
update the value to use the server-provided total when available
(data?.tx_count) and fall back to the array length with a “(recent)” hint
otherwise — i.e. change the StatItem value prop in page.tsx to check for
data?.tx_count (use String(data.tx_count)) and if missing use
String(transactions.length) + " (recent)"; reference the StatItem component and
the transactions and data variables used in this file (consistent with
AccountView/get_state_with_transactions).
explorer/src/app/page.tsx
Outdated
| {stats.txVolume14d.length > 0 && ( | ||
| <Card> | ||
| <CardHeader> | ||
| <div className="flex items-center gap-3"> | ||
| <div className="bg-cyan-50 dark:bg-cyan-950 p-2 rounded-lg"> | ||
| <BarChart3 className="w-5 h-5 text-cyan-600 dark:text-cyan-400" /> | ||
| </div> | ||
| <div> | ||
| <CardTitle className="text-lg">Transaction Volume (14 days)</CardTitle> | ||
| <p className="text-sm text-muted-foreground mt-0.5"> | ||
| {stats.txVolume14d[0]?.date} — {stats.txVolume14d[stats.txVolume14d.length - 1]?.date} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </CardHeader> | ||
| <CardContent> | ||
| <div className="text-cyan-600 dark:text-cyan-400"> | ||
| <SparklineChart | ||
| data={stats.txVolume14d.map(d => d.count)} | ||
| width={800} | ||
| height={80} | ||
| className="w-full" | ||
| /> | ||
| </div> | ||
| <div className="flex justify-between mt-2 text-xs text-muted-foreground"> | ||
| <span>{stats.txVolume14d[0]?.date}</span> | ||
| <span>Total: {stats.txVolume14d.reduce((s, d) => s + d.count, 0).toLocaleString()} txs</span> | ||
| <span>{stats.txVolume14d[stats.txVolume14d.length - 1]?.date}</span> | ||
| </div> | ||
| </CardContent> | ||
| </Card> | ||
| )} |
There was a problem hiding this comment.
Fill missing zero-volume days before plotting this 14-day series.
/api/stats currently returns only grouped dates that have transactions, so rendering stats.txVolume14d.map(d => d.count) compresses quiet days and can show a shorter-than-14-day range here. Please normalize the series to fixed daily buckets, or have the API return 14 buckets, before rendering the sparkline.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@explorer/src/app/page.tsx` around lines 211 - 242, The txVolume14d array may
omit dates with zero transactions, so normalize stats.txVolume14d into a fixed
14-day daily series before rendering: compute the 14-day date range (e.g., last
14 calendar days or from stats.txVolume14d[0] to last), build a map by date from
stats.txVolume14d, then produce an ordered array of 14 objects (each with date
and count, using 0 when missing) and replace usages of
stats.txVolume14d.map(...) and the reduce/label accesses with this
normalizedSeries (used in SparklineChart data, the two date labels, and the
Total calculation) so the sparkline shows exactly 14 daily buckets including
zero-volume days.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/protocol_rpc/explorer/queries.py (1)
489-495:⚠️ Potential issue | 🟡 MinorDocstring incorrectly states deploy transaction is type 0.
The docstring says "deploy transaction (type 0)" but deploy transactions are type 1 throughout this file (lines 134, 182, 442, 544, 582). The function itself doesn't filter by type, but the documentation is misleading.
📝 Proposed fix
def _extract_contract_code(session: Session, state_id: str) -> Optional[str]: """Find the contract source code for a given contract address. - Looks at the ``data`` JSONB column of the deploy transaction (type 0) for + Looks at the ``data`` JSONB column of the deploy transaction (type 1) for this contract. The ``contract_code`` field is stored as a base64-encoded string; we decode it to return the Python source code. """🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/protocol_rpc/explorer/queries.py` around lines 489 - 495, The docstring for _extract_contract_code incorrectly refers to deploy transactions as "type 0"; update the docstring to reflect the correct deploy transaction type (type 1) or remove the explicit type reference since _extract_contract_code does not filter by transaction type—make the docstring accurate by stating it reads the deploy transaction's ``data`` JSONB (contract_code is base64-encoded) and, if you keep the type mention, change "type 0" to "type 1" and/or note that the function does not enforce the transaction type.
🧹 Nitpick comments (2)
backend/protocol_rpc/explorer/queries.py (2)
484-484: Minor: InconsistenttotalPagescalculation.This uses
(total + limit - 1) // limitwhileget_all_transactions_paginated(line 353) usesmath.ceil(total / limit). Both produce identical results for positive integers, but consistency aids readability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/protocol_rpc/explorer/queries.py` at line 484, The totalPages calculation at "totalPages": (total + limit - 1) // limit if total > 0 else 0 is inconsistent with get_all_transactions_paginated which uses math.ceil(total / limit); update the expression to use math.ceil(total / limit) for consistency (e.g., math.ceil(total / limit) if total > 0 else 0), and ensure the math import is available in this module; keep the result as an int and preserve the zero handling when total is 0.
304-315: Consider logging or returning feedback for invalid date formats.Invalid
from_dateandto_datevalues are silently ignored. This differs from invalidstatusvalues which return empty results. Users may not realize their date filter wasn't applied.♻️ Suggested approach: return empty results for invalid dates (consistent with status handling)
if from_date: try: dt = datetime.fromisoformat(from_date) filters.append(Transactions.created_at >= dt) except ValueError: - pass + return { + "transactions": [], + "pagination": {"page": page, "limit": limit, "total": 0, "totalPages": 0}, + } if to_date: try: dt = datetime.fromisoformat(to_date) filters.append(Transactions.created_at <= dt) except ValueError: - pass + return { + "transactions": [], + "pagination": {"page": page, "limit": limit, "total": 0, "totalPages": 0}, + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/protocol_rpc/explorer/queries.py` around lines 304 - 315, The date parsing block for from_date/to_date currently swallows ValueError and silently ignores invalid ISO dates; modify the function in backend/protocol_rpc/explorer/queries.py so that when datetime.fromisoformat(from_date) or datetime.fromisoformat(to_date) raises ValueError you handle it consistently with invalid status handling—return an empty result set (or an empty query) immediately instead of silently continuing; update the callers or surrounding function (the query builder that uses Transactions.created_at and the from_date/to_date variables) to return the same empty-response behavior and optionally log the invalid date string for diagnostics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/protocol_rpc/explorer/queries.py`:
- Around line 661-668: The ACCOUNT response for the "CurrentState without deploy
tx" branch is missing tx_count, first_tx_time, and last_tx_time, causing an
inconsistent schema; update the block that returns {"type": "ACCOUNT",
"address": address, "balance": state.balance, "transactions": []} so it also
includes tx_count (e.g., 0) and first_tx_time/last_tx_time (e.g., None or null)
to match the other ACCOUNT response shape (refer to the same "ACCOUNT" return
branch and the variables state and address to locate and align the fields).
---
Outside diff comments:
In `@backend/protocol_rpc/explorer/queries.py`:
- Around line 489-495: The docstring for _extract_contract_code incorrectly
refers to deploy transactions as "type 0"; update the docstring to reflect the
correct deploy transaction type (type 1) or remove the explicit type reference
since _extract_contract_code does not filter by transaction type—make the
docstring accurate by stating it reads the deploy transaction's ``data`` JSONB
(contract_code is base64-encoded) and, if you keep the type mention, change
"type 0" to "type 1" and/or note that the function does not enforce the
transaction type.
---
Nitpick comments:
In `@backend/protocol_rpc/explorer/queries.py`:
- Line 484: The totalPages calculation at "totalPages": (total + limit - 1) //
limit if total > 0 else 0 is inconsistent with get_all_transactions_paginated
which uses math.ceil(total / limit); update the expression to use
math.ceil(total / limit) for consistency (e.g., math.ceil(total / limit) if
total > 0 else 0), and ensure the math import is available in this module; keep
the result as an int and preserve the zero handling when total is 0.
- Around line 304-315: The date parsing block for from_date/to_date currently
swallows ValueError and silently ignores invalid ISO dates; modify the function
in backend/protocol_rpc/explorer/queries.py so that when
datetime.fromisoformat(from_date) or datetime.fromisoformat(to_date) raises
ValueError you handle it consistently with invalid status handling—return an
empty result set (or an empty query) immediately instead of silently continuing;
update the callers or surrounding function (the query builder that uses
Transactions.created_at and the from_date/to_date variables) to return the same
empty-response behavior and optionally log the invalid date string for
diagnostics.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 38200cdb-27ec-4ce6-81de-69256891b18e
📒 Files selected for processing (2)
backend/protocol_rpc/explorer/queries.pyexplorer/src/app/transactions/[hash]/components/OverviewTab.tsx
…fetching Convert dashboard, validators, providers, and address pages from client-side useEffect fetching to async server components with direct backend fetch. This eliminates loading spinners on initial page load. Also converts StatsBar to a server component (cached 30s) to avoid redundant API calls on every navigation, adds Page Visibility API to transaction detail polling, and creates loading.tsx files for streaming feedback during server-side navigation. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Use cache: 'no-store' for all server-side fetches since this is a dev sandbox where data changes frequently. Pages are still server-rendered (no loading spinners) but always fetch fresh data on each request. Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Guard against invalid date values in DateTimePicker to prevent crashes - Use tx_count from backend instead of array length in contract view - Use <span> instead of <button> for non-interactive consensus steps - Add missing tx_count/first_tx_time/last_tx_time to ACCOUNT response Co-Authored-By: Claude Opus 4.6 <[email protected]>
…enhancements # Conflicts: # backend/protocol_rpc/explorer/queries.py # backend/protocol_rpc/explorer/router.py # explorer/src/app/contracts/[id]/page.tsx # explorer/src/app/page.tsx # explorer/src/app/transactions/[hash]/components/OverviewTab.tsx # explorer/src/app/transactions/page.tsx # explorer/src/app/validators/page.tsx # explorer/src/components/ConsensusJourney.tsx # explorer/src/components/DateTimePicker.tsx # explorer/src/components/GlobalSearch.tsx # explorer/src/components/TransactionTable.tsx
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Split dashboard into 3 independent async server components wrapped in Suspense boundaries so each section streams in as its data resolves. Uses React cache() to deduplicate the /stats fetch across sections. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…alization error Lucide icon components can't be serialized across the Server→Client boundary. StatCard now maps string icon names to components internally. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (5)
tests/db-sqlalchemy/explorer_queries_test.py (1)
156-162: Redundant assertion can be simplified.Line 158's assertion is always true for any list:
assert result["txVolume14d"] == [] or isinstance(result["txVolume14d"], list)✏️ Suggested simplification
- assert result["txVolume14d"] == [] or isinstance(result["txVolume14d"], list) + assert isinstance(result["txVolume14d"], list) + assert len(result["txVolume14d"]) == 14 # Should always have 14 days🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/db-sqlalchemy/explorer_queries_test.py` around lines 156 - 162, The assertion checking txVolume14d is redundant; replace the line asserting result["txVolume14d"] == [] or isinstance(result["txVolume14d"], list) with a single type check asserting isinstance(result["txVolume14d"], list) so the test only verifies the value is a list (use the existing result["txVolume14d"] symbol to locate and update the assertion).explorer/src/app/validators/loading.tsx (1)
1-9: Consider extracting a sharedRouteLoadingSpinnercomponent.Same JSX appears in multiple
loading.tsxfiles; importing a shared spinner component would reduce duplication while keeping route-levelloading.tsxconventions intact.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/app/validators/loading.tsx` around lines 1 - 9, Duplicate route loading JSX is present in the Loading component; extract the spinner markup into a shared RouteLoadingSpinner component and import it from each route-level loading.tsx to reduce duplication. Create a new RouteLoadingSpinner component that renders the centered container and Loader2 with the same classes, export it (e.g., RouteLoadingSpinner), then replace the JSX in the existing Loading function with a simple return of <RouteLoadingSpinner /> and update other route loading files to import and use RouteLoadingSpinner instead.explorer/src/lib/fetchBackend.ts (1)
11-11: Normalizepathbefore URL construction.
fetchBackendcurrently assumes callers always pass a leading slash. Normalizing once in this utility prevents subtle endpoint bugs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/lib/fetchBackend.ts` at line 11, The URL builder in fetchBackend constructs const url = `${BACKEND_URL}/api/explorer${path}` without normalizing path; update the fetchBackend utility to normalize the incoming path (e.g., ensure it starts with a single leading slash and strip extra slashes) before constructing the url so callers can pass "foo", "/foo", or "///foo" safely; modify the code around the url construction (the line using BACKEND_URL and path) to compute a normalizedPath then use `${BACKEND_URL}/api/explorer${normalizedPath}`.explorer/src/components/StatsBar.tsx (1)
15-17: Avoid fully silent failure in the catch path.Line 15 currently suppresses all errors. Consider adding a low-noise log/metric so backend outages are visible during incidents.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/components/StatsBar.tsx` around lines 15 - 17, The catch block in the StatsBar component currently swallows all errors silently; update the catch in the async/render path of StatsBar (the catch after the data fetching logic inside the StatsBar component) to emit a low-noise signal — e.g., call a lightweight logger or telemetry increment (console.warn or a metrics call such as telemetry.increment('statsbar.fetch_error')) with a short message and the error details, then still return null so UI behavior is unchanged.explorer/src/app/DashboardSections.tsx (1)
19-30: Avoid paying for two recent-transactions queries on every dashboard render.
StatsDataalready includesrecentTransactionsfrom/stats, butRecentTransactionsSectionfetches/transactions?limit=10again. Unless the table needs fields that/statsintentionally omits, this doubles the backend work and can make the dashboard sections disagree. Either reusegetStats().recentTransactionshere or droprecentTransactionsfrom the stats response.Also applies to: 202-203
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/app/DashboardSections.tsx` around lines 19 - 30, The dashboard is performing two queries for recent transactions; fix RecentTransactionsSection to reuse the recentTransactions already provided on the stats payload instead of re-fetching `/transactions?limit=10` (or conversely remove recentTransactions from the StatsData/`getStats()` response if the table requires additional fields), by changing RecentTransactionsSection to accept/use `stats.recentTransactions` from the `getStats()` result (reference: StatsData.recentTransactions, getStats(), RecentTransactionsSection) and eliminate the extra fetch to `/transactions?limit=10` so the UI uses a single source of truth and avoids double backend work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@explorer/src/app/address/`[addr]/AddressContent.tsx:
- Around line 88-90: The Transactions tab label currently shows the loaded array
lengths (txs.length / transactions.length) which can drift from the API's capped
total; update the tab badge text to use data.tx_count ?? txs.length for the
"transactions" TabsTrigger (value="transactions") and similarly use
data.tx_count ?? transactions.length wherever the other tab label at the second
location is rendered, ensuring you reference the existing variables txs,
transactions and data.tx_count when computing the displayed count.
In `@explorer/src/app/page.tsx`:
- Around line 16-42: Wrap each of the three lazy UI sections with their own
Error Boundary by converting each Suspense-wrapped component into a parallel
route (or a child route) that provides an error.tsx for that segment: create
routes/components for StatCardsSection, ChartsSection, and
RecentTransactionsSection that call fetchBackend() internally and add an
adjacent error.tsx to render a localized error UI, then replace the inline
Suspense usages with route-based imports so failures inside StatCardsSection,
ChartsSection, or RecentTransactionsSection are caught by their own error.tsx
instead of bubbling to the global Error Boundary.
In `@explorer/src/app/providers/ProvidersContent.tsx`:
- Around line 134-146: ProvidersContent.tsx currently calls format(new
Date(provider.created_at)) and format(new Date(provider.updated_at)) without
guarding for null; update the UI rendering to check provider.created_at and
provider.updated_at are non-null before formatting (e.g., conditional rendering
or a fallback string like "—" when null) and ensure the LLMProvider type
definitions for created_at/updated_at are widened to string | null so the
compiler enforces the guard; locate the usage in ProvidersContent.tsx and the
LLMProvider interface/type to apply these changes.
In `@explorer/src/app/transactions/`[hash]/page.tsx:
- Around line 71-108: The polling stops permanently after a fetch error because
the effect reschedules only when data changes; update schedulePoll usage so it
is always re-scheduled regardless of fetch success or failure: inside the
schedulePoll async callback (the fetch in useEffect / schedulePoll), ensure you
call schedulePoll again after handling the response (either in a finally block
or after both the success path and the catch) so transient network errors don't
halt polling; reference the existing symbols useEffect, schedulePoll, timer,
onVisibilityChange, fetch, and setData when making this change.
In `@explorer/src/app/validators/ValidatorsContent.tsx`:
- Around line 137-141: The component renders formatter output for
validator.created_at using format(new Date(validator.created_at), 'PPpp') which
can throw if the date is malformed; update the render in ValidatorsContent (the
block referencing validator.created_at and format) to validate the parsed date
with date-fns isValid before calling format (or parse once into a const date and
reuse it), and only render the Created row when isValid(date) is true to prevent
RangeError crashes.
In `@explorer/src/lib/fetchBackend.ts`:
- Line 12: The fetch call using fetch(url, { cache: 'no-store' }) lacks a
timeout; wrap the request with an AbortController and a timer (e.g., 5s) and
pass controller.signal into fetch so the request is aborted on timeout, clear
the timer on success, and throw or handle the abort error so the server-side
rendering won't hang; update the code paths around the variable res and the
function that performs the fetch to use this abort logic and ensure resources
are cleaned up.
In `@tests/db-sqlalchemy/explorer_queries_test.py`:
- Around line 126-162: The session fixture currently lets test code call
session.commit() and persist data across test classes (e.g.,
TestGetStatsCounts.test_counts_with_data affects
TestGetStats.test_empty_database); change the fixture that creates the
SQLAlchemy Session to open a top-level connection.transaction and then start a
nested transaction/savepoint (use sessionmaker(bind=connection,
expire_on_commit=False), call connection.begin() and connection.begin_nested()),
register an event listener on the Session for "after_transaction_end" (the
restart_savepoint pattern used in SQLAlchemy testing) to recreate the nested
savepoint after commits, yield the session to tests, and finally close the
session and rollback the outer transaction before closing the connection so
commits inside tests do not persist. Ensure you reference the session fixture,
the connection/transaction/nested savepoint, and the after_transaction_end
listener when making the change.
---
Nitpick comments:
In `@explorer/src/app/DashboardSections.tsx`:
- Around line 19-30: The dashboard is performing two queries for recent
transactions; fix RecentTransactionsSection to reuse the recentTransactions
already provided on the stats payload instead of re-fetching
`/transactions?limit=10` (or conversely remove recentTransactions from the
StatsData/`getStats()` response if the table requires additional fields), by
changing RecentTransactionsSection to accept/use `stats.recentTransactions` from
the `getStats()` result (reference: StatsData.recentTransactions, getStats(),
RecentTransactionsSection) and eliminate the extra fetch to
`/transactions?limit=10` so the UI uses a single source of truth and avoids
double backend work.
In `@explorer/src/app/validators/loading.tsx`:
- Around line 1-9: Duplicate route loading JSX is present in the Loading
component; extract the spinner markup into a shared RouteLoadingSpinner
component and import it from each route-level loading.tsx to reduce duplication.
Create a new RouteLoadingSpinner component that renders the centered container
and Loader2 with the same classes, export it (e.g., RouteLoadingSpinner), then
replace the JSX in the existing Loading function with a simple return of
<RouteLoadingSpinner /> and update other route loading files to import and use
RouteLoadingSpinner instead.
In `@explorer/src/components/StatsBar.tsx`:
- Around line 15-17: The catch block in the StatsBar component currently
swallows all errors silently; update the catch in the async/render path of
StatsBar (the catch after the data fetching logic inside the StatsBar component)
to emit a low-noise signal — e.g., call a lightweight logger or telemetry
increment (console.warn or a metrics call such as
telemetry.increment('statsbar.fetch_error')) with a short message and the error
details, then still return null so UI behavior is unchanged.
In `@explorer/src/lib/fetchBackend.ts`:
- Line 11: The URL builder in fetchBackend constructs const url =
`${BACKEND_URL}/api/explorer${path}` without normalizing path; update the
fetchBackend utility to normalize the incoming path (e.g., ensure it starts with
a single leading slash and strip extra slashes) before constructing the url so
callers can pass "foo", "/foo", or "///foo" safely; modify the code around the
url construction (the line using BACKEND_URL and path) to compute a
normalizedPath then use `${BACKEND_URL}/api/explorer${normalizedPath}`.
In `@tests/db-sqlalchemy/explorer_queries_test.py`:
- Around line 156-162: The assertion checking txVolume14d is redundant; replace
the line asserting result["txVolume14d"] == [] or
isinstance(result["txVolume14d"], list) with a single type check asserting
isinstance(result["txVolume14d"], list) so the test only verifies the value is a
list (use the existing result["txVolume14d"] symbol to locate and update the
assertion).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f71d96dd-db66-4218-ab0d-1620b5fa35ac
📒 Files selected for processing (21)
backend/protocol_rpc/explorer/queries.pyexplorer/src/app/DashboardSections.tsxexplorer/src/app/address/[addr]/AddressContent.tsxexplorer/src/app/address/[addr]/loading.tsxexplorer/src/app/address/[addr]/page.tsxexplorer/src/app/layout.tsxexplorer/src/app/loading.tsxexplorer/src/app/page.tsxexplorer/src/app/providers/ProvidersContent.tsxexplorer/src/app/providers/loading.tsxexplorer/src/app/providers/page.tsxexplorer/src/app/transactions/[hash]/page.tsxexplorer/src/app/validators/ValidatorsContent.tsxexplorer/src/app/validators/loading.tsxexplorer/src/app/validators/page.tsxexplorer/src/components/ConsensusJourney.tsxexplorer/src/components/DateTimePicker.tsxexplorer/src/components/StatCard.tsxexplorer/src/components/StatsBar.tsxexplorer/src/lib/fetchBackend.tstests/db-sqlalchemy/explorer_queries_test.py
🚧 Files skipped from review as they are similar to previous changes (1)
- explorer/src/app/address/[addr]/page.tsx
| <Suspense | ||
| fallback={ | ||
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4"> | ||
| {Array.from({ length: 5 }, (_, i) => ( | ||
| <CardSkeleton key={i} className="h-[88px]" /> | ||
| ))} | ||
| </div> | ||
| } | ||
| > | ||
| <StatCardsSection /> | ||
| </Suspense> | ||
|
|
||
| <Suspense | ||
| fallback={ | ||
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-4"> | ||
| {Array.from({ length: 3 }, (_, i) => ( | ||
| <CardSkeleton key={i} className="h-[140px]" /> | ||
| ))} | ||
| </div> | ||
| </CardHeader> | ||
| <CardContent className="p-0 pt-4"> | ||
| <TransactionTable | ||
| transactions={stats.recentTransactions} | ||
| showRelations={false} | ||
| /> | ||
| </CardContent> | ||
| </Card> | ||
| } | ||
| > | ||
| <ChartsSection /> | ||
| </Suspense> | ||
|
|
||
| <Suspense fallback={<CardSkeleton className="h-[300px]" />}> | ||
| <RecentTransactionsSection /> | ||
| </Suspense> |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In React 19 and Next.js App Router, does <Suspense> handle errors thrown by async server components, or only loading states? What boundary or pattern is needed to keep sibling sections rendering when one section's fetch fails?
💡 Result:
<Suspense>is for “waiting” (loading/streaming), not for recovering from failures. If the async work errors, React’s model is: show the nearest Error Boundary, not the Suspense fallback. (react.dev)- In Next.js App Router, errors thrown while rendering a Server Component (including failed data fetching that throws) are routed to the nearest route-segment
error.(js|tsx)boundary, which Next automatically wires up as a React Error Boundary for that segment. (nextjs.org)
To keep sibling sections rendering when one section’s fetch fails: you need an error boundary per section, not just Suspense. In App Router, the practical pattern is to isolate each section into its own route segment (often via Parallel Routes/slots) and give each segment its own error.tsx (and optionally loading.tsx). Then one section can fail and render its error.tsx while siblings continue to render. (nextjs.org)
Citations:
- 1: https://react.dev/reference/react/Suspense?utm_source=openai
- 2: https://nextjs.org/docs/14/app/building-your-application/routing/error-handling?utm_source=openai
- 3: https://nextjs.org/docs/14/app/building-your-application/routing/error-handling?utm_source=openai
🏁 Script executed:
# Check the file structure around explorer/src/app/page.tsx
cd explorer && find . -type f -name "error.tsx" -o -name "page.tsx" | head -20Repository: genlayerlabs/genlayer-studio
Length of output: 317
🏁 Script executed:
# Look for error boundaries in the app directory
find explorer/src/app -type f \( -name "*.tsx" -o -name "*.ts" \) | head -30Repository: genlayerlabs/genlayer-studio
Length of output: 1116
🏁 Script executed:
# Check if there's an error.tsx at the root or parent levels
cat -n explorer/src/app/error.tsx 2>/dev/null || echo "No error.tsx at app root"Repository: genlayerlabs/genlayer-studio
Length of output: 96
🏁 Script executed:
# Inspect the DashboardSections.tsx to see if sections have internal error handling
head -100 explorer/src/app/DashboardSections.tsx 2>/dev/null || echo "File not found"Repository: genlayerlabs/genlayer-studio
Length of output: 3763
🏁 Script executed:
# Check if there's an error.tsx at the root layout level
find explorer/src -maxdepth 2 -name "error.tsx"Repository: genlayerlabs/genlayer-studio
Length of output: 54
🏁 Script executed:
# Check the full page.tsx to see the complete Suspense setup
cat -n explorer/src/app/page.tsx | head -50Repository: genlayerlabs/genlayer-studio
Length of output: 1750
🏁 Script executed:
# Look for any try-catch or error boundaries in DashboardSections
grep -n "try\|catch\|error\|Error" explorer/src/app/DashboardSections.tsx | head -20Repository: genlayerlabs/genlayer-studio
Length of output: 54
Add per-section error boundaries to isolate fetch failures.
Suspense only handles loading states. If StatCardsSection, ChartsSection, or RecentTransactionsSection throws during fetchBackend(), the error bubbles to the nearest Error Boundary, causing the entire dashboard to fail. To keep sibling sections rendering when one fetch fails, use Parallel Routes with individual error.tsx files for each section instead of relying on Suspense alone.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@explorer/src/app/page.tsx` around lines 16 - 42, Wrap each of the three lazy
UI sections with their own Error Boundary by converting each Suspense-wrapped
component into a parallel route (or a child route) that provides an error.tsx
for that segment: create routes/components for StatCardsSection, ChartsSection,
and RecentTransactionsSection that call fetchBackend() internally and add an
adjacent error.tsx to render a localized error UI, then replace the inline
Suspense usages with route-based imports so failures inside StatCardsSection,
ChartsSection, or RecentTransactionsSection are caught by their own error.tsx
instead of bubbling to the global Error Boundary.
| <div className="flex justify-between items-center"> | ||
| <span className="text-muted-foreground">Created</span> | ||
| <span className="flex items-center gap-1"> | ||
| <Clock className="w-3 h-3" /> | ||
| {format(new Date(provider.created_at), 'PPpp')} | ||
| </span> | ||
| </div> | ||
| <div className="flex justify-between items-center"> | ||
| <span className="text-muted-foreground">Updated</span> | ||
| <span className="flex items-center gap-1"> | ||
| <Clock className="w-3 h-3" /> | ||
| {format(new Date(provider.updated_at), 'PPpp')} | ||
| </span> |
There was a problem hiding this comment.
Guard nullable provider timestamps before formatting them.
backend/protocol_rpc/explorer/queries.py:103-114 can serialize created_at and updated_at as null, but these lines always call new Date(...). That turns missing values into epoch dates or invalid output here. Please add an explicit fallback and widen LLMProvider.created_at / updated_at to string | null.
🩹 Proposed fix
- <div className="flex justify-between items-center">
- <span className="text-muted-foreground">Created</span>
- <span className="flex items-center gap-1">
- <Clock className="w-3 h-3" />
- {format(new Date(provider.created_at), 'PPpp')}
- </span>
- </div>
- <div className="flex justify-between items-center">
- <span className="text-muted-foreground">Updated</span>
- <span className="flex items-center gap-1">
- <Clock className="w-3 h-3" />
- {format(new Date(provider.updated_at), 'PPpp')}
- </span>
- </div>
+ <div className="flex justify-between items-center">
+ <span className="text-muted-foreground">Created</span>
+ <span className="flex items-center gap-1">
+ <Clock className="w-3 h-3" />
+ {provider.created_at ? format(new Date(provider.created_at), 'PPpp') : 'Unknown'}
+ </span>
+ </div>
+ <div className="flex justify-between items-center">
+ <span className="text-muted-foreground">Updated</span>
+ <span className="flex items-center gap-1">
+ <Clock className="w-3 h-3" />
+ {provider.updated_at ? format(new Date(provider.updated_at), 'PPpp') : 'Unknown'}
+ </span>
+ </div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="flex justify-between items-center"> | |
| <span className="text-muted-foreground">Created</span> | |
| <span className="flex items-center gap-1"> | |
| <Clock className="w-3 h-3" /> | |
| {format(new Date(provider.created_at), 'PPpp')} | |
| </span> | |
| </div> | |
| <div className="flex justify-between items-center"> | |
| <span className="text-muted-foreground">Updated</span> | |
| <span className="flex items-center gap-1"> | |
| <Clock className="w-3 h-3" /> | |
| {format(new Date(provider.updated_at), 'PPpp')} | |
| </span> | |
| <div className="flex justify-between items-center"> | |
| <span className="text-muted-foreground">Created</span> | |
| <span className="flex items-center gap-1"> | |
| <Clock className="w-3 h-3" /> | |
| {provider.created_at ? format(new Date(provider.created_at), 'PPpp') : 'Unknown'} | |
| </span> | |
| </div> | |
| <div className="flex justify-between items-center"> | |
| <span className="text-muted-foreground">Updated</span> | |
| <span className="flex items-center gap-1"> | |
| <Clock className="w-3 h-3" /> | |
| {provider.updated_at ? format(new Date(provider.updated_at), 'PPpp') : 'Unknown'} | |
| </span> | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@explorer/src/app/providers/ProvidersContent.tsx` around lines 134 - 146,
ProvidersContent.tsx currently calls format(new Date(provider.created_at)) and
format(new Date(provider.updated_at)) without guarding for null; update the UI
rendering to check provider.created_at and provider.updated_at are non-null
before formatting (e.g., conditional rendering or a fallback string like "—"
when null) and ensure the LLMProvider type definitions for created_at/updated_at
are widened to string | null so the compiler enforces the guard; locate the
usage in ProvidersContent.tsx and the LLMProvider interface/type to apply these
changes.
| {provider.config && (typeof provider.config === 'object' ? Object.keys(provider.config).length > 0 : provider.config) && ( | ||
| <div> | ||
| <h4 className="font-medium text-foreground mb-2">Config</h4> | ||
| <div className="bg-muted p-3 rounded-lg overflow-auto max-h-48"> | ||
| <JsonViewer data={provider.config} initialExpanded={false} /> | ||
| </div> | ||
| </div> | ||
| )} | ||
| {provider.plugin_config && Object.keys(provider.plugin_config).length > 0 && ( | ||
| <div> | ||
| <h4 className="font-medium text-foreground mb-2">Plugin Config</h4> | ||
| <div className="bg-muted p-3 rounded-lg overflow-auto max-h-48"> | ||
| <JsonViewer data={provider.plugin_config} initialExpanded={false} /> |
There was a problem hiding this comment.
Redact provider configs before they reach the browser.
provider.config and provider.plugin_config are rendered verbatim here, and backend/protocol_rpc/explorer/queries.py:103-114 serializes those fields raw. If they contain API keys, tokens, or internal endpoints, the /providers response already leaks them to any caller. Please omit or redact sensitive keys server-side, then render only sanitized values here. The same exposure pattern is also introduced in explorer/src/app/address/[addr]/AddressContent.tsx:251-262.
| {validator.created_at && ( | ||
| <div className="flex justify-between"> | ||
| <span className="text-muted-foreground">Created</span> | ||
| <span>{format(new Date(validator.created_at), 'PPpp')}</span> | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd explorer && head -20 src/app/validators/ValidatorsContent.tsx | cat -nRepository: genlayerlabs/genlayer-studio
Length of output: 1011
🏁 Script executed:
rg "import.*format" explorer/src/app/validators/ValidatorsContent.tsxRepository: genlayerlabs/genlayer-studio
Length of output: 157
🏁 Script executed:
sed -n '130,150p' explorer/src/app/validators/ValidatorsContent.tsx | cat -nRepository: genlayerlabs/genlayer-studio
Length of output: 1437
🏁 Script executed:
rg "from.*date-fns" explorer/Repository: genlayerlabs/genlayer-studio
Length of output: 1052
🏁 Script executed:
node <<'EOF'
const { format } = require('date-fns');
// Test 1: new Date() with invalid string
const invalidDate = new Date("not-a-date");
console.log("Invalid Date object:", invalidDate);
console.log("Is it NaN?", isNaN(invalidDate.getTime()));
// Test 2: does format() throw on Invalid Date?
try {
const result = format(invalidDate, 'PPpp');
console.log("format() succeeded:", result);
} catch (e) {
console.log("format() threw error:", e.message);
}
// Test 3: valid date string
const validDate = new Date("2024-01-15T10:30:00Z");
try {
const result = format(validDate, 'PPpp');
console.log("Valid date formatted:", result);
} catch (e) {
console.log("format() threw on valid date:", e.message);
}
// Test 4: null/undefined
try {
const result = format(null, 'PPpp');
console.log("null formatted:", result);
} catch (e) {
console.log("format(null) threw:", e.message);
}
EOFRepository: genlayerlabs/genlayer-studio
Length of output: 910
🏁 Script executed:
cat explorer/package.json | grep -A 5 "date-fns"Repository: genlayerlabs/genlayer-studio
Length of output: 233
🌐 Web query:
date-fns format() function behavior with invalid date object
💡 Result:
In date-fns, format(date, ...) expects a valid Date (i.e., date.getTime() is not NaN). If you pass an Invalid Date (e.g., new Date('') / new Date('invalid')), format() throws (typically a RangeError with message “Invalid time value”), rather than returning a string. This aligns with the JS/Intl behavior for invalid dates (e.g., toISOString() also throws on invalid dates). [1] [2] [3]
Practical guard:
import { format, isValid } from "date-fns";
if (isValid(d)) {
return format(d, "yyyy-MM-dd");
}
// else handle invalid inputSources: [1] date-fns docs (jsDocs) for format/validity requirements, [2] MDN on “Invalid time value” for invalid dates, [3] example showing format(new Date('invalid'), ...) throwing. (jsdocs.io)
Citations:
🌐 Web query:
JavaScript new Date() with invalid string returns Invalid Date behavior
💡 Result:
In JavaScript, new Date(badString) doesn’t throw— it creates a Date object whose internal time value is NaN. That object is commonly displayed as "Invalid Date", and most date operations on it produce NaN (or errors for methods that require a valid time value). [1][2]
What exactly happens
Date.parse(badString)returnsNaNwhen it can’t parse the string. [2]new Date(badString)effectively uses that parsing behavior for string inputs, so you get aDatewhose time value isNaN(an “Invalid Date”). [1][2]- The spec requires unrecognizable/illegal date strings to yield
NaNfromDate.parse. [3]
const d = new Date("not a date");
String(d); // "Invalid Date"
d.getTime(); // NaN
Number.isNaN(d); // false (it's an object)
Number.isNaN(d.getTime()); // true ✅ reliable checkWhy it varies across browsers
Only the standardized “date time string format” (ISO-8601–style) is reliably parsed; many other string formats are implementation-dependent, so one engine may parse them while another returns Invalid Date. [2][1]
Practical rule: only pass ISO-8601 strings (or parse yourself / use a library) if you need consistent behavior. [2][1]
Sources: MDN on Invalid Date [1], MDN on Date.parse() return value and parsing caveats [2], ECMA-262/5 Date.parse behavior (Microsoft spec summary) [3].
[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_date
[2] https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
[3] https://learn.microsoft.com/en-us/openspecs/ie_standards/ms-es5/3fbe8262-3f94-4d8d-8bb0-69d05b66a1d8
Add validity check before formatting created_at to prevent render crashes.
Line 140 formats the date without validating it. The guard validator.created_at && only checks for truthiness and does not prevent invalid date strings. If created_at is malformed, new Date() returns an Invalid Date object, which causes format() to throw a RangeError and crash the component. Use isValid() from date-fns to validate the date before formatting:
{validator.created_at && isValid(new Date(validator.created_at)) && (
<div className="flex justify-between">
<span className="text-muted-foreground">Created</span>
<span>{format(new Date(validator.created_at), 'PPpp')}</span>
</div>
)}Or store the parsed date to avoid parsing twice:
{validator.created_at && (() => {
const date = new Date(validator.created_at);
return isValid(date) ? (
<div className="flex justify-between">
<span className="text-muted-foreground">Created</span>
<span>{format(date, 'PPpp')}</span>
</div>
) : null;
})()}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@explorer/src/app/validators/ValidatorsContent.tsx` around lines 137 - 141,
The component renders formatter output for validator.created_at using format(new
Date(validator.created_at), 'PPpp') which can throw if the date is malformed;
update the render in ValidatorsContent (the block referencing
validator.created_at and format) to validate the parsed date with date-fns
isValid before calling format (or parse once into a const date and reuse it),
and only render the Created row when isValid(date) is true to prevent RangeError
crashes.
| path: string, | ||
| ): Promise<T> { | ||
| const url = `${BACKEND_URL}/api/explorer${path}`; | ||
| const res = await fetch(url, { cache: 'no-store' }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check if the file exists and read it
cat -n explorer/src/lib/fetchBackend.tsRepository: genlayerlabs/genlayer-studio
Length of output: 767
🏁 Script executed:
# Search for other fetch calls in the codebase to see if timeout patterns exist
rg "fetch\(" --type ts --type tsx -A 3 -B 1 | head -50Repository: genlayerlabs/genlayer-studio
Length of output: 99
🏁 Script executed:
# Check if there are any timeout utilities already in use in this project
rg "AbortController\|setTimeout.*fetch\|timeout" --type ts --type tsx | head -20Repository: genlayerlabs/genlayer-studio
Length of output: 99
🏁 Script executed:
# Search for timeout patterns without type filtering
rg "AbortController|setTimeout.*fetch|signal.*fetch" --max-count 20Repository: genlayerlabs/genlayer-studio
Length of output: 256
🏁 Script executed:
# Look for other server-side fetch calls that might have timeout implementations
rg "fetch\(" explorer/src --glob="*.ts" -A 2 -B 1Repository: genlayerlabs/genlayer-studio
Length of output: 328
🏁 Script executed:
# Check if there's any documentation or patterns for request timeouts in the project
rg "timeout|Timeout|TIMEOUT" explorer/src --glob="*.ts" --max-count 15Repository: genlayerlabs/genlayer-studio
Length of output: 893
🏁 Script executed:
# Check if there are other fetch calls elsewhere in the explorer app
rg "fetch\(" explorer/ --glob="*.ts" --glob="*.tsx" -B 1 -A 2 | head -30Repository: genlayerlabs/genlayer-studio
Length of output: 2230
🏁 Script executed:
# Check project configuration or environment files for timeout hints
find explorer -maxdepth 2 -type f \( -name "*.env*" -o -name "next.config.*" -o -name "package.json" \) | xargs cat 2>/dev/null | head -50Repository: genlayerlabs/genlayer-studio
Length of output: 1404
🏁 Script executed:
# Verify the Next.js version and fetch behavior expectations
cat explorer/package.json | grep -A 5 -B 5 "next"Repository: genlayerlabs/genlayer-studio
Length of output: 953
Add a timeout to backend fetch calls.
Line 12 performs a blocking upstream call without a timeout. If the backend hangs or experiences latency, server rendering can stall.
🛠️ Suggested fix
export async function fetchBackend<T = unknown>(
path: string,
): Promise<T> {
const url = `${BACKEND_URL}/api/explorer${path}`;
- const res = await fetch(url, { cache: 'no-store' });
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 10_000);
+ let res: Response;
+ try {
+ res = await fetch(url, { cache: 'no-store', signal: controller.signal });
+ } finally {
+ clearTimeout(timeoutId);
+ }
if (!res.ok) {
throw new Error(`Backend ${path}: ${res.status} ${res.statusText}`);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const res = await fetch(url, { cache: 'no-store' }); | |
| export async function fetchBackend<T = unknown>( | |
| path: string, | |
| ): Promise<T> { | |
| const url = `${BACKEND_URL}/api/explorer${path}`; | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), 10_000); | |
| let res: Response; | |
| try { | |
| res = await fetch(url, { cache: 'no-store', signal: controller.signal }); | |
| } finally { | |
| clearTimeout(timeoutId); | |
| } | |
| if (!res.ok) { | |
| throw new Error(`Backend ${path}: ${res.status} ${res.statusText}`); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@explorer/src/lib/fetchBackend.ts` at line 12, The fetch call using fetch(url,
{ cache: 'no-store' }) lacks a timeout; wrap the request with an AbortController
and a timer (e.g., 5s) and pass controller.signal into fetch so the request is
aborted on timeout, clear the timer on success, and throw or handle the abort
error so the server-side rendering won't hang; update the code paths around the
variable res and the function that performs the fetch to use this abort logic
and ensure resources are cleaned up.
| class TestGetStatsCounts: | ||
| def test_empty_database(self, session: Session): | ||
| result = queries.get_stats_counts(session) | ||
| assert result == { | ||
| "totalTransactions": 0, | ||
| "totalValidators": 0, | ||
| "totalContracts": 0, | ||
| } | ||
|
|
||
| def test_counts_with_data(self, session: Session): | ||
| contract_addr = "0xCONTRACT_001" | ||
| _make_state(session, contract_addr) | ||
| _make_tx(session, to_address=contract_addr, type=1) # deploy tx | ||
| _make_tx(session) # unrelated tx | ||
| _make_validator(session) | ||
| session.commit() | ||
|
|
||
| result = queries.get_stats_counts(session) | ||
| assert result["totalTransactions"] == 2 | ||
| assert result["totalValidators"] == 1 | ||
| assert result["totalContracts"] == 1 | ||
|
|
||
|
|
||
| class TestGetStats: | ||
| def test_empty_database(self, session: Session): | ||
| result = queries.get_stats(session) | ||
| assert result["totalTransactions"] == 0 | ||
| assert result["totalValidators"] == 0 | ||
| assert result["totalContracts"] == 0 | ||
| assert result["appealedTransactions"] == 0 | ||
| assert result["finalizedTransactions"] == 0 | ||
| assert result["avgTps24h"] >= 0 | ||
| assert result["txVolume14d"] == [] or isinstance(result["txVolume14d"], list) | ||
| assert result["recentTransactions"] == [] | ||
| assert "transactionsByStatus" in result | ||
| assert "transactionsByType" in result | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify if the test isolation issue exists by checking conftest for any
# per-test cleanup or savepoint usage
rg -n "begin_nested|SAVEPOINT|truncate" tests/db-sqlalchemy/conftest.py || echo "No savepoint/per-test cleanup found"
# Check if there's any setup/teardown that might handle this
rg -n "setup|teardown|autouse" tests/db-sqlalchemy/conftest.py tests/db-sqlalchemy/explorer_queries_test.pyRepository: genlayerlabs/genlayer-studio
Length of output: 258
🏁 Script executed:
cat -n tests/db-sqlalchemy/conftest.pyRepository: genlayerlabs/genlayer-studio
Length of output: 3460
🏁 Script executed:
sed -n '126,165p' tests/db-sqlalchemy/explorer_queries_test.pyRepository: genlayerlabs/genlayer-studio
Length of output: 1625
🏁 Script executed:
# Check if there are any autouse fixtures that might clean up between tests
rg -n "autouse|@pytest.fixture" tests/db-sqlalchemy/conftest.py tests/db-sqlalchemy/explorer_queries_test.pyRepository: genlayerlabs/genlayer-studio
Length of output: 275
Test isolation issue: committed data persists across test classes.
Tests explicitly call session.commit(), but the session fixture only rolls back uncommitted changes. Data committed in one test class persists and affects subsequent test classes. For example:
TestGetStatsCounts.test_counts_with_data(line 135) commits 2 transactions, 1 validator, and 1 contractTestGetStats.test_empty_database(line 150) expectstotalTransactions == 0but will see 2
The conftest engine fixture truncates tables only after all tests complete, not between test classes. The session fixture's rollback() call (line 77) only undoes uncommitted changes.
Modify the session fixture to use savepoints/nested transactions so session.commit() doesn't persist to the database:
`@pytest.fixture`
def session(engine: Engine) -> Iterable[Session]:
connection = engine.connect()
transaction = connection.begin()
session_maker = sessionmaker(bind=connection, expire_on_commit=False)
session = session_maker()
# Use nested transaction (savepoint) so session.commit() operates within savepoint
nested = connection.begin_nested()
`@event.listens_for`(session, "after_transaction_end")
def restart_savepoint(session, trans):
if trans.nested and not trans._parent.nested:
connection.begin_nested()
yield session
session.close()
transaction.rollback()
connection.close()Alternatively, truncate tables at the start of each test or use an autouse fixture for per-test cleanup.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/db-sqlalchemy/explorer_queries_test.py` around lines 126 - 162, The
session fixture currently lets test code call session.commit() and persist data
across test classes (e.g., TestGetStatsCounts.test_counts_with_data affects
TestGetStats.test_empty_database); change the fixture that creates the
SQLAlchemy Session to open a top-level connection.transaction and then start a
nested transaction/savepoint (use sessionmaker(bind=connection,
expire_on_commit=False), call connection.begin() and connection.begin_nested()),
register an event listener on the Session for "after_transaction_end" (the
restart_savepoint pattern used in SQLAlchemy testing) to recreate the nested
savepoint after commits, yield the session to tests, and finally close the
session and rollback the outer transaction before closing the connection so
commits inside tests do not persist. Ensure you reference the session fixture,
the connection/transaction/nested savepoint, and the after_transaction_end
listener when making the change.
- Replace icon+text logo with GenLayer mark SVG - Add Switzer font via Fontshare CDN - Rename title to "GenLayer Studio Explorer" - Replace favicon.ico with GenLayer mark PNG (icon.png) Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Rename middleware.ts to proxy.ts and update export name from `middleware` to `proxy` per Next.js 16 deprecation warning. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Fix hydration mismatch in GlobalSearch by moving navigator.userAgent check to useEffect with isMac state - Guard avgTps24h and txVolume14d with nullish coalescing to prevent TypeError when API fields are missing Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
explorer/src/app/DashboardSections.tsx (2)
19-32: Interface declares required fields that may be undefined at runtime.The
avgTps24hfield is declared as required (number), yet line 69 guards it withstats.avgTps24h ?? 0. Similarly,txVolume14dis declared required but the commit messages mention guarding it. Consider making these fields optional in the interface to match runtime behavior:interface StatsData { // ... - avgTps24h: number; - txVolume14d: { date: string; count: number }[]; + avgTps24h?: number; + txVolume14d?: { date: string; count: number }[]; // ... }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/app/DashboardSections.tsx` around lines 19 - 32, The StatsData interface declares avgTps24h and txVolume14d as required but runtime guards use `stats.avgTps24h ?? 0` and similar checks; update the interface `StatsData` to make `avgTps24h` and `txVolume14d` optional (e.g., `avgTps24h?: number` and `txVolume14d?: { date: string; count: number }[]`) so the type matches runtime behavior, then verify callers of `getStats`/`StatsData` handle undefined (keep existing nullish-coalescing guards where used) to satisfy the compiler and reflect possible missing data.
38-39: Consider error handling for failed backend fetches.The
fetchBackendutility throws anErroron non-ok responses. Whilepage.tsxwraps these sections in Suspense for loading states, there is no error boundary to catch thrown exceptions. If/statsor/transactionsendpoints fail (network error, 5xx, etc.), the error will propagate unhandled and crash the page.Consider adding an
error.tsxfile in the app directory to provide a graceful fallback UI, or wrap critical sections with try/catch and return fallback content on failure.Also applies to: 89-90, 202-203
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/app/DashboardSections.tsx` around lines 38 - 39, StatCardsSection (which calls getStats -> fetchBackend) and other server components like TransactionsTableSection should handle backend fetch failures to avoid unhandled thrown Errors; either add a local try/catch around the await getStats()/await getTransactions() calls and return a lightweight fallback UI when an exception occurs, or add an app-level error boundary by creating app/error.tsx to render a graceful fallback for Suspense-thrown errors; update StatCardsSection, TransactionsTableSection, and any similar components (references: StatCardsSection, getStats, TransactionsTableSection, getTransactions, fetchBackend) to use one of these approaches so network/5xx failures don’t crash the page.explorer/src/components/GlobalSearch.tsx (1)
133-133: Placeholder text doesn't mention validators.The search now includes validators, but the placeholder only mentions "transactions and contracts". Consider updating for consistency.
✏️ Suggested fix
- placeholder="Search transactions and contracts..." + placeholder="Search transactions, contracts, and validators..."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/components/GlobalSearch.tsx` at line 133, Update the placeholder in the GlobalSearch component to reflect that validators are searchable: locate the placeholder prop in GlobalSearch.tsx (the string "Search transactions and contracts...") and change it to include validators (e.g. "Search transactions, contracts, and validators..." or a similar phrasing) so the UI matches the actual search scope.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@explorer/src/app/DashboardSections.tsx`:
- Around line 164-189: The block reading stats.txVolume14d should guard against
undefined to avoid a TypeError; create a local variable (e.g., volumeData =
stats.txVolume14d ?? []) and use volumeData for the length check, reduce, map,
and index accesses (instead of stats.txVolume14d) so the Card and SparklineChart
(and the date spans) safely handle missing backend data.
---
Nitpick comments:
In `@explorer/src/app/DashboardSections.tsx`:
- Around line 19-32: The StatsData interface declares avgTps24h and txVolume14d
as required but runtime guards use `stats.avgTps24h ?? 0` and similar checks;
update the interface `StatsData` to make `avgTps24h` and `txVolume14d` optional
(e.g., `avgTps24h?: number` and `txVolume14d?: { date: string; count: number
}[]`) so the type matches runtime behavior, then verify callers of
`getStats`/`StatsData` handle undefined (keep existing nullish-coalescing guards
where used) to satisfy the compiler and reflect possible missing data.
- Around line 38-39: StatCardsSection (which calls getStats -> fetchBackend) and
other server components like TransactionsTableSection should handle backend
fetch failures to avoid unhandled thrown Errors; either add a local try/catch
around the await getStats()/await getTransactions() calls and return a
lightweight fallback UI when an exception occurs, or add an app-level error
boundary by creating app/error.tsx to render a graceful fallback for
Suspense-thrown errors; update StatCardsSection, TransactionsTableSection, and
any similar components (references: StatCardsSection, getStats,
TransactionsTableSection, getTransactions, fetchBackend) to use one of these
approaches so network/5xx failures don’t crash the page.
In `@explorer/src/components/GlobalSearch.tsx`:
- Line 133: Update the placeholder in the GlobalSearch component to reflect that
validators are searchable: locate the placeholder prop in GlobalSearch.tsx (the
string "Search transactions and contracts...") and change it to include
validators (e.g. "Search transactions, contracts, and validators..." or a
similar phrasing) so the UI matches the actual search scope.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1b545fa5-2d80-4c64-b1fc-c015ba7d9f0c
⛔ Files ignored due to path filters (3)
explorer/public/genlayer-logo.svgis excluded by!**/*.svgexplorer/src/app/favicon.icois excluded by!**/*.icoexplorer/src/app/icon.pngis excluded by!**/*.png
📒 Files selected for processing (6)
explorer/src/app/DashboardSections.tsxexplorer/src/app/globals.cssexplorer/src/app/layout.tsxexplorer/src/components/GlobalSearch.tsxexplorer/src/components/Navigation.tsxexplorer/src/proxy.ts
✅ Files skipped from review due to trivial changes (1)
- explorer/src/app/globals.css
🚧 Files skipped from review as they are similar to previous changes (1)
- explorer/src/app/layout.tsx
…ions Decode calldata from call/deploy transactions to show method names and parameters in an Etherscan-style panel with Decoded/Hex/Raw toggle. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Show decoded method name in both main and address transaction tables. Reorder columns, hide Accepted/Finalized by default, fix column picker scroll on macOS. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add ContractInteraction component with Etherscan-style Code, Read Contract, and Write Contract sub-tabs. Uses genlayer-js SDK client for schema fetch and read method execution. Proxy bypasses /api/rpc for Next.js API routes. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (2)
explorer/src/app/address/[addr]/AddressContent.tsx (2)
164-167:⚠️ Potential issue | 🟡 MinorSame tab count drift issue in ContractView.
Apply the same fix here for consistency with the stat card.
🩹 Proposed fix
- Transactions ({transactions.length}) + Transactions ({data.tx_count ?? transactions.length})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/app/address/`[addr]/AddressContent.tsx around lines 164 - 167, TabsTrigger in AddressContent currently shows Transactions ({transactions.length}) which can drift from the stat card; change the count to use the same canonical source used by the stat card (e.g. transactionsCount or transactionsTotal) so both UI spots read from the same derived/state value rather than raw transactions.length; locate TabsTrigger with value="transactions" inside AddressContent and replace the inline transactions.length with the shared count variable (fall back to transactions.length only if that shared variable is undefined).
89-92:⚠️ Potential issue | 🟡 MinorTab badge count may drift from actual transaction count.
The tab label uses
txs.lengthwhich reflects the loaded array size. If the API caps the returned transactions, this will differ from the actual count shown in the stat card (which usesdata.tx_count ?? txs.length). Consider using the same logic for consistency.🩹 Proposed fix
- Transactions ({txs.length}) + Transactions ({data.tx_count ?? txs.length})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/app/address/`[addr]/AddressContent.tsx around lines 89 - 92, The Transactions tab badge currently uses txs.length which can differ from the real total when the API caps results; update the TabsTrigger label to use the same count logic as the stat card—use (data?.tx_count ?? txs.length) instead of txs.length—so the badge reflects the authoritative count; ensure data is in scope (data variable used elsewhere in this component) and fall back to txs.length when data?.tx_count is undefined.
🧹 Nitpick comments (4)
explorer/src/components/DataDecodePanel.tsx (1)
38-38: Intentional use of control character matching — static analysis warning is a false positive.The regex
/[\x00-\x08\x0E-\x1F]/deliberately matches non-printable control characters to filter out binary data from text decoding. This is the correct approach for detecting non-textual content. The Biome lint warning can be safely suppressed here since the intent is explicitly to match these characters.🔇 Suppress the lint warning with an inline directive
- if (/[\x00-\x08\x0E-\x1F]/.test(text)) return null; + // biome-ignore lint/suspicious/noControlCharactersInRegex: Intentionally matching control chars to detect binary data + if (/[\x00-\x08\x0E-\x1F]/.test(text)) return null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/components/DataDecodePanel.tsx` at line 38, In DataDecodePanel.tsx the regex /[\x00-\x08\x0E-\x1F]/ used to test the variable text intentionally matches control characters and should not be flagged; suppress the Biome lint false positive by adding an inline disable comment directive for that specific lint rule on the line containing the regex test (reference the if statement that tests text with /[\x00-\x08\x0E-\x1F]/), keeping the directive narrowly scoped to this line only.explorer/src/components/ContractInteraction.tsx (1)
38-45: Potential state update on unmounted component.If the component unmounts while
fetchContractSchemais in-flight, the.then(setSchema)and.finally()callbacks will still execute, potentially causing a React warning about state updates on unmounted components.🛡️ Suggested fix using an abort flag
useEffect(() => { + let cancelled = false; setSchemaLoading(true); setSchemaError(null); fetchContractSchema(address) - .then(setSchema) - .catch((e) => setSchemaError(e.message)) - .finally(() => setSchemaLoading(false)); + .then((schema) => { if (!cancelled) setSchema(schema); }) + .catch((e) => { if (!cancelled) setSchemaError(e.message); }) + .finally(() => { if (!cancelled) setSchemaLoading(false); }); + return () => { cancelled = true; }; }, [address]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/components/ContractInteraction.tsx` around lines 38 - 45, The effect in ContractInteraction.tsx calls fetchContractSchema and updates state (setSchema, setSchemaError, setSchemaLoading) after the promise resolves, which can run after the component unmounts; fix by adding an "isMounted" flag (or AbortController if fetchContractSchema supports a signal): inside the useEffect create let isMounted = true; before calling setSchema / setSchemaError / setSchemaLoading wrap each call with if (isMounted) { ... } (or pass signal to fetchContractSchema and handle abort), and return a cleanup function that sets isMounted = false (or aborts the controller) so no state updates occur on an unmounted component.explorer/src/components/AddressTransactionTable.tsx (1)
50-54: Method name derivation logic may have an edge case.The
Transaction.datafield is typed asRecord<string, unknown> | null, so the optional chaining and type guard are appropriate. However, the constructor fallback logic on line 54 is slightly confusing:const methodName = decodedInput?.methodName ?? (decodedInput && !decodedInput.methodName ? '(constructor)' : undefined);If
decodedInputexists butmethodNameis falsy (null, undefined, or empty string), it shows'(constructor)'. This seems intentional for deployment transactions, but the conditiondecodedInput && !decodedInput.methodNameafter the nullish coalescing is redundant since we only reach it whendecodedInput?.methodNameis nullish.♻️ Simplified equivalent
- const methodName = decodedInput?.methodName ?? (decodedInput && !decodedInput.methodName ? '(constructor)' : undefined); + const methodName = decodedInput?.methodName || (decodedInput ? '(constructor)' : undefined);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/components/AddressTransactionTable.tsx` around lines 50 - 54, The current method name derivation is redundant and hard to read; replace the expression that sets methodName with an explicit conditional that checks decodedInput first and then uses the nullish fallback for methodName so the intent is clear—e.g., if decodedInput exists return decodedInput.methodName if defined otherwise "(constructor)", else return undefined. Update the assignment that references decodedInput, methodName, decodeCalldata, and calldataB64 accordingly to use this clearer conditional form.explorer/src/lib/contractSchema.ts (1)
28-32: Type casting masks SDK schema mismatch that should be resolved for maintainability.The local
ContractSchemainterface definesctoras{ params: ContractParam[]; kwparams: Record<string, string> }, but the SDK'sgetContractSchemareturns aContractSchemawherectoris aContractMethodBasetype. The double castas unknown as ContractSchemaworks around this structural mismatch but hides the incompatibility.Either align the local interface with the SDK's actual schema structure or add a runtime validation layer and documentation explaining why this cast is necessary. This will prevent silent failures if the SDK evolves.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@explorer/src/lib/contractSchema.ts` around lines 28 - 32, The current double-cast in fetchContractSchema hides a structural mismatch between our local ContractSchema (whose ctor is { params: ContractParam[]; kwparams: Record<string,string> }) and the SDK's getContractSchema return where ctor is a ContractMethodBase; fix this by either updating the local ContractSchema.ctor type to match the SDK's ContractMethodBase (or import that type directly) or add a small runtime validation/mapper inside fetchContractSchema that converts the SDK's ContractMethodBase shape into our expected { params, kwparams } shape (and remove the unsafe "as unknown as ContractSchema" cast), and document the choice near the fetchContractSchema and ContractSchema declarations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@explorer/src/app/api/rpc/route.ts`:
- Around line 6-13: The handler currently always calls res.json() and returns
NextResponse.json(data), which fails for 204/empty upstream responses and hides
the upstream HTTP status; update the logic after fetch(BACKEND_URL) to: check
res.status (and optionally res.headers.get('content-length') or missing
content-type) and if the response is empty (e.g. status === 204) return a
NextResponse with no body and the upstream status; otherwise parse res.json()
into data and return NextResponse.json(data, { status: res.status }); also
propagate relevant upstream headers (e.g. content-type) from the fetch response
when constructing the NextResponse to preserve backend metadata. Ensure you
update the code around request.json(), fetch(...), res.json(), and
NextResponse.json(...) accordingly.
In `@explorer/src/app/transactions/`[hash]/components/DataTab.tsx:
- Around line 14-16: The code reads tx.data.calldata without ensuring it's a
string, so non-string truthy values can incorrectly trigger InputDataPanel;
update the calldataB64 computation to explicitly check that tx.data exists and
that (tx.data as Record<string, unknown>).calldata is of type string (e.g.
typeof calldata === 'string' && calldata.length > 0) before casting and passing
to InputDataPanel; apply the same strict type-guard in the other similar block
(lines noted in the review) so non-string calldata falls back to the generic
data view.
In `@explorer/src/components/InputDataPanel.tsx`:
- Around line 22-31: serializeArg currently skips plain objects so nested
bigints inside object literals aren't converted and JSON.stringify can still
fail; update serializeArg (and the duplicate implementation elsewhere) to detect
plain objects (typeof val === 'object' && val !== null && !Array.isArray(val) &&
!(val instanceof Map)) and recursively walk its own enumerable keys, calling
serializeArg on each value and returning a new object with stringified bigint
values; preserve existing handling for Array and Map and ensure null is returned
unchanged.
- Around line 41-49: The handleCopy function currently calls
navigator.clipboard.writeText without awaiting or handling errors which can show
the "Copied" state on failure; make handleCopy async, await
navigator.clipboard.writeText(text) inside a try/catch, only call
setCopied(true) when the await succeeds, handle failures in the catch (e.g.,
optionally log or show an error), and still clear the copied state with
setTimeout (or move the timeout into a finally block) so setCopied(false) always
runs after the delay; reference handleCopy, navigator.clipboard.writeText, and
setCopied to locate and update the logic.
In `@explorer/src/components/TransactionTable.tsx`:
- Around line 193-197: The code unsafely casts tx.data.calldata to string and
may pass non-string values into decodeCalldata and render; change the guard so
calldataB64 is only set when calldata is actually a string: extract calldata via
(tx.data as Record<string, unknown>).calldata into a local unknown, check typeof
calldata === 'string' before assigning calldataB64 and before calling
decodeCalldata, and then derive decodedInput and methodName from that safely
(use decodeCalldata only when calldataB64 is a string and ensure methodName
handling uses decodedInput !== null/undefined).
---
Duplicate comments:
In `@explorer/src/app/address/`[addr]/AddressContent.tsx:
- Around line 164-167: TabsTrigger in AddressContent currently shows
Transactions ({transactions.length}) which can drift from the stat card; change
the count to use the same canonical source used by the stat card (e.g.
transactionsCount or transactionsTotal) so both UI spots read from the same
derived/state value rather than raw transactions.length; locate TabsTrigger with
value="transactions" inside AddressContent and replace the inline
transactions.length with the shared count variable (fall back to
transactions.length only if that shared variable is undefined).
- Around line 89-92: The Transactions tab badge currently uses txs.length which
can differ from the real total when the API caps results; update the TabsTrigger
label to use the same count logic as the stat card—use (data?.tx_count ??
txs.length) instead of txs.length—so the badge reflects the authoritative count;
ensure data is in scope (data variable used elsewhere in this component) and
fall back to txs.length when data?.tx_count is undefined.
---
Nitpick comments:
In `@explorer/src/components/AddressTransactionTable.tsx`:
- Around line 50-54: The current method name derivation is redundant and hard to
read; replace the expression that sets methodName with an explicit conditional
that checks decodedInput first and then uses the nullish fallback for methodName
so the intent is clear—e.g., if decodedInput exists return
decodedInput.methodName if defined otherwise "(constructor)", else return
undefined. Update the assignment that references decodedInput, methodName,
decodeCalldata, and calldataB64 accordingly to use this clearer conditional
form.
In `@explorer/src/components/ContractInteraction.tsx`:
- Around line 38-45: The effect in ContractInteraction.tsx calls
fetchContractSchema and updates state (setSchema, setSchemaError,
setSchemaLoading) after the promise resolves, which can run after the component
unmounts; fix by adding an "isMounted" flag (or AbortController if
fetchContractSchema supports a signal): inside the useEffect create let
isMounted = true; before calling setSchema / setSchemaError / setSchemaLoading
wrap each call with if (isMounted) { ... } (or pass signal to
fetchContractSchema and handle abort), and return a cleanup function that sets
isMounted = false (or aborts the controller) so no state updates occur on an
unmounted component.
In `@explorer/src/components/DataDecodePanel.tsx`:
- Line 38: In DataDecodePanel.tsx the regex /[\x00-\x08\x0E-\x1F]/ used to test
the variable text intentionally matches control characters and should not be
flagged; suppress the Biome lint false positive by adding an inline disable
comment directive for that specific lint rule on the line containing the regex
test (reference the if statement that tests text with /[\x00-\x08\x0E-\x1F]/),
keeping the directive narrowly scoped to this line only.
In `@explorer/src/lib/contractSchema.ts`:
- Around line 28-32: The current double-cast in fetchContractSchema hides a
structural mismatch between our local ContractSchema (whose ctor is { params:
ContractParam[]; kwparams: Record<string,string> }) and the SDK's
getContractSchema return where ctor is a ContractMethodBase; fix this by either
updating the local ContractSchema.ctor type to match the SDK's
ContractMethodBase (or import that type directly) or add a small runtime
validation/mapper inside fetchContractSchema that converts the SDK's
ContractMethodBase shape into our expected { params, kwparams } shape (and
remove the unsafe "as unknown as ContractSchema" cast), and document the choice
near the fetchContractSchema and ContractSchema declarations.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7dbfe018-4787-40a7-9ae2-4a6470d3a32a
📒 Files selected for processing (14)
explorer/src/app/address/[addr]/AddressContent.tsxexplorer/src/app/api/rpc/route.tsexplorer/src/app/transactions/[hash]/components/DataTab.tsxexplorer/src/app/transactions/[hash]/components/OverviewTab.tsxexplorer/src/components/AddressTransactionTable.tsxexplorer/src/components/ContractInteraction.tsxexplorer/src/components/DataDecodePanel.tsxexplorer/src/components/InputDataPanel.tsxexplorer/src/components/TransactionTable.tsxexplorer/src/lib/contractSchema.tsexplorer/src/lib/genlayerClient.tsexplorer/src/lib/jsonrpc.tsexplorer/src/lib/resultDecoder.tsexplorer/src/proxy.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- explorer/src/app/transactions/[hash]/components/OverviewTab.tsx
| const body = await request.json(); | ||
| const res = await fetch(`${BACKEND_URL}/api`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| const data = await res.json(); | ||
| return NextResponse.json(data); |
There was a problem hiding this comment.
Handle empty upstream bodies and preserve upstream status codes.
res.json() will fail for valid 204 No Content responses, and always returning NextResponse.json(data) masks backend status codes.
Suggested fix
export async function POST(request: NextRequest) {
- const body = await request.json();
+ const body = await request.text();
const res = await fetch(`${BACKEND_URL}/api`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body),
+ body,
});
- const data = await res.json();
- return NextResponse.json(data);
+ if (res.status === 204) {
+ return new NextResponse(null, { status: 204 });
+ }
+ const text = await res.text();
+ if (!text) return new NextResponse(null, { status: res.status });
+ return NextResponse.json(JSON.parse(text), { status: res.status });
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@explorer/src/app/api/rpc/route.ts` around lines 6 - 13, The handler currently
always calls res.json() and returns NextResponse.json(data), which fails for
204/empty upstream responses and hides the upstream HTTP status; update the
logic after fetch(BACKEND_URL) to: check res.status (and optionally
res.headers.get('content-length') or missing content-type) and if the response
is empty (e.g. status === 204) return a NextResponse with no body and the
upstream status; otherwise parse res.json() into data and return
NextResponse.json(data, { status: res.status }); also propagate relevant
upstream headers (e.g. content-type) from the fetch response when constructing
the NextResponse to preserve backend metadata. Ensure you update the code around
request.json(), fetch(...), res.json(), and NextResponse.json(...) accordingly.
| const calldataB64 = (tx.type === 1 || tx.type === 2) && tx.data && typeof tx.data === 'object' | ||
| ? (tx.data as Record<string, unknown>).calldata as string | undefined | ||
| : undefined; |
There was a problem hiding this comment.
Type-guard tx.data.calldata before using it as base64.
Right now a non-string truthy calldata can render InputDataPanel and suppress the generic data view.
Suggested fix
- const calldataB64 = (tx.type === 1 || tx.type === 2) && tx.data && typeof tx.data === 'object'
- ? (tx.data as Record<string, unknown>).calldata as string | undefined
- : undefined;
+ const rawCalldata =
+ (tx.type === 1 || tx.type === 2) && tx.data && typeof tx.data === 'object'
+ ? (tx.data as Record<string, unknown>).calldata
+ : undefined;
+ const calldataB64 = typeof rawCalldata === 'string' ? rawCalldata : undefined;Also applies to: 33-45
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@explorer/src/app/transactions/`[hash]/components/DataTab.tsx around lines 14
- 16, The code reads tx.data.calldata without ensuring it's a string, so
non-string truthy values can incorrectly trigger InputDataPanel; update the
calldataB64 computation to explicitly check that tx.data exists and that
(tx.data as Record<string, unknown>).calldata is of type string (e.g. typeof
calldata === 'string' && calldata.length > 0) before casting and passing to
InputDataPanel; apply the same strict type-guard in the other similar block
(lines noted in the review) so non-string calldata falls back to the generic
data view.
| function serializeArg(val: unknown): unknown { | ||
| if (typeof val === 'bigint') return val.toString(); | ||
| if (Array.isArray(val)) return val.map(serializeArg); | ||
| if (val instanceof Map) { | ||
| const obj: Record<string, unknown> = {}; | ||
| val.forEach((v, k) => { obj[String(k)] = serializeArg(v); }); | ||
| return obj; | ||
| } | ||
| return val; | ||
| } |
There was a problem hiding this comment.
serializeArg misses plain objects, which can break copy/export on BigInt-containing structs.
When decoded args include objects with nested bigint, JSON.stringify still throws because plain objects are not recursively normalized.
Suggested fix
function serializeArg(val: unknown): unknown {
if (typeof val === 'bigint') return val.toString();
if (Array.isArray(val)) return val.map(serializeArg);
if (val instanceof Map) {
const obj: Record<string, unknown> = {};
val.forEach((v, k) => { obj[String(k)] = serializeArg(v); });
return obj;
}
+ if (val && typeof val === 'object') {
+ const obj: Record<string, unknown> = {};
+ for (const [k, v] of Object.entries(val as Record<string, unknown>)) {
+ obj[k] = serializeArg(v);
+ }
+ return obj;
+ }
return val;
}Also applies to: 45-55
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@explorer/src/components/InputDataPanel.tsx` around lines 22 - 31,
serializeArg currently skips plain objects so nested bigints inside object
literals aren't converted and JSON.stringify can still fail; update serializeArg
(and the duplicate implementation elsewhere) to detect plain objects (typeof val
=== 'object' && val !== null && !Array.isArray(val) && !(val instanceof Map))
and recursively walk its own enumerable keys, calling serializeArg on each value
and returning a new object with stringified bigint values; preserve existing
handling for Array and Map and ensure null is returned unchanged.
| const handleCopy = () => { | ||
| let text = ''; | ||
| if (mode === 'hex') text = hex; | ||
| else if (mode === 'raw') text = calldataB64; | ||
| else if (decoded) text = JSON.stringify({ method: decoded.methodName, args: decoded.args.map(serializeArg) }, null, 2); | ||
| navigator.clipboard.writeText(text); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 1500); | ||
| }; |
There was a problem hiding this comment.
Handle clipboard failures before showing “Copied”.
navigator.clipboard.writeText is async; without await/catch, failures can still show success state.
Suggested fix
- const handleCopy = () => {
+ const handleCopy = async () => {
let text = '';
if (mode === 'hex') text = hex;
else if (mode === 'raw') text = calldataB64;
else if (decoded) text = JSON.stringify({ method: decoded.methodName, args: decoded.args.map(serializeArg) }, null, 2);
- navigator.clipboard.writeText(text);
- setCopied(true);
- setTimeout(() => setCopied(false), 1500);
+ if (!text) return;
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ } catch {
+ setCopied(false);
+ }
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleCopy = () => { | |
| let text = ''; | |
| if (mode === 'hex') text = hex; | |
| else if (mode === 'raw') text = calldataB64; | |
| else if (decoded) text = JSON.stringify({ method: decoded.methodName, args: decoded.args.map(serializeArg) }, null, 2); | |
| navigator.clipboard.writeText(text); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 1500); | |
| }; | |
| const handleCopy = async () => { | |
| let text = ''; | |
| if (mode === 'hex') text = hex; | |
| else if (mode === 'raw') text = calldataB64; | |
| else if (decoded) text = JSON.stringify({ method: decoded.methodName, args: decoded.args.map(serializeArg) }, null, 2); | |
| if (!text) return; | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 1500); | |
| } catch { | |
| setCopied(false); | |
| } | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@explorer/src/components/InputDataPanel.tsx` around lines 41 - 49, The
handleCopy function currently calls navigator.clipboard.writeText without
awaiting or handling errors which can show the "Copied" state on failure; make
handleCopy async, await navigator.clipboard.writeText(text) inside a try/catch,
only call setCopied(true) when the await succeeds, handle failures in the catch
(e.g., optionally log or show an error), and still clear the copied state with
setTimeout (or move the timeout into a finally block) so setCopied(false) always
runs after the delay; reference handleCopy, navigator.clipboard.writeText, and
setCopied to locate and update the logic.
- Guard txVolume14d against undefined in dashboard sparkline - Use tx_count instead of array length for transaction tab counts - Guard nullable timestamps in ProvidersContent - Fix polling that stops permanently after a network error Co-Authored-By: Claude Opus 4.6 <[email protected]>
- useColumnVisibility: column state + localStorage from TransactionTable - usePagination: shared pagination logic from TransactionsPage/ContractsPage - useTransactionPolling: polling with visibility-pause from tx detail page Co-Authored-By: Claude Opus 4.6 <[email protected]>
…lication Replace inline truncateAddress + Link + CopyButton pattern used across 9+ instances with a single reusable AddressDisplay component. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Split 475-line MonitoringTab into 5 single-responsibility components: MonitoringStats, NewFormatTimeline, LegacyFormatTimeline, TimingInformation, AppealProcessing. MonitoringTab is now a thin wrapper. Co-Authored-By: Claude Opus 4.6 <[email protected]>
- MethodForm: extracted from ContractInteraction (120 lines) - StatItem: extracted from AddressContent for reuse across pages Co-Authored-By: Claude Opus 4.6 <[email protected]>
This reverts commit 38a7103.
Summary
/address/[addr]) — resolves contracts, validators, and accounts with type-specific detail views/address/routesPATCH/DELETEtransaction endpoints, renamed/api/explorer/state→/api/explorer/contracts, removed redundant/contracts/{id}detail endpointTest plan
tests/db-sqlalchemy/explorer_queries_test.py)/address/{addr}resolves contracts, validators, and accounts/contractspage fetches from/api/contracts/address/🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Removals