* 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]>
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/(andui/) as unscoped buckets — their contents move under/clientor 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/apimust not import runtime logic fromclient/slices/*orclient/features/*(avoids cycles).import typefrom slices is permitted when an API service file maps a resource to its response type — the type lives with the slice that owns it, andclient/apireferences 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;txcomposes internal-tx components (e.g. tab content). Prefer no imports frominternal-txback intotxto avoid cycles; share viaclient/sharedor 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, …
statsandgas-trackerare features (not slices) — they are config-gated per chain.validatorsis a feature — chain-specific, not present on a vanilla EVM chain.epochsis a sub-concern of the Celo chain variant (features/chain-variants/celo/), not a standalone feature.data-availabilitywas previously referred to asblobs— usedata-availabilitythroughout.
Classification rules:
- "Can this area exist on a vanilla EVM chain with no feature flag?" Yes → slice. No → feature.
- Config-gated infrastructure with no user-facing UI (analytics providers, error monitoring, A/B flag providers) lives in
client/shared/, notfeatures/— the presence of a config flag alone is not sufficient to qualify for a feature folder. - 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.tsbarrel 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
/configsnever 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.
/clientimports/configsfor 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 inconfigs/.
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.tsxalongside components), same as today.
8. Dependency rules (high level)
- ESLint rules (
import/no-cycle+ explicitboundariesrules forclient/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/; legacylib/andui/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/apiallowsimport typefrom 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.TxDetailsimportsTxDetailsArbitrum). client/shared→ sliceimport typeis acceptable where shared infrastructure genuinely needs a slice-owned type (e.g.client/shared/metadata/referencingTokenInfofromslices/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: avoidtx→internal-tx→txcycles; lift shared bits tosharedor 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 tokebab-casein 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
- Pre-migration PR: rename
configs/app/features/files to kebab-case. - Pre-migration PR: enable ESLint
import/no-cycle+boundariesrules; fix existing violations. - Bottom-up migration: start with
client/apiandclient/shared(lowest in the dependency graph, most imported). Establishes the foundation without touching any UI. - Pilot slice —
tx: migratelib/tx/, thetxAPI service,ui/tx/**, hooks, types, mocks end-to-end. Use the result as the canonical template for all subsequent slices. - Remaining slices in parallel (one PR per slice).
- Features in parallel (one PR per feature or logical group).
client/shell— migrate after slices/features it depends on are in place.- 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 insideclient/. types/api.tsis 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 ESLintboundaries.pages/thin wrappers: no UI component or business logic — a dynamic import and, if needed, agetServerSidePropsinline (per-page logic may stay inline; only shared/reusable SSR helpers move tonextjs/). 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 inclient/slices/tx/after the pilot becomes an explicit migration task for the relevant slice/feature PR.
11. Open topics (round 4)
Path aliases—baseUrl: "."intsconfig.jsonmakesclient/**absolute imports work without explicitpathsentries. No action needed.Public type surfaces—types/api.tsis the stable external surface per slice/feature; documented in §10. Cross-slicetypes/client.tsimports left as an open question to resolve with real examples during implementation.Storybook / toolkit— project does not use Storybook. Topic closed.— all hooks explicitly placed; see §6 migration map.lib/hooks/full audit— all files stay underlib/api/services/general/auditgeneral/(General API namespace). No splits needed beyond existing file boundaries.Project glossary— aGLOSSARY.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-detailspage orTxDetailssection) so rollup-specific imports are localized and cycles remain unlikely. - Types: the
Transactiontype inslices/tx/types/api.tsimports feature-specific sub-types (e.g.ArbitrumTransactionDatafromfeatures/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/apithen referencesTransactionviaimport typefrom the slice. UI component imports follow the same direction:TxDetailsimportsTxDetailsArbitrum.
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