Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions apps/docs/content/developers/architecture.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
title: SO4 architecture
description: The monorepo map, data flow from contracts to screen, state locations, and repository invariants explained.
updated: 2026-08-25
status: stable
---

SO4 is a trading platform built on Soroban smart contracts. Understanding how data flows from the contract layer through to the web app helps you navigate both for changes and for integration.

## Workspace layout

The monorepo uses Turborepo with workspaces declared in `package.json`. Each workspace owns one concern.

| Path | Purpose |
| ------------------------ | -------------------------------------------------------- |
| `apps/web` | React + Vite web app for traders, liquidity providers |
| `apps/s03-indexer` | Node.js service that syncs contract state to a database |
| `apps/docs` | Documentation site, MDX + React |
| `packages/contracts` | TypeScript contract bindings and client layer |
| `packages/ui` | Shared React component library: buttons, forms, tables |
| `packages/vitest-config` | Shared Vitest configuration for unit tests |

Most trading logic lives in `apps/web`. The indexer watches contracts and makes historical state queryable. Design tokens and UI components ship from `packages/ui` to avoid duplication.

## Data flow: contracts to screen

Market data, position state, and order history flow through four stops:

```
Soroban contracts (on-chain state)
RPC nodes (live reads)
s03-indexer (database sync)
web app (TanStack Query + Zustand)
trader screen
```

### 1. Contracts

SO4 contracts live on Soroban and hold the authoritative market and position state:

- **ExchangeRouter:** Entry point for all state-changing actions (open position, place order, close position).
- **SyntheticsReader:** Market snapshots: open interest, prices, funding rates, oracle state.
- **DataStore:** Persistent market and position data (pools, collateral, leverage).
- **OrderVault:** Holds collateral until the order is filled or cancelled.

Each contract enforces access control, validates amounts, and raises errors for invalid operations. Bindings for these contracts are committed in `packages/contracts/src/generated/` and hand-adapted in `src/clients/`.

### 2. RPC nodes

Soroban RPC is the live read path. `packages/contracts` exports clients for read-only calls (e.g., fetching the current market state). These simulation-based reads require no transaction or account, only the network URL and contract IDs from `contracts.json`.

### 3. Indexer

The indexer is a separate Node.js service (`apps/s03-indexer`) that:

1. Watches contract events from Stellar for changes (positions opened, orders filled, liquidations).
2. Syncs state into a local database.
3. Exposes a GraphQL API for historical queries.

The indexer is optional for read-only integrations (you can call RPC directly), but essential for the web app's order history, position tracking, and liquidation feeds.

Running the indexer locally:

```bash
bun run --cwd apps/s03-indexer dev
```

It syncs from Stellar every N blocks and emits GraphQL types (see `apps/s03-indexer/codegen`).

### 4. Web app state

The web app holds three types of state:

| Layer | Purpose | Lifespan | Write |
| -------------------------- | ------------------------------------- | ------------------ | -------------------------------------------------- |
| TanStack Query (server) | Cached contract reads, indexer data | Configurable TTL | Refetch on interval, manual invalidate on change |
| Zustand store (UI) | Trading form, UI toggles, alerts | Session | User interaction (form inputs), UI events |
| localStorage | Theme preference, previous orders | Browser storage | Set on preference change, trade submission |

TanStack Query manages freshness: it refetches market data every 3 seconds and position state every 5 seconds. The Zustand store is lightweight—it holds form state and UI flags, not derived calculations. localStorage is only for UI preferences that should persist across browser sessions.

## Where state lives

Different concerns own different state:

| State | Owner | Immutable after creation? | Edited by |
| ---------------------------- | ----------------------- | ------------------------- | --------------------------------------------- |
| Contract IDs, network config | `contracts.json` | Yes (on re-deploy) | Contract deployment script, committed to repo |
| Market data (prices, OI) | Contracts + RPC | Per block | Soroban runtime, oracle updates |
| Positions, orders | Contracts + indexer DB | Append-only after event | User transactions, liquidation bot |
| Portfolio, P&L calculations | Web app (Query + Zustand) | Per calculation | User interactions, data refetches |
| Form state (order size, etc) | Zustand | Per input | Keyboard/mouse input |
| Visual preferences | localStorage | Per session | User toggle (theme, layout) |

