| 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 |
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.
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 inQueryErrorResetBoundary+ErrorBoundary+Suspensefor automatic retry on query failureswithSuspense(Component)-- simpler variant without retrywithWalletStatusVerifier(Component)-- gates a component behind wallet connection + chain sync
Contract hooks (auto-generated by wagmi-cli from ABIs):
useRead{Contract}{Function}-- standard readuseSuspenseRead{Contract}{Function}-- Suspense-enabled read (preferred)useWrite{Contract}{Function}-- write/mutationuseSimulate{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 withNumberTypecontexts (TokenTx,FiatTokenPrice,SwapPrice,PortfolioBalance)getExplorerLink(chainId, hash, type)-- block explorer URLs per chaintruncateStringInTheMiddle(str),getTruncatedHash(hash)-- string display helpersisNativeToken(address)-- checks against native token addressdetectHash(str)-- identifies ENS names, tx hashes, contract addresses, EOAs
Four external data paths. Components never call external services directly -- they use hooks.
-
Blockchain reads:
useSuspenseRead{Contract}{Function}hooks (generated) -> wagmireadContract-> TanStack Query cache. All contract reads are Suspense-based via the customreactSuspenseReadwagmi plugin insrc/lib/wagmi/plugins/. -
Blockchain writes:
TransactionButtoncallstransaction: () => Promise<Hash>prop -> wagmiwriteContract->useWaitForTransactionReceipt->TransactionNotificationProvidertoasts (loading, success/revert, explorer link). -
Subgraph queries:
graphql-requestwith typed document nodes from@graphql-codegen-> TanStack Query cache. Queries live insrc/subgraphs/queries/, generated types insrc/subgraphs/gql/. -
Token data: LI.FI SDK (
getChains->getTokens->getTokenBalances) viauseTokenshook. Token lists fetched in parallel viauseTokenLists(Suspense-based, sources: 1INCH, CoinGecko, optional Uniswap default list). Tokens deduplicated bychainId + address, native tokens auto-injected per chain.
| 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.
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.
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 |
| 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) |
Token amounts are always bigint internally. Never use parseFloat, Number(), or .toFixed() on token values.
- Input:
BigNumberInputcomponent wrapsreact-number-format+ viem'sparseUnits/formatUnits. TheBigNumberInputis the only place raw user strings convert to bigint. - Display:
formatNumber(value, type)fromsrc/utils/numberFormat.tswith aNumberTypecontext that sets decimal precision and formatting rules:TokenTx-- token transaction amountsFiatTokenPrice-- USD pricesSwapPrice-- swap rate displayPortfolioBalance-- portfolio totals
- The bigint -> formatted string conversion only happens at the UI boundary.
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.
Generated contract hooks (src/hooks/generated.ts, auto-generated by pnpm wagmi-generate):
useRead{Contract}{Function}-- standard read, manual refetchuseSuspenseRead{Contract}{Function}-- Suspense read (preferred; component suspends until resolved)useWrite{Contract}{Function}-- write/mutationuseSimulate{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 bywithWalletStatusVerifier
Token hooks:
useTokens()-- token list + LI.FI prices + account balances, sorted by balance valueuseTokenLists()-- Suspense-based parallel fetch of all configured token list URLsuseTokenInput()-- manages selected token + amount state, fetches balanceuseTokenSearch(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.
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.
Contracts are registered centrally and consumed via generated hooks.
- ABIs: stored as
as consttyped exports insrc/constants/contracts/abis/. - Registration:
src/constants/contracts/contracts.ts-- array typed asContractConfig<Abi>[]withname,abi, and optionaladdress: { [chainId]: '0x...' }. - Runtime lookup:
getContract(name, chainId)returns{ abi, address }-- throws if contract or chain address not found. - Hook generation:
pnpm wagmi-generatereads the contracts array and writessrc/hooks/generated.ts. The customreactSuspenseReadplugin insrc/lib/wagmi/plugins/reactSuspenseRead.tsadds Suspense variants for all read functions.
To add a new contract: save the ABI, add it to the contracts array, run pnpm wagmi-generate.
WalletStatusVerifier component (src/components/sharedComponents/WalletStatusVerifier.tsx) -- renders a fallback cascade based on wallet state:
- Not connected ->
ConnectWalletButton - Connected but
walletChainId !== appChainId-> "Switch to [Network]" button - 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.