Skip to content

Latest commit

 

History

History
269 lines (206 loc) · 14.7 KB

File metadata and controls

269 lines (206 loc) · 14.7 KB

Architecture Overview

Tech Stack

Category Technology Notes
Framework React 19 StrictMode enabled
Language TypeScript 5 (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 + Testing Library jsdom environment, colocated test files
Build Vite 6 + SWC Manual chunk splitting for vendors
Linting Biome Format + lint in one tool
Env validation Zod + @t3-oss/env-core All vars PUBLIC_ prefixed

Project Structure

src/
  components/
    pageComponents/       Page-level components (home/, demos/)
    sharedComponents/     Reusable UI + business components
    ui/                   Chakra provider, color mode, toaster, tooltip
  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)

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
  • withWalletStatusVerifier(Component) -- gates a component behind wallet connection + chain sync

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, sources: 1INCH, 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: Infinity, gcTime: Infinity (fetched once per session).
  • 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 + TanStack Router route watching
pnpm build tsc --noEmit + Vite 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 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)

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, ... }
  • useWeb3StatusConnected() -- same but throws if wallet is not connected; use inside components that are already gated by withWalletStatusVerifier

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 !== appChainId -> "Switch to [Network]" button
  3. Connected + synced -> renders children

withWalletStatusVerifier(Component) HOC -- wraps the component with the above logic. Already applied to TransactionButton and SignButton -- do not double-wrap.

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