| 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 |
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.
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 retry
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, source: 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: 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.
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 |
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/ |
pnpm-workspace.yaml carries the settings pnpm 12 no longer reads from package.json:
engineStrict: true-- refuse to install on a Node belowengines.nodeallowBuilds-- the only packages allowed to run install scriptsoverrides-- 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).
.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.
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.
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, ... }
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 !== targetChainId(wheretargetChainIdis the optionalchainIdprop if provided, otherwiseappChainId) -> "Switch to [Network]" button - 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.