Skip to content

feat(explorer): add solvers page and integrate solvers info fetching#7037

Merged
fairlighteth merged 28 commits intodevelopfrom
feat/solvers-explorer
Feb 27, 2026
Merged

feat(explorer): add solvers page and integrate solvers info fetching#7037
fairlighteth merged 28 commits intodevelopfrom
feat/solvers-explorer

Conversation

@fairlighteth
Copy link
Copy Markdown
Contributor

@fairlighteth fairlighteth commented Feb 18, 2026

Summary

Screenshot 2026-02-18 at 15 32 36

Adds a new public Solvers page in Explorer and wires it into navigation/routes, with CMS-backed solver data and a polished UX for discovery + inspection.

This change includes:

  • New /solvers route and menu entry under More
  • Replaced legacy/temporary solver sourcing with CMS-driven fetching and mapping
  • New “Live activity snapshot” section with Dune embed in an accordion-style container
  • “Solvers directory” table with:
    • Inline search + network/environment filters
    • Dynamic shown-count in the section title
    • Larger solver icons
    • Network chips with chain icons
    • Environment tags (prod/barn) with visual color-coding
    • Expandable per-solver deployment details (including solver + payout addresses)

Screenshots:

  • Updated page header, accordion snapshot, and directory table
  • Expanded solver row with payout details
  • Filter/search interactions

To Test

  1. Open the Solvers page in Explorer
  • Go to Explorer and open More -> Solvers
  • Verify URL resolves to /solvers (with network prefix behavior unchanged from other routes)
  • Verify page renders title, description, snapshot section, and directory section
  1. Verify snapshot accordion behavior
  • Confirm “Live activity snapshot” is expanded by default
  • Click “Hide chart” and verify only header remains visible
  • Click “Show chart” and verify iframe chart returns
  • Verify header/toggle alignment and section spacing look correct
  1. Verify directory title/count + filters
  • Confirm title reads Solvers directory (N shown) with (N shown) in grey
  • Type into search and verify result count updates
  • Apply network filter and verify rows + count update
  • Apply environment filter and verify rows + count update
  • Combine search + filters and verify count remains accurate
  1. Verify table visual/interaction details
  • Confirm solver icons appear larger than previous implementation
  • Confirm networks display as chips with network icons
  • Confirm environment values render as tags (prod green, barn orange)
  • Expand a solver row and verify detail panel title and columns:
  • Solver address and Payout address labels are explicit
  • Address values are visible/readable and correspond to filtered context
  1. Verify links and text contrast
  • Confirm website links render and open correctly
  • Confirm directory search placeholder has sufficient contrast
  • Confirm snapshot section title text color is white and readable

Background

Explorer previously had a dormant temporary solver source path and no user-facing solvers directory page. This PR consolidates solver presentation around CMS metadata, keeps a live activity view via
Dune embed, and improves discoverability by merging summary + deployment details into a single expandable directory workflow.

Summary by CodeRabbit

  • New Features

    • Added "Solvers" to the main menu and a dedicated Solvers page with live activity snapshot, embedded analytics, and a searchable, filterable directory with expandable deployment details.
  • Improvements

    • Fetching now exposes loading and error states for clearer UI feedback.
    • CMS-backed data fetching and stronger typing improve reliability and deterministic ordering.
  • Tests

    • Added unit tests covering data fetching and the Solvers data hook.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 18, 2026

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

Project Deployment Actions Updated (UTC)
cowfi Ready Ready Preview Feb 27, 2026 3:30pm
explorer-dev Ready Ready Preview Feb 27, 2026 3:30pm
swap-dev Ready Ready Preview Feb 27, 2026 3:30pm
widget-configurator Ready Ready Preview Feb 27, 2026 3:30pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
cosmos Ignored Ignored Feb 27, 2026 3:30pm
sdk-tools Ignored Ignored Preview Feb 27, 2026 3:30pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 18, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new Solvers feature: menu entry and route, lazily-loaded Solvers page, directory UI (table, rows, filters, styles), CMS-backed fetcher with types and caching, hook refactor returning loading/error, tests, and a small package dependency.

Changes

Cohort / File(s) Summary
Navigation & Routing
apps/explorer/src/components/common/MenuDropdown/mainMenu.ts, apps/explorer/src/explorer/const.ts, apps/explorer/src/explorer/ExplorerApp.tsx
Added Solvers menu item and Routes.SOLVERS; registered a lazily-loaded Solvers route and adjusted ExplorerApp export wiring.
Page & Page Styles
apps/explorer/src/explorer/pages/Solvers.tsx, apps/explorer/src/explorer/pages/Solvers.styles.tsx
New Solvers page component (snapshot iframe, header, directory) and page-specific styled components (accordion, chart wrapper, placeholders, etc.).
Directory: components, body, rows & filters
apps/explorer/src/explorer/pages/SolversDirectoryTable.tsx, apps/explorer/src/explorer/pages/SolversDirectoryTableBody.tsx, apps/explorer/src/explorer/pages/SolversDirectoryTableRows.tsx, apps/explorer/src/explorer/pages/SolversDirectoryTableFilters.tsx
New directory component set: table container, body renderer, summary/details rows, and filter bar with search/network/environment controls; expansion state and filtered-count callback implemented.
Directory: styles & helpers
apps/explorer/src/explorer/pages/SolversDirectoryTable.styles.tsx, apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx
Added styling primitives for table, controls, chips/tags, and deployments grid; helpers for extracting options, filtering solvers/deployments, and ALL_FILTER.
Data fetching, types & hook
apps/explorer/src/utils/fetchSolversInfo.ts, apps/explorer/src/utils/fetchSolversInfo.types.ts, apps/explorer/src/hooks/useSolversInfo.ts
Reworked fetch to map CMS responses to typed Solver structures, added caching and optional network filtering; introduced CMS-related TS types; refactored hook to return { solversInfo, isLoading, error }.
Tests
apps/explorer/src/test/hooks/useSolversInfo.test.tsx, apps/explorer/src/test/utils/fetchSolversInfo.test.ts
Added tests for hook lifecycle, refetch on network change, fetch mapping, filtering, normalization, and caching.
Small integration & package
apps/explorer/src/components/common/MenuDropdown/mainMenu.ts, apps/explorer/package.json
Imported solver icon asset for menu; added @cowprotocol/core workspace dependency.

