|
| 1 | +# ARCHITECTURE — jumper-exchange |
| 2 | + |
| 3 | +Shape of this Next.js app. See [AGENTS.md](./AGENTS.md) for how to work in this repo (run, test, conventions, where new things go). |
| 4 | + |
| 5 | +## What this app is |
| 6 | + |
| 7 | +The B2C frontend for [jumper.xyz](https://jumper.xyz). Users land here to bridge and swap between any tokens across any chains (EVM, SVM, Sui, Bitcoin, Tron). Retention features (earn, portfolio, quests, campaigns, missions) are layered on top of the core swap flow. |
| 8 | + |
| 9 | +The app embeds the `@lifi/widget` for the swap UX and wraps it with Jumper-specific surfaces (auth, profile, XP, content, partner themes, …). |
| 10 | + |
| 11 | +## Stack |
| 12 | + |
| 13 | +| Layer | Choice | |
| 14 | +| --------------- | --------------------------------------------------------------------------------------------------- | |
| 15 | +| Framework | Next.js 16 (App Router) on React 19 with the React Compiler | |
| 16 | +| Styling | MUI v9 + Emotion | |
| 17 | +| Server state | `@tanstack/react-query` | |
| 18 | +| Client state | `zustand` (one store per feature, under `src/stores/<feature>/`) | |
| 19 | +| Forms | `@tanstack/react-form` | |
| 20 | +| i18n | `next-i18n-router` + `i18next`, locale segment in the URL (`/[lng]/…`) | |
| 21 | +| Wallet | `@lifi/sdk` + `@lifi/widget` + per-chain providers (EVM via Wagmi/Viem, Solana, Sui, Bitcoin, Tron) | |
| 22 | +| Observability | Sentry (browser + server + edge) | |
| 23 | +| Tests | Playwright (E2E), Vitest (unit, snapshot, Storybook) | |
| 24 | +| Package manager | pnpm (`packageManager` field pins the version) | |
| 25 | + |
| 26 | +## Directory layout |
| 27 | + |
| 28 | +``` |
| 29 | +jumper-exchange/ |
| 30 | +├── src/ |
| 31 | +│ ├── app/ # Next.js App Router |
| 32 | +│ │ ├── [lng]/ # all user-facing pages — locale-prefixed |
| 33 | +│ │ │ ├── (main)/ # route group: landing + main shell |
| 34 | +│ │ │ ├── (infos)/ # route group: legal / informational pages |
| 35 | +│ │ │ ├── bridge/, swap/ # core swap surfaces |
| 36 | +│ │ │ ├── earn/, portfolio/, quests/, missions/, campaign/, zap/, scan/, onboard/ |
| 37 | +│ │ │ ├── error-preview/, meta/ |
| 38 | +│ │ │ ├── error.tsx, layout.tsx |
| 39 | +│ │ ├── api/ # Next.js route handlers (thin proxies to jumper-backend) |
| 40 | +│ │ ├── lib/, ui/ # framework-level helpers |
| 41 | +│ │ ├── layout.tsx, global.css, global-error.tsx, not-found.tsx, robots.ts, sitemap.xml/ |
| 42 | +│ ├── components/ # feature components (one folder per feature) |
| 43 | +│ │ ├── core/, composite/, headless/ # primitives by composition level |
| 44 | +│ │ ├── <FeatureName>/ # e.g. EarnDetails, ConnectButton, Cards, … |
| 45 | +│ ├── providers/ # provider tree: WalletProvider, ThemeProvider, |
| 46 | +│ │ # ReactQueryProvider, TranslationProvider, … |
| 47 | +│ ├── stores/<feature>/ # zustand stores (one folder per feature) |
| 48 | +│ ├── hooks/<feature>/ # react-query hooks + feature hooks |
| 49 | +│ ├── config/ # runtime config (env, wallet connectors, widget) |
| 50 | +│ ├── i18n/translations/<lng>/ # translation JSON; resources.d.ts is generated |
| 51 | +│ ├── theme/ # MUI theme + design tokens |
| 52 | +│ ├── const/, types/, utils/, fonts/, stories/ |
| 53 | +│ ├── Layout.tsx, proxy.ts |
| 54 | +├── tests/ # Playwright E2E + page objects + test data |
| 55 | +├── public/ # static assets |
| 56 | +├── .storybook/ # Storybook config |
| 57 | +├── instrumentation.ts, instrumentation-client.ts, sentry.*.config.ts |
| 58 | +├── next.config.mjs, vitest.config.ts, playwright.config.ts, eslint.config.mjs |
| 59 | +├── gen-api.sh # regenerates the typed API client from jumper-backend's Swagger |
| 60 | +``` |
| 61 | + |
| 62 | +## Request and data flow |
| 63 | + |
| 64 | +``` |
| 65 | + ┌─────────────────────────────────────────────────────────┐ |
| 66 | + │ Browser (Next.js client) │ |
| 67 | + │ ┌───────────────────────────────────────────────────┐ │ |
| 68 | + │ │ React tree (App Router) │ │ |
| 69 | + │ │ • providers/ wires query, wallet, theme, i18n │ │ |
| 70 | + │ │ • components/ render features │ │ |
| 71 | + │ │ • stores/ hold client state (zustand) │ │ |
| 72 | + │ │ • hooks/ wrap react-query / wallet calls │ │ |
| 73 | + │ └─────────────┬─────────────────────────────────────┘ │ |
| 74 | + └────────────────┼────────────────────────────────────────┘ |
| 75 | + │ |
| 76 | + ┌───────────────┼─────────────────┐ |
| 77 | + │ │ │ |
| 78 | + ▼ ▼ ▼ |
| 79 | + Next.js API jumper-backend LI.FI SDK |
| 80 | + routes (REST, (chains, quotes, |
| 81 | + (src/app/api/) primary) executions) |
| 82 | + │ |
| 83 | + ▼ |
| 84 | + jumper-backend (proxied) |
| 85 | + ┌─────────────────┐ |
| 86 | + │ strapi-cms │ ← legacy direct paths |
| 87 | + │ (REST) │ (content + campaigns) |
| 88 | + └─────────────────┘ |
| 89 | +``` |
| 90 | + |
| 91 | +- **Server state** (chains, tokens, quotes, profile, campaigns, …) is fetched through react-query hooks under `src/hooks/`. Most go through `jumper-backend` either directly or via a Next.js route handler under `src/app/api/`. A few legacy hooks still call `strapi-cms` directly. |
| 92 | +- **Client state** (selected chain/token, route choice, settings, theme, modals, …) lives in zustand stores under `src/stores/<feature>/`. Stores never call APIs — they hold UI state and derived selectors. |
| 93 | +- **The swap engine** is `@lifi/widget`. We embed it inside our pages and configure it via `src/config/widgetConfig.ts`. Wallet connectors are wired in `src/providers/WalletProvider/` so the widget sees them. |
| 94 | +- **Sentry** is initialised once in `instrumentation*.ts` / `sentry.*.config.ts`. Feature code uses helpers — never `Sentry.init` directly. |
| 95 | + |
| 96 | +## Internal dependency rules |
| 97 | + |
| 98 | +| From → To | components | hooks | stores | providers | config | app/ | utils | |
| 99 | +| -------------- | ---------- | ----- | ------ | ------------------- | ------ | ---- | ----- | |
| 100 | +| **components** | ✓ | ✓ | ✓ | ✓ (consume context) | ✓ | ✗ | ✓ | |
| 101 | +| **hooks** | ✗ | ✓ | ✓ | ✓ (consume context) | ✓ | ✗ | ✓ | |
| 102 | +| **stores** | ✗ | ✗ | ✓ | ✗ | ✓ | ✗ | ✓ | |
| 103 | +| **providers** | ✓ (render) | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | |
| 104 | +| **config** | ✗ | ✗ | ✗ | ✗ | ✓ | ✗ | ✓ | |
| 105 | +| **app/** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| 106 | +| **utils** | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | |
| 107 | + |
| 108 | +Read as: a row module _may_ import from a column module where ✓; _must not_ where ✗. |
| 109 | + |
| 110 | +Contribution Rules: |
| 111 | + |
| 112 | +- **`utils/` is a leaf.** It must not import from anything else inside `src/`. Pure helpers only. |
| 113 | +- **`stores/` does not import from `components/`, `hooks/`, or `providers/`.** Stores must be renderable in isolation (and unit-testable) without pulling in the whole tree. |
| 114 | +- **`hooks/` does not import from `components/`.** Hooks describe data; components consume them. |
| 115 | +- **Nothing inside `src/` imports from `src/app/`.** App routes are leaves of the dependency graph — they compose everything else. |
| 116 | +- **No barrel `index.ts` files.** Import from source files directly. This keeps the dependency graph honest and tree-shakeable. |
| 117 | + |
| 118 | +If a change requires reversing one of these directions, **stop**. The right shape is usually to move the shared code down a level (often into `utils/` or `config/`) rather than to invert the arrow. |
| 119 | + |
| 120 | +## Invariants |
| 121 | + |
| 122 | +| # | Invariant | Enforcement | |
| 123 | +| --- | ------------------------------------------------------------------------------ | ---------------------- | |
| 124 | +| 1 | All user-facing pages live under `src/app/[lng]/` | doc; obvious on review | |
| 125 | +| 2 | API route handlers in `src/app/api/` are thin proxies — no business logic | doc | |
| 126 | +| 3 | Server state goes through react-query; no direct `fetch` in components | doc | |
| 127 | +| 4 | One zustand store per feature folder under `src/stores/` | doc | |
| 128 | +| 5 | No barrel files (`index.ts` re-exports) | doc; ESLint candidate | |
| 129 | +| 6 | Sentry is initialised only in `instrumentation*.ts` / `sentry.*.config.ts` | doc | |
| 130 | +| 7 | Secrets never committed; new env vars documented in `src/config/env-config.ts` | per-repo CI (existing) | |
| 131 | +| 8 | Pre-commit hook (`tsc --noEmit` + ESLint + Prettier) must pass | Husky + lint-staged | |
| 132 | + |
| 133 | +## Cross-repo position |
| 134 | + |
| 135 | +- This app depends on `jumper-backend` (primary, via REST) and on `strapi-cms` (legacy direct paths, via REST). |
| 136 | +- The TypeScript types of the backend API are vendored here, regenerated from `jumper-backend`'s Swagger via `pnpm api` (which calls `gen-api.sh`). Never hand-edit the generated file — change the upstream Swagger and regenerate. |
0 commit comments