The rule: on-chain data is source of truth. The web app is a view layer that reads and writes that state.

## Repository invariants

These four principles are not negotiable and root every architectural decision:

### 1. Generated bindings are hand-adapted

Run `stellar contract bindings typescript` and the output uses snake_case field names and lacks helper types. The committed bindings in `packages/contracts/src/generated/` have been manually edited to use camelCase and to export `*Args` and `*Val` types that `src/clients/` imports.

**Do not regenerate bindings casually.** Regenerating with `bun run contracts:gen:all` overwrites the hand-written adaptations and breaks the build until you reapply them or update `src/clients/`.

If you must regenerate (e.g., after a contract upgrade), commit the changes and immediately apply the adaptations so the break is visible in CI.

### 2. Every dependency must be declared

Bun's isolated linker means undeclared dependencies fail silently in development and loudly in CI. Always run `bun install --frozen-lockfile` after editing `package.json` and commit `bun.lock`.

### 3. Indexer codegen must run before build

The indexer generates TypeScript types from the contract ABI and the GraphQL schema. Run `bun run --cwd apps/s03-indexer codegen` before building `apps/web` or any app that imports indexer types.

The build will error if codegen is stale:

```
error: could not find a matching export in '/indexer/src/generated'
```

### 4. No design-token hardcoding

Design tokens live in `packages/ui/tokens.json` and are generated into CSS, TypeScript, and JSON at build time. Never hardcode color values, spacing, or typography—import from `@workspace/ui/tokens` instead.

Running `bun run check:tokens` validates that no hardcoded values sneak in. This check is part of the commit gate.

## Development workflow

Typical flows:

1. **Edit contract bindings:** Regenerate with `bun run contracts:gen:all`, reapply hand-adaptations in `src/generated/`, commit.
2. **Change contract IDs:** Update `packages/contracts/contracts.json`, run `bun run contracts:gen:all`, commit. CI rebuilds both the indexer and the web app.
3. **Add an indexer type:** Edit the contract ABI or GraphQL schema, run `bun run --cwd apps/s03-indexer codegen`, then use the new type in `apps/web`.
4. **Add a UI component:** Create it in `packages/ui`, export from `index.ts`, use in `apps/web`. No separate publish step—Turbo links workspaces.

Every change runs through the commit gate (lint, typecheck, test, build) before it can be committed. See [Local setup](/developers/local-setup) for how to run it.
177 changes: 177 additions & 0 deletions apps/docs/content/developers/local-setup.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
---
title: Local setup
description: Clone the repo, install dependencies, run the dev server, and understand the commit gate.
updated: 2026-08-25
status: stable
---

You can run the full SO4 stack locally: the indexer, web app, and all tooling. Everything after a fresh clone requires only `bun install --frozen-lockfile`, a dev server, and understanding the pre-commit checks.

## Prerequisites

**Node.js and Bun:**
- Node.js ≥ 20 (from `package.json` `engines`)
- Bun 1.3.13 (from `package.json` `packageManager`)

