Files
fce9bd8f96 Agent tooling relocation, architecture redesign docs (#3343)
* add export for address txs

* transfer all export from page to dialog

* remove old route and components

* improve file name and fix loading state of icon button

* basic implementation

* refetch item status

* status indicator for download items

* changes for multichain

* handle too many requests for downloads error

* update button styles and spacing

* fix tests

* disable async export for multichain

* Fix CSV export cancel flow and stabilize context updates

Applied via @cursor push command

* test and review fixes

* move file to client folder

* round 3

* add glossary

* migration tasks and skill

* move skills to .agents folder

* rename migrate skill

* refine project rules

* move MIGRATION_TASKS.md to client directory

* update sym links

* refine migration skills

* fixes after merging the main branch

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-03-02 16:42:12 +02:00

24 KiB

Client architecture blueprint


1. Purpose

  • Co-locate everything that belongs to a slice (core explorer domain) or feature (optional / config-driven) in one place: UI, hooks, utils, API response types for that area, mocks, stubs, etc.
  • Retire top-level lib/, mocks/, stubs/, types/ (and ui/) as unscoped buckets — their contents move under /client or other agreed homes (see §6).
  • Keep clear boundaries so the graph stays maintainable (see §8).

2. Naming conventions (agreed)

Aligned with Next.js-style paths: kebab-case for directories and for non-component TS modules (helpers, parsers, constants files).

Artifact Convention Examples
Directories kebab-case slices/tx/, features/user-ops/, pages/tx-details/
React components PascalCase.tsx TxDetails.tsx, TxIndexTable.tsx
Hooks use + camelCase in the filename (ecosystem norm) useTxQuery.ts, useIsMobile.ts
Helper / util modules (non-hook) kebab-case.ts format-tx-hash.ts, get-tab-list.ts
Role files lowercase types.ts, mocks.ts, stubs.ts

Same folder may contain TxDetails.tsx, useTxQuery.ts — hooks stay useCamelCase.ts; everything else non-component prefers kebab-case.

No re-export-only barrel files inside /client. Deep imports are preferred; dumb barrels (files that only export * from ...) slow TypeScript server performance and break tree-shaking at scale. Aggregation files that define or curate their own public surface (e.g. client/api/services/general/index.ts acting as a facade for the General API) are acceptable where they provide genuine value.


3. Top-level repo layout (conceptual)

Path Role
/client Product explorer: shell, API client layer, slices, features, shared. Replaces /ui and absorbs most of lib, mocks, stubs, types (see §6).
/configs Env-derived config and feature flags. Must not import from /client. Consts + types derived from those consts (e.g. supported ad providers) live here.
/nextjs Next integration, SSR helpers, server utilities.
/pages Next.js file-based routes only (thin wrappers — a dynamic import and getServerSideProps call; nothing else). 404 vs "empty page" is defined here — invalid routes do not render the page component.
/toolkit Design system and shared code published to npm / reused across company projects — stays outside client. Referenced as a local path alias during this migration; publishing scope is out of scope for this task.
Root lib/ Removed after migration except pieces explicitly moved elsewhere (e.g. monitoring → nextjs).

4. Inside /client

4.1 client/shell

Application chrome: layout, error UI, header, footer, page title, top-bar, and other frame-level concerns.

ui/snippets/ split (complete):

Source Destination
header/, footer/, navigation/, topBar/ client/shell/
searchBar/ client/slices/search/
networkLogo/ client/shared/
networkMenu/ client/features/multichain/
auth/, user/ client/features/account/

Context split from lib/contexts/:

Source Destination
app.tsx, fallback.tsx client/shell/
settings.tsx client/shell/top-bar/ (alongside TopBar component)
multichain.tsx client/features/multichain/
rewards.tsx client/features/rewards/
marketplace.tsx client/features/marketplace/
addressHighlight.tsx client/slices/address/

Hooks from lib/hooks/:

  • useNavItems → client/shell/
  • Everything else generic → client/shared/hooks/ (see §4.5)

4.2 client/api

  • Full migration of today's lib/api (transport: fetch, resources, URL building, query client wiring, etc.).
  • client/api must not import runtime logic from client/slices/* or client/features/* (avoids cycles). import type from slices is permitted when an API service file maps a resource to its response type — the type lives with the slice that owns it, and client/api references it for the payload mapping only.
  • Per-resource response types and view-specific types are not centralized under client/api; they live in the slice or feature that owns the domain (co-location with hooks/components).
  • WebSocket transport (lib/socket/) → client/api/socket/ — same reasoning as fetch: it is a transport mechanism, not a UI concern.

client/api/services/ structure:

Keep the current organization largely intact. Do not flatten everything into a 1:1 mapping with slices — feature services often have only one or two resources and do not warrant separate files.

general/ maps to the Blockscout General API namespace — it is not restricted to vanilla-EVM resources. Files inside it follow the API grouping, not the slice/feature taxonomy.

client/api/
  services/
    general/          ← Blockscout General API namespace; only split files inside where genuinely needed
      tx.ts
      block.ts
      misc.ts         ← resources with no clear single-domain home stay here
      rollup.ts       ← rollup resources are part of the General API
      v1.ts           ← previous API version; one active resource
      ...
    rewards.ts        ← feature-specific APIs stay flat alongside general/
    stats.ts
    user-ops.ts
    bens.ts
    ...
  types.ts            ← shared API types incl. IsPaginated (absorbs services/utils.ts)
  socket/             ← migrated from lib/socket/

4.3 client/slices

Core explorer entities — always part of the product surface (not optional "features" in the configs/app/features sense).

Classification criterion: "Can this area exist on a vanilla EVM chain with no feature flag?" Yes → slice. No (requires a configs/app/features flag) → feature.

Confirmed slices (non-exhaustive): tx, block, search, token, address, contract, internal-tx, home, tokens, accounts, token-instance, …

  • contract (slice): Contract verification and SolidityScan (lib/solidityScan/) are slice concerns (available on any chain).
  • internal-tx (slice): Own slice; tx composes internal-tx components (e.g. tab content). Prefer no imports from internal-tx back into tx to avoid cycles; share via client/shared or a thin types-only surface if needed.

Rollup sub-types on slice types: slices/tx/types/api.ts imports feature-specific sub-types (e.g. ArbitrumTransactionData) from their respective feature via import type. Optional fields fan out at the slice type level — this mirrors the real API contract and is intentional (see §11).

Typical slice shape:

client/slices/tx/
  pages/
    tx-index/           # kebab-case folder = one "screen" / route target
      TxIndex.tsx
      TxIndexTable.tsx
      ...
    tx-details/
      TxDetails.tsx
      ...
  components/
  hooks/                 # e.g. useTxQuery.ts
  utils/                 # kebab-case .ts files
  types/
    api.ts               # response / DTO types owned by this slice
  mocks.ts
  stubs.ts

Routing rule: One Next route ≈ one folder under pages/ inside the slice (or feature). The file under root pages/ stays a thin dynamic import into client/....

4.4 client/features

Optional product areas — roughly mirrors configs/app/features, but:

  • Folders may exist for readability and maintenance (e.g. per rollup type) even when not everything is enabled for a given chain; runtime behavior still comes from config/env, not "folder exists = on."

Confirmed features (non-exhaustive): user-ops, data-availability, multichain, name-domains, account, stats, gas-tracker, validators, marketplace, rewards, rollup/*, chain-variants/*, advanced-filter, ad-banner, safe-address-tags, metasuites, csv-export, …

  • stats and gas-tracker are features (not slices) — they are config-gated per chain.
  • validators is a feature — chain-specific, not present on a vanilla EVM chain.
  • epochs is a sub-concern of the Celo chain variant (features/chain-variants/celo/), not a standalone feature.
  • data-availability was previously referred to as blobs — use data-availability throughout.

Classification rules:

  1. "Can this area exist on a vanilla EVM chain with no feature flag?" Yes → slice. No → feature.
  2. Config-gated infrastructure with no user-facing UI (analytics providers, error monitoring, A/B flag providers) lives in client/shared/, not features/ — the presence of a config flag alone is not sufficient to qualify for a feature folder.
  3. Every config-gated feature with user-facing UI gets its own folder under features/, regardless of size — consistency and discoverability outweigh folder count.

Chain-specific non-rollup features — chain variants that are not rollups (ZetaChain, TAC, SUAVE, Celo, MegaETH, Beacon chain, etc.) are grouped under features/chain-variants/<name>/, parallel to features/rollup/<type>/. Example: features/chain-variants/celo/, features/chain-variants/tac/, features/chain-variants/zeta-chain/.

Variant / chain-specific transaction lists (e.g. kettle, zeta-specific list UIs) live under features/<name>/pages/..., not as variants inside slices/tx.

Rollups: features/rollup/<rollup-type>/ (e.g. optimism, arbitrum, …) — organized by rollup type, because views and helpers differ by rollup. Sub-areas (deposits, batches, withdrawals) are subfolders under that rollup, not separate top-level rollup taxonomies.

Chain variants (non-rollup): features/chain-variants/<name>/ (e.g. celo, tac, zeta-chain, mega-eth, suave, beacon-chain) — same sub-folder structure as rollups.

Typical feature shape: same idea as slices — pages/, components/, hooks/, utils/, types/, mocks.ts, stubs.ts.

4.5 client/shared

Cross-cutting UI and helpers with no single domain owner.

  • No files directly in client/shared/ — only subfolders grouped by purpose.
  • No index.ts barrel files (same rule as the rest of /client).

Confirmed subfolders (round 2 decisions):

Subfolder Contents / origin
analytics/ lib/mixpanel/
auth/ lib/decodeJWT.ts
chain/ lib/networks/ (all network → chain renamed) + lib/units.ts
date-and-time/ lib/hooks/useTimeAgoIncrement and similar date utils
errors/ lib/errors/
feature-flags/ lib/growthbook/ (runtime A/B flags — distinct from build-time configs/)
hooks/ Generic hooks from lib/hooks/ with no single domain owner
i18n/ lib/setLocale.ts
links/utils/ lib/utils/stripUtmParams.ts and URL-related helpers
lists/ lib/hooks/useLazyRenderedList, lib/hooks/useInitialList, lib/getItemIndex.ts
metadata/ lib/metadata/ (centralized — see note below)
monitoring/rollbar/ lib/rollbar/ (client-side observability infrastructure)
router/ lib/router/ + query-param filter helpers (getFilterValueFromQuery, etc.)
storage/ lib/cookies.ts
text/ lib/capitalizeFirstLetter.ts, shortenString.ts, escapeRegExp.ts, highlightText.ts
transformers/ Hex / bytes / base64 conversion utils (hexToBytes, base64ToHex, etc.)
utils/ Tiny misc utilities with no better home (delay.ts, isMetaKey.tsx)
web3/ lib/web3/
(existing) pagination/, charts/, entities/, etc. — keep as-is

client/shared/metadata/ note: The title/description template map is a routing manifest (Record<Route['pathname'], string>) with TypeScript exhaustiveness enforced across all routes. Splitting it into slices/features loses that guarantee. It stays centralized in client/shared/metadata/. The ApiData<Pathname> type within it uses import type from the relevant slices (e.g. TokenInfo from slices/token/types/api.ts) — a shared → slice import type is acceptable here (no cycle, no runtime import).


5. Config vs client

  • /configs never imports /client.
  • Types that describe config shape (including types derived from consts in config, e.g. supported ad banner providers) live in config next to the relevant feature/config module.
  • /client imports /configs for feature flags and app configuration.
  • Runtime feature flags (Growthbook A/B) live in client/shared/feature-flags/ — they require a React context and cannot live in configs/.

6. Migration map (sources → destinations)

ui/ → client/

Current Destination
ui/** client/** (restructured into shell / slices / features / shared)
ui/snippets/header/, footer/, navigation/, topBar/ client/shell/
ui/snippets/searchBar/ client/slices/search/
ui/snippets/networkLogo/ client/shared/
ui/snippets/networkMenu/ client/features/multichain/
ui/snippets/auth/, user/ client/features/account/

lib/ → destinations

Current Destination
lib/api/** client/api/** (services structure preserved; services/utils.ts → client/api/types.ts)
lib/socket/ client/api/socket/
lib/monitoring/ nextjs/
lib/rollbar/ client/shared/monitoring/rollbar/
lib/metadata/ client/shared/metadata/
lib/router/ client/shared/router/
lib/web3/ client/shared/web3/
lib/errors/ client/shared/errors/
lib/mixpanel/ client/shared/analytics/
lib/growthbook/ client/shared/feature-flags/
lib/networks/ + lib/units.ts client/shared/chain/ (rename network → chain throughout)
lib/hooks/useNavItems client/shell/
lib/hooks/useTimeAgoIncrement client/shared/date-and-time/
lib/hooks/useLazyRenderedList client/shared/lists/
lib/hooks/useInitialList client/shared/lists/
lib/hooks/useRewardsActivity client/features/rewards/
lib/hooks/useAddressProfileApiQuery client/slices/address/
lib/hooks/useFetch client/api/
lib/hooks/useGetCsrfToken client/features/account/
lib/hooks/useGraphLinks client/features/marketplace/
lib/hooks/useAdblockDetect client/features/ad-banner/
lib/hooks/useIsSafeAddress client/features/safe-address-tags/
lib/hooks/useNotifyOnNavigation client/features/metasuites/
lib/hooks/ (remaining) client/shared/hooks/
lib/contexts/app.tsx, fallback.tsx client/shell/
lib/contexts/settings.tsx client/shell/top-bar/
lib/contexts/multichain.tsx client/features/multichain/
lib/contexts/rewards.tsx client/features/rewards/
lib/contexts/marketplace.tsx client/features/marketplace/
lib/contexts/addressHighlight.tsx client/slices/address/
lib/tx/ client/slices/tx/utils/
lib/token/ client/slices/token/
lib/address/ client/slices/address/
lib/rollups/ client/features/rollup/
lib/multichain/ client/features/multichain/
lib/search/ client/slices/search/
lib/contracts/ client/slices/contract/
lib/solidityScan/ client/slices/contract/
lib/stats/ client/features/stats/
lib/utils/stripUtmParams.ts client/shared/links/utils/
lib/capitalizeFirstLetter.ts, shortenString.ts, escapeRegExp.ts, highlightText.ts client/shared/text/
lib/base64ToHex.ts, bytesToBase64.ts, bytesToHex.ts, hexToBase64.ts, hexToBytes.ts, hexToAddress.ts, hexToDecimal.ts, hexToUtf8.ts client/shared/transformers/
lib/cookies.ts client/shared/storage/
lib/decodeJWT.ts client/shared/auth/
lib/setLocale.ts client/shared/i18n/
lib/delay.ts, lib/isMetaKey.tsx client/shared/utils/
lib/recentSearchKeywords.ts client/slices/search/
lib/getFilterValueFromQuery.ts, getFilterValuesFromQuery.ts, getValuesArrayFromQuery.ts client/shared/router/
lib/getItemIndex.ts client/shared/lists/
lib/getErrorMessage.ts client/shared/errors/

Other sources

Current Destination
mocks/**, stubs/** Co-located mocks.ts / stubs.ts under the relevant slice/feature; shared test data → client/shared/... if truly cross-domain
types/api/*, types/client/*, etc. Slice/feature types/ (and config-owned types → configs) — not a single global types/ tree at repo root after migration

Migration process: incremental PRs/tasks — update all imports in the same PR as the move. No long-lived re-export shims. Import path updates for test files can be scripted with a codemod.


7. Testing

  • Playwright / visual tests: stay next to the implementation (e.g. *.pw.tsx alongside components), same as today.

8. Dependency rules (high level)

  • ESLint rules (import/no-cycle + explicit boundaries rules for client/api, configs, toolkit) must be enabled before the migration starts. Running them against the existing code surfaces real problem areas cheaply, before they become migration blockers.
  • Enforcement strategy: warn on legacy, error on new. Configure boundary rules to emit errors only within client/; legacy lib/ and ui/ paths receive warnings only. This avoids a costly upfront clean-up of the entire codebase while ensuring every migrated file is immediately held to the new standard.
  • Guidelines:
    • client/api allows import type from slices/features for response-shape typing; no runtime imports (no logic, hooks, or components — types only).
    • Slice → feature type imports are intentional: the backend includes feature-specific fields (e.g. arbitrum?, scroll?) in API responses unconditionally when the rollup is enabled — the frontend type must mirror the full API contract. UI component imports follow the same direction (e.g. TxDetails imports TxDetailsArbitrum).
    • client/shared → slice import type is acceptable where shared infrastructure genuinely needs a slice-owned type (e.g. client/shared/metadata/ referencing TokenInfo from slices/token/types/api.ts). No runtime imports from shared → slice.
    • Avoid circular graphs between slices/features; use composition at a shallow level (page or a single container component) when optional UI plugs into a core screen.
    • internal-tx → tx: avoid tx → internal-tx → tx cycles; lift shared bits to shared or types-only exports.

9. Config ↔ feature folder naming

  • Feature directories under client/features/: kebab-case (user-ops, name-domains).
  • Config files under configs/app/features/: migrate to kebab-case in a single dedicated pre-migration PR (before feature folder migration starts), so config file names and feature folder names share one mental model.

10. Migration execution plan

Order of operations

  1. Pre-migration PR: rename configs/app/features/ files to kebab-case.
  2. Pre-migration PR: enable ESLint import/no-cycle + boundaries rules; fix existing violations.
  3. Bottom-up migration: start with client/api and client/shared (lowest in the dependency graph, most imported). Establishes the foundation without touching any UI.
  4. Pilot slice — tx: migrate lib/tx/, the tx API service, ui/tx/**, hooks, types, mocks end-to-end. Use the result as the canonical template for all subsequent slices.
  5. Remaining slices in parallel (one PR per slice).
  6. Features in parallel (one PR per feature or logical group).
  7. client/shell — migrate after slices/features it depends on are in place.
  8. Remove root lib/ — confirm empty, delete.

Rules during migration

  • Update all imports in the same PR as the file move — no shims. For high-fanout files use an automated codemod as a dedicated commit within the branch.
  • Rename to kebab-case at move time — file moves and naming convention changes happen in one pass, never two separate PRs.
  • Each PR leaves ESLint green within client/; do not merge with new cycle/boundary violations inside client/.
  • types/api.ts is the stable external type surface for each slice/feature. External consumers (other slices, features, client/api/services/) must import types only from <slice|feature>/types/api.ts, never from deeper internal paths. Enforced via ESLint boundaries.
  • pages/ thin wrappers: no UI component or business logic — a dynamic import and, if needed, a getServerSideProps inline (per-page logic may stay inline; only shared/reusable SSR helpers move to nextjs/). SEO metadata lives in the slice/feature page folder.
  • Tx pilot PR strategy: migrate the tx slice end-to-end (UI, hooks, utils, types, mocks) while leaving cross-slice dependencies (address, rollup types, etc.) at their old lib/ paths. Every remaining old-path import in client/slices/tx/ after the pilot becomes an explicit migration task for the relevant slice/feature PR.

11. Open topics (round 4)

  • Path aliases — baseUrl: "." in tsconfig.json makes client/** absolute imports work without explicit paths entries. No action needed.
  • Public type surfaces — types/api.ts is the stable external surface per slice/feature; documented in §10. Cross-slice types/client.ts imports left as an open question to resolve with real examples during implementation.
  • Storybook / toolkit — project does not use Storybook. Topic closed.
  • lib/hooks/ full audit — all hooks explicitly placed; see §6 migration map.
  • lib/api/services/general/ audit — all files stay under general/ (General API namespace). No splits needed beyond existing file boundaries.
  • Project glossary — a GLOSSARY.md (or section in CONTRIBUTING) mapping internal codenames to their product/feature meaning (e.g. tac → Ton Application Chain, blobs/data-availability, cctx → cross-chain transactions, epochs → Celo epoch pages). Aids both engineers and agents navigating the codebase.

12. Example: rollup-specific field on tx page

  • Core tx UI stays in slices/tx.
  • Arbitrum-only (or other rollup) presentation lives in features/rollup/arbitrum/.
  • Compose at a high level (tx-details page or TxDetails section) so rollup-specific imports are localized and cycles remain unlikely.
  • Types: the Transaction type in slices/tx/types/api.ts imports feature-specific sub-types (e.g. ArbitrumTransactionData from features/rollup/arbitrum/types/api.ts) because the backend includes these fields in the response unconditionally when the rollup is enabled — the frontend type must reflect the full API contract. client/api then references Transaction via import type from the slice. UI component imports follow the same direction: TxDetails imports TxDetailsArbitrum.

File layout for this example

client/
  api/
    services/
      general/
        tx.ts
          └─ import type { Transaction } from 'client/slices/tx/types/api'
  slices/
    tx/
      pages/
        tx-details/
          TxDetails.tsx
            └─ import TxDetailsArbitrum from 'client/features/rollup/arbitrum/components/TxDetailsArbitrum'
      types/
        api.ts                         # owns the Transaction interface
          └─ import type { ArbitrumTransactionData } from 'client/features/rollup/arbitrum/types/api'
          export interface Transaction {
            // ... base tx fields ...
            arbitrum?: ArbitrumTransactionData
            optimism?: OptimismTransactionData
            scroll?: ScrollTransactionData
            // ... other rollup optional fields ...
          }
  features/
    rollup/
      arbitrum/
        types/
          api.ts                       # owns ArbitrumTransactionData
        components/
          TxDetailsArbitrum.tsx