Sequence Diagram

sequenceDiagram
    participant User
    participant Browser as SolversPage (Browser)
    participant Hook as useSolversInfo
    participant Utils as fetchSolversInfo
    participant CMS as Backend/CMS API

    User->>Browser: navigate to /solvers
    activate Browser
    Browser->>Hook: call useSolversInfo(network?)
    activate Hook
    Hook->>Hook: set isLoading=true, error=null
    Hook->>Utils: fetchSolversInfo(network?)
    activate Utils
    Utils->>CMS: GET /solvers
    activate CMS
    CMS-->>Utils: return solver data
    deactivate CMS
    Utils-->>Hook: return SolversInfo (cached/filtered)
    deactivate Utils
    Hook->>Hook: set solversInfo, isLoading=false
    Hook-->>Browser: return { solversInfo, isLoading, error }
    deactivate Hook
    Browser->>Browser: render snapshot iframe + directory table
    User->>Browser: apply search/filters/expand rows
    Browser->>Browser: local filtering + UI update
    deactivate Browser
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Bridge

Suggested reviewers

  • elena-zh
  • shoom3301
  • cowdan

Poem

🐇 I bumped a route and found a door,
Solvers hop in, data to explore.
Rows unfold and charts parade,
CMS whispers, caches laid,
Hooray — the explorer wants some more!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(explorer): add solvers page and integrate solvers info fetching' is clear, specific, and directly reflects the main changes—adding a new Solvers page and integrating CMS-backed solver data fetching.
Description check ✅ Passed The description includes a summary, detailed testing checklist with verifiable steps, and background context. It follows the repository template structure with screenshots and covers all major features and changes.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/solvers-explorer

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (11)
apps/explorer/src/utils/fetchSolversInfo.ts (1)

111-114: Prefer explicit network === undefined over falsy !network

!network is true for 0, NaN, and null in addition to undefined. Although chain ID 0 is not a real value today, the intent is clearer and safer with an explicit check.