Install Bun from [bun.sh](https://bun.sh). Verify your versions:

```bash
node --version # v20.x or later
bun --version # v1.3.13 or later
```

**Optional: Stellar CLI** (for regenerating contract bindings)

The indexer codegen and contract binding generation both use the Stellar CLI. If you plan to regenerate bindings or run indexer codegen locally:

```bash
cargo install stellar-cli --version 22.2.1
stellar --version # stellar-cli 22.2.1
```

Without it, development works fine using committed bindings and generated types. You only need it if you're regenerating after a contract upgrade.

## Clone and install

Clone the repository:

```bash
git clone https://github.com/SO4-Markets/interface.git
cd interface
```

Install all dependencies with the frozen lockfile (no updates):

```bash
bun install --frozen-lockfile
```

This installs workspaces for all apps and packages. Turbo caches dependencies per workspace, so installing is usually a few seconds even for a large monorepo.

## Running the dev server

Each workspace has its own dev server. Start them in separate terminals:

### Web app

```bash
bun run --cwd apps/web dev
```

This starts Vite on http://localhost:5173 with hot reload. The web app reads contract IDs from `apps/web/.env.testnet`.

### Indexer (optional, but recommended)

```bash
bun run --cwd apps/s03-indexer dev
```

The indexer syncs contract events from Stellar testnet and serves GraphQL on http://localhost:4000/graphql. The web app prefers indexer data for historical queries; RPC-only reads work but are slower.

The indexer requires a PostgreSQL database locally. If you don't have one running, the indexer will error but the web app still works (it falls back to RPC-only reads).

### Documentation site

```bash
bun run --cwd apps/docs dev
```

Docs site runs on http://localhost:3000 with live reload. This is optional—docs are not required for trading locally.

## The commit gate

Before opening a pull request, every commit must pass the commit gate: a sequence of checks that validate correctness, type safety, and test coverage. Run it from the repository root:

```bash
bun lint # zero errors
bun typecheck # zero errors
bun run check:tokens # zero violations
bun run test # all pass
bun run test:coverage # all pass, thresholds met
bun run build # succeeds
```

If your change touches `apps/s03-indexer` or `apps/web`, also run integration checks:

```bash
bun run --cwd apps/s03-indexer codegen
bun run --cwd apps/s03-indexer build
bun run --cwd apps/s03-indexer test
SO4_CONTRACTS_REPO="$PWD/apps/s03-indexer/tests/fixtures/contracts-repo" \
bun run --cwd apps/s03-indexer sync:contracts:local
bash scripts/validate-manifest.sh apps/s03-indexer/config/contracts.local.json
bun run --cwd apps/web typecheck
bun run --cwd apps/web build
```

**Key rule: run the whole gate, not a subset.** Even if you only changed a comment, run all commands. Turbo caches aggressively, so the full run is usually just a few seconds.

The gate exists to catch:

- Lint errors (code style)
- Type errors (TypeScript safety)
- Design token violations (hardcoded colors, spacing)
- Broken tests (functionality)
- Coverage gaps (regression risk)
- Build failures (dead code, missing types)

Your PR will not merge until CI passes the gate.

## Common failure modes

### "Undeclared dependency" in Bun

Bun's isolated linker is stricter than npm's. If a package is not explicitly declared in your `package.json`, Bun will not find it at runtime, even if npm would.

**Fix:** Add it to the workspace that uses it:

```bash
cd apps/web
bun add some-package
```

Then commit `bun.lock` and re-run the gate.

### "Type not found" after changing contracts

If the indexer or contract bindings changed, generated types are stale. Regenerate them:

```bash
bun run --cwd apps/s03-indexer codegen
```

Then re-run `bun typecheck`.

### Bindings regeneration breaks the build

If you ran `bun run contracts:gen:all` and now the build fails, the hand-written adaptations (camelCase fields, helper types) were overwritten.

**Two options:**

1. Revert the regeneration and update the contract IDs only in `contracts.json`, letting CI regenerate with adaptations applied.
2. Manually reapply the camelCase and helper-type changes to `packages/contracts/src/generated/`.

See [Architecture: Repository invariants](/developers/architecture#repository-invariants) for why this happens.

### Clean-room verification

If you added or upgraded a dependency, run a clean rebuild to ensure nothing was accidentally broken:

```bash
rm -rf node_modules bun.lock
bun install --frozen-lockfile
bun run build
```

This catches cases where a new dependency silently shadows an old one or changes how the build resolves modules.

## Next steps

Once the dev server is running locally:

- Read [Architecture](/developers/architecture) to understand the data flow.
- Check [Contract clients](/developers/contract-clients) to call contracts from Node or the browser.
- See [Writing transactions](/developers/writing-transactions) to submit a trade.
- Browse [Reading data](/developers/reading-data) for indexer queries.

Questions? Run the commit gate—it will tell you exactly what failed and why.
4 changes: 3 additions & 1 deletion apps/docs/content/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
{
"label": "Developers",
"pages": [
"developers/architecture",
"developers/local-setup",
"developers/indexer",
"developers/contract-clients",
"developers/reading-data",
Expand All @@ -17,7 +19,7 @@
"concepts/liquidation"
]
},
{ "label": "Reference", "pages": ["reference/glossary"] },
{ "label": "Reference", "pages": ["reference/contracts.generated", "reference/exchange-router", "reference/glossary"] },
{
"label": "Resources",
"pages": [
Expand Down
35 changes: 35 additions & 0 deletions apps/docs/content/reference/contracts.generated.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Contract addresses
description: Deployed SO4 contract IDs per network and their roles.
updated: 2026-08-25
status: stable
---

SO4 contract IDs are source of truth and committed in [`packages/contracts/contracts.json`](https://github.com/SO4-Markets/interface/blob/main/packages/contracts/contracts.json). This page is generated on every build; see [Architecture](/developers/architecture) for why.

## Testnet

| Contract | ID | Explorer | Role |
|----------|-------|----------|------|
| `exchange-router` | <ContractAddress id="CBD6BQSQFROWIIT5QCYN7KL5LJJWUIH7CEWUSZIFMUJO6NPXE6CVGYNW" /> | [explorer](https://stellar.expert/explorer/testnet/contract/CBD6BQSQFROWIIT5QCYN7KL5LJJWUIH7CEWUSZIFMUJO6NPXE6CVGYNW) | Entry point for all trading operations: opening and closing positions, placing and cancelling orders. |
| `synthetics-reader` | <ContractAddress id="CC6OZUHF3LVO6PNP3V2EB36ORB3YSVYSH3LWD3RFLO4NUO3BYCXSWSYC" /> | [explorer](https://stellar.expert/explorer/testnet/contract/CC6OZUHF3LVO6PNP3V2EB36ORB3YSVYSH3LWD3RFLO4NUO3BYCXSWSYC) | Market snapshots including prices, open interest, funding rates, and oracle state. |
| `test-faucet` | <ContractAddress id="CCWXXBKXHHP5DXC6TYVIL22XUNHD5A75O6WM5D2KM5PY45IOV5VDMARJ" /> | [explorer](https://stellar.expert/explorer/testnet/contract/CCWXXBKXHHP5DXC6TYVIL22XUNHD5A75O6WM5D2KM5PY45IOV5VDMARJ) | Testnet utility to distribute test tokens to new traders. |
| `test-token` | <ContractAddress id="CBAN5YU3KRDKPTQ2H76D6S7HQFPRBGUD524F65BUM2RQCITPTRLKWKES" /> | [explorer](https://stellar.expert/explorer/testnet/contract/CBAN5YU3KRDKPTQ2H76D6S7HQFPRBGUD524F65BUM2RQCITPTRLKWKES) | Testnet representative Stellar asset (TUSDC); all test token contracts share this interface. |
| `glv-router` | Not deployed | — | Generalized liquidity vault for multi-pool liquidity provision and risk management. |

Use these addresses to:
- Call contracts from [TypeScript](/developers/contract-clients)
- Configure the indexer (see [`apps/s03-indexer/config/contracts.testnet.json`](https://github.com/SO4-Markets/interface/tree/main/apps/s03-indexer/config))
- Submit transactions with [Freighter](https://www.freighter.app/)

## `<ContractAddress` component

Copy a contract ID with one click:

```jsx
import { ContractAddress } from "@/components/docs"

<ContractAddress id="CBD6BQSQFROWIIT5QCYN7KL5LJJWUIH7CEWUSZIFMUJO6NPXE6CVGYNW" />
```

Renders a copiable link to the contract on Stellar Expert.
Loading
Loading