Skip to content

Latest commit

 

History

History
350 lines (264 loc) · 19.9 KB

File metadata and controls

350 lines (264 loc) · 19.9 KB

Architecture Overview

Tech Stack

Category Technology Notes
Framework React 19 StrictMode enabled
Language TypeScript 6 (strict, ESM) Path aliases: @/src/*, @packageJSON
Blockchain wagmi 2 + viem 2 Type-safe Ethereum interaction
Data fetching TanStack Query 5, graphql-request Suspense-based contract reads
Routing TanStack Router File-based with auto code-splitting
UI Chakra UI 3 + Emotion Semantic tokens, light/dark mode
Testing Vitest 5 + Testing Library jsdom environment, colocated test files
Build Vite 8 + React plugin Manual chunk splitting for vendors
Linting Biome 2 Format + lint in one tool
Env validation Zod + @t3-oss/env-core All vars PUBLIC_ prefixed
Dead code knip Config in knip.json
Secret scanning gitleaks Pinned in .gitleaks-version, run by the git hooks and CI
Dependency updates Renovate Config in renovate.json, inert until the GitHub App is installed
Docs site vocs 2 Lives in docs/, built on Waku and React Server Components
API reference typedoc Output in typedoc/
Package manager pnpm 12 Settings and overrides in pnpm-workspace.yaml

Project Structure

src/
  components/
    pageComponents/       Page-level components (home/, demos/)
    sharedComponents/     Reusable UI + business components
    ui/                   Chakra provider, color mode, toaster
  hooks/                  Custom React hooks
    generated.ts          Auto-generated by wagmi-cli (do not edit)
  providers/              Web3Provider, TransactionNotificationProvider
  routes/                 TanStack Router file-based routes
    __root.tsx            Root layout (provider stack + shell)
  lib/
    networks.config.ts    Chain + transport configuration
    wagmi/                Wagmi CLI config + custom Suspense plugin
    wallets/              ConnectKit, RainbowKit, Web3Modal, Porto configs
  constants/
    contracts/            Contract ABIs + addresses per chain
    tokenLists.ts         Token list URLs
    common.ts             Shared constants (isDev, includeTestnets)
  types/                  TypeScript type definitions (Token via Zod)
  utils/                  Utility functions
  subgraphs/              GraphQL codegen config + queries
  env.ts                  Zod-validated environment variables
  main.tsx                Entry point
  routeTree.gen.ts        Auto-generated route tree (do not edit)

docs/
  pages/                  Documentation site content (mdx)
  public/                 Documentation site assets
  vocs.config.ts          Documentation site config, run from this directory
scripts/
  install-gitleaks.sh     Downloads the pinned gitleaks into bin/ (gitignored)
.github/workflows/        pr-assign, pr

No top-level barrel exports. Import directly from each file/folder path.

Key Abstractions

Component structure -- simple components are a single file; complex components use a folder:

ComponentName/
  index.tsx         main component and public API
  Components.tsx    sub-components (styled primitives, layout pieces)
  styles.ts         style objects consumed via Chakra's css prop
  useComponentName.ts  dedicated hook (optional)

HOC patterns:

  • withSuspenseAndRetry(Component) -- primary pattern for async components; wraps in QueryErrorResetBoundary + ErrorBoundary + Suspense for automatic retry on query failures
  • withSuspense(Component) -- simpler variant without retry

Contract hooks (auto-generated by wagmi-cli from ABIs):

  • useRead{Contract}{Function} -- standard read
  • useSuspenseRead{Contract}{Function} -- Suspense-enabled read (preferred)
  • useWrite{Contract}{Function} -- write/mutation
  • useSimulate{Contract}{Function} -- simulation before write

Contract lookup: getContract(name, chainId) from src/constants/contracts/contracts.ts returns typed { abi, address }. Validates address with isAddress(), throws with a helpful message if not found.

Utility functions (src/utils/):

  • formatNumber(value, type) -- user-facing number formatting with NumberType contexts (TokenTx, FiatTokenPrice, SwapPrice, PortfolioBalance)
  • getExplorerLink(chainId, hash, type) -- block explorer URLs per chain
  • truncateStringInTheMiddle(str), getTruncatedHash(hash) -- string display helpers
  • isNativeToken(address) -- checks against native token address
  • detectHash(str) -- identifies ENS names, tx hashes, contract addresses, EOAs

Data Access Layer

Four external data paths. Components never call external services directly -- they use hooks.

  1. Blockchain reads: useSuspenseRead{Contract}{Function} hooks (generated) -> wagmi readContract -> TanStack Query cache. All contract reads are Suspense-based via the custom reactSuspenseRead wagmi plugin in src/lib/wagmi/plugins/.

  2. Blockchain writes: TransactionButton calls transaction: () => Promise<Hash> prop -> wagmi writeContract -> useWaitForTransactionReceipt -> TransactionNotificationProvider toasts (loading, success/revert, explorer link).

  3. Subgraph queries: graphql-request with typed document nodes from @graphql-codegen -> TanStack Query cache. Queries live in src/subgraphs/queries/, generated types in src/subgraphs/gql/.

  4. Token data: LI.FI SDK (getChains -> getTokens -> getTokenBalances) via useTokens hook. Token lists fetched in parallel via useTokenLists (Suspense-based, source: CoinGecko, optional Uniswap default list). Tokens deduplicated by chainId + address, native tokens auto-injected per chain.

Routes

Route Module Description
/ src/components/pageComponents/home/ Landing page with welcome + feature demos
* src/components/pageComponents/NotFound404.tsx Catch-all 404 page

File-based routing via TanStack Router. Use .lazy.tsx extension for code-split lazy loading; plain .tsx only for routes that must eagerly load. Route tree auto-generates to src/routeTree.gen.ts via the TanStack Router Vite plugin.

Data Flow

External source                     Data layer                   State                       UI
───────────────                     ──────────                   ─────                       ──
EVM chains (RPC)              ───>  wagmi readContract     ───>  TanStack Query cache  ───>  Chakra components
EVM chains (write)            ───>  wagmi writeContract    ───>  TransactionContext     ───>  Toast notifications
The Graph subgraphs           ───>  graphql-request        ───>  TanStack Query cache  ───>  Chakra components
LI.FI API (prices/balances)   ───>  @lifi/sdk              ───>  TanStack Query cache  ───>  Chakra components
Token list URLs               ───>  fetch + Zod validate   ───>  TanStack Query cache  ───>  Chakra components

Caching strategy:

  • All async state lives in TanStack Query. Components do not hold async data in local state.
  • Token lists: staleTime: 1 hour, gcTime: 1 hour.
  • Token balances: refreshed every ~32s (BALANCE_EXPIRATION_TIME).
  • Contract reads: Suspense-based -- component suspends until data arrives, then cached normally.

Environment Variables

All variables use the PUBLIC_ prefix and are validated with Zod in src/env.ts. Access via import { env } from '@/src/env' -- never import.meta.env directly.

App

Variable Required Purpose
PUBLIC_APP_NAME yes Application name
PUBLIC_APP_DESCRIPTION no App description
PUBLIC_APP_URL no Canonical URL
PUBLIC_APP_LOGO no Logo URL
PUBLIC_ENABLE_PORTO no (default: true) Enable Porto wallet connector
PUBLIC_INCLUDE_TESTNETS no (default: true) Include testnet chains
PUBLIC_USE_DEFAULT_TOKENS no (default: true) Include Uniswap default token list

RPC endpoints (all optional -- wagmi falls back to public RPCs)

PUBLIC_RPC_MAINNET, PUBLIC_RPC_OPTIMISM, PUBLIC_RPC_POLYGON, PUBLIC_RPC_ARBITRUM, PUBLIC_RPC_BASE, PUBLIC_RPC_GNOSIS, PUBLIC_RPC_SEPOLIA, PUBLIC_RPC_OPTIMISM_SEPOLIA, PUBLIC_RPC_ARBITRUM_SEPOLIA, PUBLIC_RPC_BASE_SEPOLIA, PUBLIC_RPC_GNOSIS_CHIADO, PUBLIC_RPC_POLYGON_MUMBAI

Wallet / API

Variable Required Purpose
PUBLIC_WALLETCONNECT_PROJECT_ID no (default: '') WalletConnect project ID
PUBLIC_ALCHEMY_KEY no Alchemy RPC key
PUBLIC_INFURA_KEY no Infura RPC key
PUBLIC_NATIVE_TOKEN_ADDRESS no (default: 0x0...0) Native token sentinel address

Subgraph (all-or-nothing: set all or remove subgraph code)

Variable Required Purpose
PUBLIC_SUBGRAPHS_API_KEY yes The Graph API key
PUBLIC_SUBGRAPHS_CHAINS_RESOURCE_IDS yes chainId:subgraphId:resourceId comma-separated
PUBLIC_SUBGRAPHS_ENVIRONMENT no (default: production) development or production
PUBLIC_SUBGRAPHS_DEVELOPMENT_URL no Dev subgraph URL template
PUBLIC_SUBGRAPHS_PRODUCTION_URL no Production subgraph URL template

Scripts

Command Purpose
pnpm dev Vite dev server
pnpm dev:watch Vite dev server + TanStack Router route watching
pnpm typecheck tsc --noEmit
pnpm build pnpm typecheck + Vite production build
pnpm preview Serve the production build
pnpm test Vitest single run
pnpm test:watch Vitest watch mode
pnpm test:coverage Coverage report (v8)
pnpm lint Biome check
pnpm lint:fix Biome auto-fix
pnpm knip Report unreachable files, exports and dependencies
pnpm wagmi-generate Generate typed contract hooks from ABIs into src/hooks/generated.ts
pnpm routes:generate Generate TanStack Router route tree into src/routeTree.gen.ts
pnpm subgraph-codegen Generate GraphQL types from subgraph schemas into src/subgraphs/gql/
pnpm docs:dev Documentation site dev server (vocs, from docs/)
pnpm docs:build Build the documentation site into docs/dist/public
pnpm docs:preview Serve the built documentation site
pnpm typedoc:build Build the API reference into typedoc/

Dependency Resolution

pnpm-workspace.yaml carries the settings pnpm 12 no longer reads from package.json:

  • engineStrict: true -- refuse to install on a Node below engines.node
  • allowBuilds -- the only packages allowed to run install scripts
  • overrides -- versions forced across the whole tree

Three overrides and why they exist:

Package Held to Reason
@graphql-codegen/cli ^7 @bootnodedev/db-subgraph asks for ^5; without this we install two copies
@wagmi/core 2.22.1 @reown/appkit-adapter-wagmi and porto leave it an open peer, so a fresh resolve fills it with 3.x while wagmi 2 depends on 2.22.1
@wagmi/connectors 6.2.0 Same, and the 8.x build imports @wagmi/core/tempo, which 2.x does not export

Both wagmi pins are exact, matching what wagmi itself depends on, so the override dedupes the tree without widening the range wagmi chose. Renovate is told to leave them alone.

Majors held back in renovate.json, each with the reason in the config: wagmi (no connectkit or rainbowkit release for 3), graphql (db-subgraph asks for ^16), typescript (typedoc cannot run on the Go-native compiler).

CI

.github/workflows/pr.yml runs on every pull request:

Job Runs
biome pnpm lint
typecheck + build + knip subgraph codegen, pnpm build, pnpm knip, pnpm typedoc:build, pnpm docs:build
test pnpm test on the Node in .nvmrc and again on Node 26
commitlint the PR's commit range and its title
gitleaks the full git history

The typecheck job needs the subgraph types, which are generated from secrets, so it is skipped on pull requests from forks. Every action is pinned to a commit digest, and Renovate keeps the digests current.

pr-assign.yml assigns the author, skipping forks and bots.

Deployments

Three Vercel projects build from this repo, all with root directory ., so they share the root vercel.json and differ only in their install command, build command and output directory:

Project Install command Build command Output directory
demo.dappbooster pnpm install && pnpm wagmi-generate && pnpm subgraph-codegen pnpm build (default dist)
components.dappbooster pnpm install pnpm typedoc:build typedoc
docs.dappbooster pnpm install pnpm docs:build (see below) ignored, see below

Everything host-specific lives in those two fields, not in package.json, so the scripts stay the ones a developer runs.

The demo project names the two code generators in its install command rather than leaning on postinstall. Vercel restores a build cache, so pnpm install can report "Already up to date" and never run postinstall, and src/hooks/generated.ts is generated and gitignored, so it is in neither the repo nor the cache. The build then fails on Cannot find module '@/src/hooks/generated'. The other two projects do not need it: typedoc produces identical output with or without those hooks, and the docs site never touches src/.

When VERCEL is set, vocs builds through waku's Vercel adapter and writes a Build Output API directory instead of a plain folder of files. Vercel uses that in preference to the project's output directory setting, so the docs project's setting no longer matters.

The adapter writes it relative to the working directory, and docs:build runs from docs/, so the output lands in docs/.vercel/output while Vercel looks in .vercel/output. Moving it is the deploy host's problem, not the repo's, so it lives in the docs project's build command on Vercel rather than in package.json:

pnpm docs:build && if [ -d docs/.vercel/output ]; then mkdir -p .vercel && rm -rf .vercel/output && mv docs/.vercel/output .vercel/output; fi

Locally VERCEL is unset, the adapter never runs, and pnpm docs:build just writes docs/dist for pnpm docs:preview to serve.


Domain-Specific Sections

Number / Precision Handling

Token amounts are always bigint internally. Never use parseFloat, Number(), or .toFixed() on token values.

  • Input: BigNumberInput component wraps react-number-format + viem's parseUnits / formatUnits. The BigNumberInput is the only place raw user strings convert to bigint.
  • Display: formatNumber(value, type) from src/utils/numberFormat.ts with a NumberType context that sets decimal precision and formatting rules:
    • TokenTx -- token transaction amounts
    • FiatTokenPrice -- USD prices
    • SwapPrice -- swap rate display
    • PortfolioBalance -- portfolio totals
  • The bigint -> formatted string conversion only happens at the UI boundary.

Provider / Context Hierarchy

From src/routes/__root.tsx, outermost to innermost:

Chakra Provider              (theme system, color mode via next-themes, default: dark)
  Web3Provider
    WagmiProvider            (chains, transports, wallet connectors)
      QueryClientProvider    (TanStack Query client)
        WalletProvider       (ConnectKit wallet UI)
          TransactionNotificationProvider  (tx/signature toast context)
            Header
            Outlet           (page content)
            Footer
            Devtools         (React Query + Router devtools, dev only)
            Vercel Analytics
            Toaster

When adding a new provider: place it inside the outermost provider it depends on. Anything needing blockchain access goes inside Web3Provider. Anything needing tx notifications goes inside TransactionNotificationProvider.

Hook Patterns

Generated contract hooks (src/hooks/generated.ts, auto-generated by pnpm wagmi-generate):

  • useRead{Contract}{Function} -- standard read, manual refetch
  • useSuspenseRead{Contract}{Function} -- Suspense read (preferred; component suspends until resolved)
  • useWrite{Contract}{Function} -- write/mutation
  • useSimulate{Contract}{Function} -- simulate before write to surface errors early

Web3 connection state (src/hooks/useWeb3Status.tsx):

  • useWeb3Status() -- returns { readOnlyClient, appChainId, address, isWalletConnected, isWalletSynced, switchChain, disconnect, ... }

Token hooks:

  • useTokens() -- token list + LI.FI prices + account balances, sorted by balance value
  • useTokenLists() -- Suspense-based parallel fetch of all configured token list URLs
  • useTokenInput() -- manages selected token + amount state, fetches balance
  • useTokenSearch(tokens, query) -- filters by name or symbol

Suspense convention: all contract reads use Suspense. Wrap consuming components with withSuspenseAndRetry() to handle the loading and error states.

Error Handling Patterns

withSuspenseAndRetry(Component) (src/utils/suspenseWrapper.tsx) -- primary pattern:

  • Wraps in QueryErrorResetBoundary (TanStack Query) + ErrorBoundary + Suspense
  • On error: shows error UI with a retry button that resets the query and re-renders
  • Props: suspenseFallback, errorFallback, defaultFallbackFormat ('default' | 'dialog'), spinnerSize

withSuspense(Component) -- simpler variant when retry is not needed (no query resets).

defaultFallbackFormat: 'dialog' -- renders the error in a modal. Use for critical errors where inline display is disruptive.

Transaction errors -- handled by TransactionNotificationProvider. It catches rejected signatures, reverted transactions, and gas estimation failures, and surfaces them as toast notifications with user-friendly messages. Handles transaction replacement (gas bump) and cancellation by the wallet.

Smart Contract Integration

Contracts are registered centrally and consumed via generated hooks.

  1. ABIs: stored as as const typed exports in src/constants/contracts/abis/.
  2. Registration: src/constants/contracts/contracts.ts -- array typed as ContractConfig<Abi>[] with name, abi, and optional address: { [chainId]: '0x...' }.
  3. Runtime lookup: getContract(name, chainId) returns { abi, address } -- throws if contract or chain address not found.
  4. Hook generation: pnpm wagmi-generate reads the contracts array and writes src/hooks/generated.ts. The custom reactSuspenseRead plugin in src/lib/wagmi/plugins/reactSuspenseRead.ts adds Suspense variants for all read functions.

To add a new contract: save the ABI, add it to the contracts array, run pnpm wagmi-generate.

Wallet Access Control

WalletStatusVerifier component (src/components/sharedComponents/WalletStatusVerifier.tsx) -- renders a fallback cascade based on wallet state:

  1. Not connected -> ConnectWalletButton
  2. Connected but walletChainId !== targetChainId (where targetChainId is the optional chainId prop if provided, otherwise appChainId) -> "Switch to [Network]" button
  3. Connected + synced -> renders children

useWeb3StatusConnected() (same file) -- companion hook that provides typed connected-wallet status (address, readOnlyClient, etc.) inside the <WalletStatusVerifier> tree. Throws a DeveloperError if called outside it.

TransactionButton and SignButton use the useWalletStatus() hook directly for inline wallet state checks rather than wrapping with the WalletStatusVerifier component.

Sync check: isWalletSynced = isWalletConnected && walletChainId === appChainId.