✏️ Proposed fix
-  if (!network) {
+  if (network === undefined) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/explorer/src/utils/fetchSolversInfo.ts` around lines 111 - 114, The
guard in filterSolversByNetwork currently uses a falsy check (!network) which
treats 0, NaN, and null as "no network"; update the check to explicitly test for
undefined (network === undefined) so only an omitted network returns allSolvers.
Locate function filterSolversByNetwork and replace the early-return condition to
use network === undefined, leaving the rest of the logic unchanged.
apps/explorer/src/explorer/pages/SolversDirectoryTable.tsx (1)

40-43: buildBodyRows is a JSX-returning render factory — extract it as a component

Wrapping a JSX-producing helper in useMemo doesn't make it a component; React cannot track component identity, reconcile state, or manage lifecycle for nodes produced this way. Per coding guidelines: "Never declare components inside render bodies or rely on render*/get* helpers that return JSX; hoist subcomponents to module scope."

Extract buildBodyRows into a proper <SolversDirectoryTableBody> component that receives the same parameters as props:

-  const body = useMemo(
-    () => buildBodyRows(filteredSolvers, expandedRows, networkFilter, environmentFilter, toggleExpandedRow),
-    [environmentFilter, expandedRows, filteredSolvers, networkFilter, toggleExpandedRow],
-  )
+// module scope (or separate file)
+interface TableBodyProps {
+  solvers: SolverInfo[]
+  expandedRows: Record<string, boolean>
+  networkFilter: string
+  environmentFilter: string
+  onToggle: (solverId: string) => void
+}
+const SolversDirectoryTableBody = React.memo(function SolversDirectoryTableBody({
+  solvers, expandedRows, networkFilter, environmentFilter, onToggle,
+}: TableBodyProps): React.ReactNode { /* ... */ })

Then render it directly in JSX:

+        <SolversDirectoryTableBody
+          solvers={filteredSolvers}
+          expandedRows={expandedRows}
+          networkFilter={networkFilter}
+          environmentFilter={environmentFilter}
+          onToggle={toggleExpandedRow}
+        />

As per coding guidelines: "Never use inline render factories to dodge react/no-unstable-nested-components warnings; extract components instead."

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

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.tsx` around lines 40 -
43, The current useMemo wraps a JSX-producing helper buildBodyRows inside
SolversDirectoryTable, which prevents React from treating it as a proper
component; extract buildBodyRows into a module-level React component named
SolversDirectoryTableBody that accepts props (filteredSolvers, expandedRows,
networkFilter, environmentFilter, toggleExpandedRow), move any logic from
buildBodyRows into that component (and keep only pure prop/state usage), remove
the useMemo call in SolversDirectoryTable, and render <SolversDirectoryTableBody
...props /> directly in the JSX so React can reconcile, manage lifecycle, and
satisfy react/no-unstable-nested-components rules.
apps/explorer/src/hooks/useSolversInfo.ts (2)

47-47: Return object must be wrapped in useMemo

{ solversInfo, isLoading, error } is a new object reference on every render. Per coding guidelines, hooks returning objects must use useMemo. Any consumer that places the return value in a useEffect/useMemo dependency array, or a child wrapped in React.memo, will re-run unnecessarily.

♻️ Proposed fix
-  return { solversInfo, isLoading, error }
+  return useMemo(() => ({ solversInfo, isLoading, error }), [solversInfo, isLoading, error])

Also export the type so consumers can annotate variables explicitly:

-type UseSolversInfo = {
+export type UseSolversInfo = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/explorer/src/hooks/useSolversInfo.ts` at line 47, The hook
useSolversInfo currently returns a fresh object literal { solversInfo,
isLoading, error } on every render which breaks referential stability for
consumers; wrap that return value in useMemo to memoize the object (e.g. return
useMemo(() => ({ solversInfo, isLoading, error }), [solversInfo, isLoading,
error])) so the reference only changes when inputs change, and export a
corresponding type (e.g. SolversInfoHookReturn) for consumers to import and
annotate variables explicitly; update the export list to include that type.

13-47: New data fetching should use Jotai atomWithQuery instead of useState + useEffect

Per coding guidelines: "New data fetching must use Jotai atomWithQuery (jotai/query); SWR is deprecated." The current useState + useEffect pattern duplicates fetch lifecycle management that atomWithQuery handles automatically (loading/error state, deduplication, cache). Migrating to an atom also removes the need for the isSubscribed cleanup flag and the manual EMPTY_SOLVERS_INFO sentinel.

Based on learnings: "New data fetching must use Jotai atomWithQuery (jotai/query); SWR is deprecated."

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

In `@apps/explorer/src/hooks/useSolversInfo.ts` around lines 13 - 47, Replace the
manual useState/useEffect fetch in useSolversInfo with a Jotai atomWithQuery:
create an atom (e.g., solversInfoQueryAtom) that calls fetchSolversInfo(network)
and exposes data/loading/error, remove isSubscribed logic and EMPTY_SOLVERS_INFO
usage, then have useSolversInfo read that atom (useAtomValue/useAtom) and return
{ solversInfo: data ?? undefined, isLoading: isLoading, error }; ensure the atom
key depends on the network parameter so queries are cached/deduped per network
and reference the existing fetchSolversInfo, UseSolversInfo type, and
EMPTY_SOLVERS_INFO concept when mapping fallback data.
apps/explorer/src/explorer/pages/Solvers.tsx (1)

52-52: Extract the hardcoded Dune embed URL to a named constant

The magic string "https://dune.com/embeds/5931238/9574995" will silently become stale if the embed changes. Hoisting it to a named constant (e.g., SOLVERS_DUNE_EMBED_URL) in const.ts or at the top of this file makes future updates discoverable.

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

In `@apps/explorer/src/explorer/pages/Solvers.tsx` at line 52, The hardcoded Dune
embed URL in the Solvers component should be moved to a named constant so it’s
discoverable and easy to update: create a constant (e.g.,
SOLVERS_DUNE_EMBED_URL) either in this file or in a shared consts module
(const.ts) and replace the inline string
"https://dune.com/embeds/5931238/9574995" in Solvers.tsx with that constant;
update any imports if you place it in const.ts and ensure the iframe/src or
embed prop references SOLVERS_DUNE_EMBED_URL.
apps/explorer/src/explorer/pages/SolversDirectoryTable.styles.tsx (2)

171-188: Duplicated grid-template-columns value.

The grid template 12rem 8rem 1fr 1fr 6rem is repeated in both DeploymentsGridHeader and DeploymentsGridRow. Extract it into a shared constant to keep them in sync.

Suggested fix
+const DEPLOYMENTS_GRID_COLUMNS = '12rem 8rem 1fr 1fr 6rem'
+
 export const DeploymentsGridHeader = styled.div`
   display: grid;
-  grid-template-columns: 12rem 8rem 1fr 1fr 6rem;
+  grid-template-columns: ${DEPLOYMENTS_GRID_COLUMNS};
   gap: 0.8rem;
   color: ${Color.explorer_textSecondary2};
   font-size: 1.1rem;
   margin-bottom: 0.8rem;
 `

 export const DeploymentsGridRow = styled.div`
   display: grid;
-  grid-template-columns: 12rem 8rem 1fr 1fr 6rem;
+  grid-template-columns: ${DEPLOYMENTS_GRID_COLUMNS};
   gap: 0.8rem;
   padding: 0.6rem 0;
   border-top: 0.1rem solid ${Color.explorer_border};
   align-items: center;
   font-size: 1.2rem;
 `

As per coding guidelines: "Hoist repeating strings/tooltips into constants colocated with the feature."

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

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.styles.tsx` around
lines 171 - 188, The grid-template-columns value "12rem 8rem 1fr 1fr 6rem" is
duplicated in DeploymentsGridHeader and DeploymentsGridRow; hoist it into a
single constant (e.g., deploymentsGridTemplateColumns) colocated in this file
and replace the literal in both styled components to reference that constant so
the layout stays in sync.

139-150: EnvTag color logic assumes only two environments.

The ternary maps 'prod' to green and everything else to orange. This is reasonable for the current prod/barn model, but if additional environments are introduced, they'll all appear as orange. Consider using an enum + Record<Enum, T> pattern or at least adding a brief comment noting the intentional fallback.

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

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.styles.tsx` around
lines 139 - 150, EnvTag's color logic currently treats only 'prod' as green and
everything else as orange, which will mis-color any new environments; update the
implementation to map environments to colors using a clear mapping (e.g., an
Environment enum or union type plus a Record<Environment, { color, background,
border }>) and have EnvTag read values from that map instead of a ternary, or at
minimum add a comment above EnvTag explaining the deliberate fallback for
non-prod environments; reference the EnvTag styled component and the
$environment prop when making the change.
apps/explorer/src/explorer/pages/Solvers.styles.tsx (2)

92-109: Raw rgba value instead of a Color token.

Line 94 uses rgba(255, 255, 255, 0.06) while the rest of the file consistently uses Color.* tokens. If a suitable token exists (e.g., Color.explorer_border or similar), prefer it for theming consistency.

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

In `@apps/explorer/src/explorer/pages/Solvers.styles.tsx` around lines 92 - 109,
The ChartWrapper styled component uses a raw rgba value for the border; replace
that hardcoded "rgba(255, 255, 255, 0.06)" with the appropriate theme Color
token (e.g., Color.explorer_border or the matching token used elsewhere) in the
border property of ChartWrapper, and update imports if necessary to pull in
Color; if no token exists add a new Color token with that rgba value to the
theme tokens and use it here to keep theming consistent.

34-37: Inconsistent unit: 16px vs rem.

Every other font-size in both style files uses rem units (e.g., 1.2rem, 1.3rem, 2rem). Line 36 uses 16px. For consistency, use 1.6rem instead.

Suggested fix
 export const SectionTitleMeta = styled.span`
   color: ${Color.explorer_textSecondary2};
-  font-size: 16px;
+  font-size: 1.6rem;
 `
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/explorer/src/explorer/pages/Solvers.styles.tsx` around lines 34 - 37,
The SectionTitleMeta styled component uses an inconsistent unit for font-size
("16px"); update SectionTitleMeta (styled.span) to use rem units to match the
rest of the file—change font-size from 16px to 1.6rem so it follows the existing
rem-based sizing convention.
apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx (2)

1-220: File is 220 LOC — consider splitting rendering and filtering concerns.

The guideline asks for ~200 LOC per file with justification needed above that. This file mixes pure filtering/data logic (matchesSearch, filterSolvers, filterDeployments, getNetworkOptions, getEnvironmentOptions) with rendering logic (renderSummaryRow, buildBodyRows, etc.). Splitting these into separate modules (e.g., SolversDirectoryTable.filters.ts for pure logic, keeping rendering helpers in the current file) would bring both under the limit and improve cohesion.

As per coding guidelines: "Keep TypeScript/TSX sources around 200 LOC; anything over 200 needs active justification."

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

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx` around
lines 1 - 220, This file mixes pure data/filter logic with rendering and exceeds
the 200 LOC guideline; extract the pure functions matchesSearch,
filterDeployments, getNetworkOptions, getEnvironmentOptions, and filterSolvers
into a new module (e.g., SolversDirectoryTable.filters.ts) and export them,
leaving only rendering helpers (renderSolverIcon, renderNetworkChips,
renderEnvironmentTags, renderSummaryRow, renderDetailsRow, buildBodyRows) in the
original file; update imports/exports so the renderer file imports the moved
functions, keep all type references (SolverInfo, SolverDeployment) intact, and
run a quick compile to adjust any named imports/exports.

31-33: Cast chainId as SupportedChainId may silently return undefined.

If a CMS-sourced chainId doesn't exist in CHAIN_INFO, the optional chain ?.logo?.light safely returns undefined, so this won't crash. However, the cast bypasses type safety. A runtime guard (e.g., checking chainId in CHAIN_INFO) would be more explicit and avoid the cast entirely.

Suggested approach
 function getChainIcon(chainId: number): string | undefined {
-  return CHAIN_INFO[chainId as SupportedChainId]?.logo?.light || undefined
+  const info = chainId in CHAIN_INFO ? CHAIN_INFO[chainId as SupportedChainId] : undefined
+  return info?.logo?.light || undefined
 }

As per coding guidelines: "Double-check casts, especially across chains and bridge flows."

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

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx` around
lines 31 - 33, The getChainIcon function currently casts chainId as
SupportedChainId which bypasses type safety; replace the cast with an explicit
runtime guard against CHAIN_INFO (e.g., check that chainId exists in CHAIN_INFO
or that CHAIN_INFO[chainId] is defined) and then return
CHAIN_INFO[chainId].logo?.light or undefined accordingly so you remove the
unsafe cast of SupportedChainId and preserve the existing optional chaining
behavior; reference function getChainIcon and constant CHAIN_INFO.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/explorer/src/explorer/pages/Solvers.tsx`:
- Around line 51-57: Add a sandbox attribute to the Dune iframe to restrict
capabilities: update the iframe element in Solvers.tsx (the iframe with src
"https://dune.com/embeds/5931238/9574995" and title "Solvers across networks")
to include sandbox="allow-scripts allow-same-origin" while keeping existing
attributes (loading, referrerPolicy, allow) unchanged so the chart still renders
but the frame gains defense-in-depth restrictions.

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx`:
- Around line 35-38: Convert the helper functions that return JSX into top-level
React function components: replace renderSolverIcon with a SolverIcon component
that accepts a solver prop (used as <SolverIcon solver={solver} />), convert
renderNetworkChips -> NetworkChips, renderEnvironmentTags -> EnvironmentTags,
renderSummaryRow -> SummaryRow, and renderDetailsRow -> DetailsRow; hoist each
new component to module scope (outside any render bodies), accept the same data
props the helper previously took, return identical JSX, and update all call
sites to use the new JSX component form so React can properly reconcile and
allow future memoization (e.g., React.memo) if desired.

In `@apps/explorer/src/utils/fetchSolversInfo.ts`:
- Line 20: The module-level new URL(CMS_BASE_URL).origin (CMS_ORIGIN) can throw
during import if the env var is malformed; update fetchSolversInfo.ts to guard
this by wrapping the URL parse in a try/catch (or a small init function) that
catches TypeError and falls back to a safe value (e.g., empty string or
window.location.origin) and logs or warns about the malformed CMS_BASE_URL, then
use that safe CMS_ORIGIN everywhere; ensure you reference and replace the
module-level constant CMS_ORIGIN with the new guarded value or accessor so
imports won't crash on invalid env values.
- Around line 116-127: filterSolversByNetwork currently keeps solvers if
solver.deployments.length > 0 even when all those deployments yield no networks
(e.g., all are inactive), breaking the invariant that solvers have networks; fix
by computing the networks for the per-chain deployments using
mapSolverNetworks(deployments) and only returning the solver when
networks.length > 0, returning the same deployments and networks values (i.e.,
replace the final .filter((solver) => solver.deployments.length > 0) with a
filter that requires mapSolverNetworks(deployments).length > 0 or compute const
networks = mapSolverNetworks(deployments) and filter on networks.length > 0
before returning { ...solver, deployments, networks } so inactive-only
deployments are excluded and the networks invariant is preserved.

---

Nitpick comments:
In `@apps/explorer/src/explorer/pages/Solvers.styles.tsx`:
- Around line 92-109: The ChartWrapper styled component uses a raw rgba value
for the border; replace that hardcoded "rgba(255, 255, 255, 0.06)" with the
appropriate theme Color token (e.g., Color.explorer_border or the matching token
used elsewhere) in the border property of ChartWrapper, and update imports if
necessary to pull in Color; if no token exists add a new Color token with that
rgba value to the theme tokens and use it here to keep theming consistent.
- Around line 34-37: The SectionTitleMeta styled component uses an inconsistent
unit for font-size ("16px"); update SectionTitleMeta (styled.span) to use rem
units to match the rest of the file—change font-size from 16px to 1.6rem so it
follows the existing rem-based sizing convention.

In `@apps/explorer/src/explorer/pages/Solvers.tsx`:
- Line 52: The hardcoded Dune embed URL in the Solvers component should be moved
to a named constant so it’s discoverable and easy to update: create a constant
(e.g., SOLVERS_DUNE_EMBED_URL) either in this file or in a shared consts module
(const.ts) and replace the inline string
"https://dune.com/embeds/5931238/9574995" in Solvers.tsx with that constant;
update any imports if you place it in const.ts and ensure the iframe/src or
embed prop references SOLVERS_DUNE_EMBED_URL.

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx`:
- Around line 1-220: This file mixes pure data/filter logic with rendering and
exceeds the 200 LOC guideline; extract the pure functions matchesSearch,
filterDeployments, getNetworkOptions, getEnvironmentOptions, and filterSolvers
into a new module (e.g., SolversDirectoryTable.filters.ts) and export them,
leaving only rendering helpers (renderSolverIcon, renderNetworkChips,
renderEnvironmentTags, renderSummaryRow, renderDetailsRow, buildBodyRows) in the
original file; update imports/exports so the renderer file imports the moved
functions, keep all type references (SolverInfo, SolverDeployment) intact, and
run a quick compile to adjust any named imports/exports.
- Around line 31-33: The getChainIcon function currently casts chainId as
SupportedChainId which bypasses type safety; replace the cast with an explicit
runtime guard against CHAIN_INFO (e.g., check that chainId exists in CHAIN_INFO
or that CHAIN_INFO[chainId] is defined) and then return
CHAIN_INFO[chainId].logo?.light or undefined accordingly so you remove the
unsafe cast of SupportedChainId and preserve the existing optional chaining
behavior; reference function getChainIcon and constant CHAIN_INFO.

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.styles.tsx`:
- Around line 171-188: The grid-template-columns value "12rem 8rem 1fr 1fr 6rem"
is duplicated in DeploymentsGridHeader and DeploymentsGridRow; hoist it into a
single constant (e.g., deploymentsGridTemplateColumns) colocated in this file
and replace the literal in both styled components to reference that constant so
the layout stays in sync.
- Around line 139-150: EnvTag's color logic currently treats only 'prod' as
green and everything else as orange, which will mis-color any new environments;
update the implementation to map environments to colors using a clear mapping
(e.g., an Environment enum or union type plus a Record<Environment, { color,
background, border }>) and have EnvTag read values from that map instead of a
ternary, or at minimum add a comment above EnvTag explaining the deliberate
fallback for non-prod environments; reference the EnvTag styled component and
the $environment prop when making the change.

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.tsx`:
- Around line 40-43: The current useMemo wraps a JSX-producing helper
buildBodyRows inside SolversDirectoryTable, which prevents React from treating
it as a proper component; extract buildBodyRows into a module-level React
component named SolversDirectoryTableBody that accepts props (filteredSolvers,
expandedRows, networkFilter, environmentFilter, toggleExpandedRow), move any
logic from buildBodyRows into that component (and keep only pure prop/state
usage), remove the useMemo call in SolversDirectoryTable, and render
<SolversDirectoryTableBody ...props /> directly in the JSX so React can
reconcile, manage lifecycle, and satisfy react/no-unstable-nested-components
rules.

In `@apps/explorer/src/hooks/useSolversInfo.ts`:
- Line 47: The hook useSolversInfo currently returns a fresh object literal {
solversInfo, isLoading, error } on every render which breaks referential
stability for consumers; wrap that return value in useMemo to memoize the object
(e.g. return useMemo(() => ({ solversInfo, isLoading, error }), [solversInfo,
isLoading, error])) so the reference only changes when inputs change, and export
a corresponding type (e.g. SolversInfoHookReturn) for consumers to import and
annotate variables explicitly; update the export list to include that type.
- Around line 13-47: Replace the manual useState/useEffect fetch in
useSolversInfo with a Jotai atomWithQuery: create an atom (e.g.,
solversInfoQueryAtom) that calls fetchSolversInfo(network) and exposes
data/loading/error, remove isSubscribed logic and EMPTY_SOLVERS_INFO usage, then
have useSolversInfo read that atom (useAtomValue/useAtom) and return {
solversInfo: data ?? undefined, isLoading: isLoading, error }; ensure the atom
key depends on the network parameter so queries are cached/deduped per network
and reference the existing fetchSolversInfo, UseSolversInfo type, and
EMPTY_SOLVERS_INFO concept when mapping fallback data.

In `@apps/explorer/src/utils/fetchSolversInfo.ts`:
- Around line 111-114: The guard in filterSolversByNetwork currently uses a
falsy check (!network) which treats 0, NaN, and null as "no network"; update the
check to explicitly test for undefined (network === undefined) so only an
omitted network returns allSolvers. Locate function filterSolversByNetwork and
replace the early-return condition to use network === undefined, leaving the
rest of the logic unchanged.

Comment thread apps/explorer/src/explorer/pages/Solvers.tsx
Comment thread apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx Outdated
Comment thread apps/explorer/src/utils/fetchSolversInfo.ts Outdated
Comment thread apps/explorer/src/utils/fetchSolversInfo.ts
@fairlighteth
Copy link
Copy Markdown
Contributor Author

Solver addresses now also hyperlinked:
Screenshot 2026-02-18 at 16 27 13

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx (1)

32-34: || undefined in getChainIcon is redundant.

Optional chaining already returns undefined when any link in the chain is nullish. The || undefined only adds noise (and would silently suppress an unexpected empty-string URL). Remove it or, if guarding empty strings is intentional, use || undefined with a comment.

♻️ Proposed fix
-  return CHAIN_INFO[chainId as SupportedChainId]?.logo?.light || undefined
+  return CHAIN_INFO[chainId as SupportedChainId]?.logo?.light
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx` around
lines 32 - 34, The getChainIcon function uses optional chaining on
CHAIN_INFO[chainId as SupportedChainId]?.logo?.light so the trailing "||
undefined" is redundant and should be removed; update the getChainIcon
implementation to simply return CHAIN_INFO[chainId as
SupportedChainId]?.logo?.light, or if you intentionally want to guard against
empty-string URLs, replace the "|| undefined" with an explicit check (e.g.,
return value || undefined) and add a short comment explaining the empty-string
guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx`:
- Around line 140-142: Update the ExpandButton to expose its state and target to
assistive tech: set aria-expanded to the boolean isExpanded and change the
aria-label to a descriptive string that includes the target solver (e.g.,
reference solver.solverId or solver.name) so the button reads like "Toggle
deployments for {solver.solverId}"; keep the onClick handler (onToggle)
unchanged.
- Around line 214-238: Replace the render-factory function buildBodyRows with a
proper React component named SolverTableBody that accepts props (solvers,
expandedRows, networkFilter, environmentFilter, onToggle) and returns the same
JSX; hoist and convert the helper renderSummaryRow and renderDetailsRow into
module-scope components (e.g., SummaryRow, DetailsRow) and use them inside
SolverTableBody so reconciliation is explicit and no JSX-returning helpers are
used inside render paths; update the calling component to render
<SolverTableBody .../> with the same props and preserve existing behavior for
empty solvers and expandedRows logic.

---

Duplicate comments:
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx`:
- Around line 36-39: The file still uses plain functions that return JSX —
renderSolverIcon, renderNetworkChips, renderEnvironmentTags, renderSummaryRow,
and renderDetailsRow — which must be converted into module‑scope React function
components (e.g., SolverIcon, NetworkChips, EnvironmentTags, SummaryRow,
DetailsRow) so React can properly reconcile and you can apply memoization;
replace calls to the render* helpers with JSX usage of the new components,
accept the same props (SolverInfo, networks/env/summar y props) as parameters,
move them to top-level module scope, and wrap with React.memo where appropriate
(for example SolverIcon and NetworkChips) to preserve performance semantics.

---

Nitpick comments:
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx`:
- Around line 32-34: The getChainIcon function uses optional chaining on
CHAIN_INFO[chainId as SupportedChainId]?.logo?.light so the trailing "||
undefined" is redundant and should be removed; update the getChainIcon
implementation to simply return CHAIN_INFO[chainId as
SupportedChainId]?.logo?.light, or if you intentionally want to guard against
empty-string URLs, replace the "|| undefined" with an explicit check (e.g.,
return value || undefined) and add a short comment explaining the empty-string
guard.

Comment on lines +140 to +142
<ExpandButton onClick={(): void => onToggle(solver.solverId)} aria-label="Toggle deployments">
{isExpanded ? '▾' : '▸'}
</ExpandButton>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

ExpandButton is missing aria-expanded and has a non-descriptive aria-label.

  • aria-label="Toggle deployments" gives no context about which solver is being toggled.
  • The expanded/collapsed state is conveyed only visually (/); aria-expanded is absent, so assistive technologies cannot report the current state.
🛡️ Proposed fix
-<ExpandButton onClick={(): void => onToggle(solver.solverId)} aria-label="Toggle deployments">
-  {isExpanded ? '▾' : '▸'}
+<ExpandButton
+  onClick={(): void => onToggle(solver.solverId)}
+  aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${solver.displayName} deployments`}
+  aria-expanded={isExpanded}
+>
+  {isExpanded ? '▾' : '▸'}
📝 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.

Suggested change
<ExpandButton onClick={(): void => onToggle(solver.solverId)} aria-label="Toggle deployments">
{isExpanded ? '▾' : '▸'}
</ExpandButton>
<ExpandButton
onClick={(): void => onToggle(solver.solverId)}
aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${solver.displayName} deployments`}
aria-expanded={isExpanded}
>
{isExpanded ? '▾' : '▸'}
</ExpandButton>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx` around
lines 140 - 142, Update the ExpandButton to expose its state and target to
assistive tech: set aria-expanded to the boolean isExpanded and change the
aria-label to a descriptive string that includes the target solver (e.g.,
reference solver.solverId or solver.name) so the button reads like "Toggle
deployments for {solver.solverId}"; keep the onClick handler (onToggle)
unchanged.

Comment on lines +214 to +238
export function buildBodyRows(
solvers: SolverInfo[],
expandedRows: Record<string, boolean>,
networkFilter: string,
environmentFilter: string,
onToggle: (solverId: string) => void,
): React.ReactNode {
if (!solvers.length) {
return (
<tr>
<td colSpan={5}>
<Placeholder>No solvers match your current filters.</Placeholder>
</td>
</tr>
)
}

return solvers.flatMap((solver) => {
const isExpanded = !!expandedRows[solver.solverId]
const deployments = filterDeployments(solver.deployments, networkFilter, environmentFilter)
const summary = renderSummaryRow(solver, isExpanded, onToggle)

if (!isExpanded) return [summary]
return [summary, renderDetailsRow(solver, deployments)]
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

buildBodyRows is itself a JSX-returning render factory and should be a proper component.

Even though it doesn't have the render* prefix, buildBodyRows is invoked inside a component's JSX return and produces React nodes — the same render-factory anti-pattern the guidelines prohibit. It should be converted to a proper <SolverTableBody> component that accepts the same parameters as props:

♻️ Proposed refactor
-export function buildBodyRows(
-  solvers: SolverInfo[],
-  expandedRows: Record<string, boolean>,
-  networkFilter: string,
-  environmentFilter: string,
-  onToggle: (solverId: string) => void,
-): React.ReactNode {
-  if (!solvers.length) {
-    return (
-      <tr>
-        <td colSpan={5}>
-          <Placeholder>No solvers match your current filters.</Placeholder>
-        </td>
-      </tr>
-    )
-  }
-
-  return solvers.flatMap((solver) => {
-    const isExpanded = !!expandedRows[solver.solverId]
-    const deployments = filterDeployments(solver.deployments, networkFilter, environmentFilter)
-    const summary = renderSummaryRow(solver, isExpanded, onToggle)
-    if (!isExpanded) return [summary]
-    return [summary, renderDetailsRow(solver, deployments)]
-  })
-}
+interface SolverTableBodyProps {
+  solvers: SolverInfo[]
+  expandedRows: Record<string, boolean>
+  networkFilter: string
+  environmentFilter: string
+  onToggle: (solverId: string) => void
+}
+
+export function SolverTableBody({
+  solvers,
+  expandedRows,
+  networkFilter,
+  environmentFilter,
+  onToggle,
+}: SolverTableBodyProps): React.ReactNode {
+  if (!solvers.length) {
+    return (
+      <tr>
+        <td colSpan={5}>
+          <Placeholder>No solvers match your current filters.</Placeholder>
+        </td>
+      </tr>
+    )
+  }
+
+  return solvers.flatMap((solver) => {
+    const isExpanded = !!expandedRows[solver.solverId]
+    const deployments = filterDeployments(solver.deployments, networkFilter, environmentFilter)
+    return isExpanded
+      ? [<SummaryRow key={solver.solverId} solver={solver} isExpanded onToggle={onToggle} />,
+         <DetailsRow key={`${solver.solverId}-details`} solver={solver} deployments={deployments} />]
+      : [<SummaryRow key={solver.solverId} solver={solver} isExpanded={false} onToggle={onToggle} />]
+  })
+}

Combined with converting the private render* helpers into named components (SummaryRow, DetailsRow, etc.), this brings the module well within the 200-LOC target and makes React reconciliation fully explicit.

As per coding guidelines: "Never declare components inside render bodies or rely on render*/get* helpers that return JSX; hoist subcomponents to module scope."

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

In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx` around
lines 214 - 238, Replace the render-factory function buildBodyRows with a proper
React component named SolverTableBody that accepts props (solvers, expandedRows,
networkFilter, environmentFilter, onToggle) and returns the same JSX; hoist and
convert the helper renderSummaryRow and renderDetailsRow into module-scope
components (e.g., SummaryRow, DetailsRow) and use them inside SolverTableBody so
reconciliation is explicit and no JSX-returning helpers are used inside render
paths; update the calling component to render <SolverTableBody .../> with the
same props and preserve existing behavior for empty solvers and expandedRows
logic.

Copy link
Copy Markdown
Contributor

@elena-zh elena-zh left a comment

Choose a reason for hiding this comment

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

Hey @fairlighteth , cool, great job!
As usual. some issues-questions from my side:

  1. Page icon in the menu: we use balck-white everywhere else in the menu, but this one is green. Looks weird, frankly
Image

2. Might it be possible to add navigation links to block explorers to Solver addresses? If it is too complicated, add a copy icon to these addresses.. WDYT? This must have been fixed while I was writing my comment.

Image
  1. Might be not important, but 'lens' chain icon is not visible
Image
  1. I see that in some chains INK solver details are duplicated. Could you please check why?
Image
  1. For some solvers like '1Inch, Baseline, Quasimodo Webside navigates to Gnosis.io. Is it correct info?
Image
  1. The page is closed when I change a chain in the network selecotor. I think, it should not depend of a chain selected there. Might it be possible not to navigate a user to the Home page when another chain is picked?
Image
  1. It would be great to improve responsive views:
Image Image
  1. WDYT about adding a link to this page to the CoW Swap/Cow'fi footer? Or to the menu there? :)

  2. I noticed, that Ink chain is missing in the chart. Is it OK?

  3. WDYT about adding one more filter: 'Active: Yes/No"?

  4. Search field: It would be great to add a cross icon into there to reset a search value :)

Image
  1. What is 'chain 10' for Baseline solver?
Image
  1. It is weird: Copium solver has a solver address on Barn, but it is filetrerd as running on Prod only. Is it OK?
Image

Thank you!


export async function fetchSolversInfo(network?: number): Promise<SolversInfo> {
if (!solversInfoCache) {
const response = await fetch(SOLVERS_API_URL)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would be great to use import { getCmsClient } from '@cowprotocol/core' instead

return CHAIN_INFO[chainId as SupportedChainId]?.logo?.light || undefined
}

function renderSolverIcon(solver: SolverInfo): React.ReactNode {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

render function

})
}

function renderNetworkChips(solver: SolverInfo): React.ReactNode {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Render function

)
}

function renderEnvironmentTags(solver: SolverInfo): React.ReactNode {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Render function

@fairlighteth
Copy link
Copy Markdown
Contributor Author

  1. I think it's fine as a filter as is. IMO the CMS data should be correct/fixed. If we don't want to have it, we should cleanup CMS data.
  2. Good idea, addressed now.

@fairlighteth
Copy link
Copy Markdown
Contributor Author

@shoom3301 addressed all issues

Copy link
Copy Markdown
Contributor

@elena-zh elena-zh left a comment

Choose a reason for hiding this comment

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

Thank you, @fairlighteth , looks good.
As for the cms, should we let the solvers team know to prettify solvers data before releasing these changes?

@fairlighteth
Copy link
Copy Markdown
Contributor Author

@elena-zh informed the team. Would not make it a blocker for releasing this PR.

import { getExplorerBaseUrl } from '@cowprotocol/common-utils'
import { SupportedChainId } from '@cowprotocol/cow-sdk'

export function getSolversExplorerUrl(): string {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nitpick, it can be a const

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed now.

)
}

export function SolverDetailsRow({ solver, deployments }: SolverDetailsRowProps): React.ReactNode {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@fairlighteth this file is big enough, you could extract pure components to a separate file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed now.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants