diff --git a/apps/docs/content/developers/architecture.mdx b/apps/docs/content/developers/architecture.mdx new file mode 100644 index 00000000..c032bac4 --- /dev/null +++ b/apps/docs/content/developers/architecture.mdx @@ -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. diff --git a/apps/docs/content/developers/local-setup.mdx b/apps/docs/content/developers/local-setup.mdx new file mode 100644 index 00000000..af24e112 --- /dev/null +++ b/apps/docs/content/developers/local-setup.mdx @@ -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. diff --git a/apps/docs/content/meta.json b/apps/docs/content/meta.json index d8f22a44..ddce92a0 100644 --- a/apps/docs/content/meta.json +++ b/apps/docs/content/meta.json @@ -3,6 +3,8 @@ { "label": "Developers", "pages": [ + "developers/architecture", + "developers/local-setup", "developers/indexer", "developers/contract-clients", "developers/reading-data", @@ -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": [ diff --git a/apps/docs/content/reference/contracts.generated.mdx b/apps/docs/content/reference/contracts.generated.mdx new file mode 100644 index 00000000..2157e40e --- /dev/null +++ b/apps/docs/content/reference/contracts.generated.mdx @@ -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` | | [explorer](https://stellar.expert/explorer/testnet/contract/CBD6BQSQFROWIIT5QCYN7KL5LJJWUIH7CEWUSZIFMUJO6NPXE6CVGYNW) | Entry point for all trading operations: opening and closing positions, placing and cancelling orders. | +| `synthetics-reader` | | [explorer](https://stellar.expert/explorer/testnet/contract/CC6OZUHF3LVO6PNP3V2EB36ORB3YSVYSH3LWD3RFLO4NUO3BYCXSWSYC) | Market snapshots including prices, open interest, funding rates, and oracle state. | +| `test-faucet` | | [explorer](https://stellar.expert/explorer/testnet/contract/CCWXXBKXHHP5DXC6TYVIL22XUNHD5A75O6WM5D2KM5PY45IOV5VDMARJ) | Testnet utility to distribute test tokens to new traders. | +| `test-token` | | [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/) + +## ` +``` + +Renders a copiable link to the contract on Stellar Expert. diff --git a/apps/docs/content/reference/exchange-router.mdx b/apps/docs/content/reference/exchange-router.mdx new file mode 100644 index 00000000..2b6ee320 --- /dev/null +++ b/apps/docs/content/reference/exchange-router.mdx @@ -0,0 +1,290 @@ +--- +title: ExchangeRouter contract +description: Entry point for opening positions, placing orders, and claiming funding fees. +updated: 2026-08-25 +status: stable +--- + +ExchangeRouter is the primary interface for traders and integrators. Every state-changing trading action—opening a position, placing an order, or closing a position—goes through this contract. + +## Role and position in the call graph + +When you submit a trade, your transaction calls ExchangeRouter, which then: + +1. Validates your collateral and approves it (via `OrderVault`). +2. Encodes your order into `DataStore`. +3. Emits an event that keepers watch. +4. Holders can later claim funding fees accrued during your position. + +``` +Your transaction (Freighter or wallet) + ↓ + ExchangeRouter.create_order() ← entry point + ↓ + OrderVault (holds your collateral) + DataStore (stores the order) + ↓ + Keepers submit execution (offchain) + ↓ + Orders filled, positions opened/closed + ↓ + claim_funding_fees() ← you call this to withdraw accrued fees +``` + +ExchangeRouter does not calculate prices, P&L, or liquidation prices. Those come from `SyntheticsReader` and the oracle layer. It is purely the sequencer for order placement and execution. + +## Public functions + +### `create_order(caller: Address, params: CreateOrderParams) -> OrderKey` + +Create a new order and reserve collateral in the OrderVault until the order is filled or cancelled. + +**Authorization:** Requires the caller's signature. + +**Parameters:** + +", + decimals: "-", + description: "Intermediate markets for multi-hop swaps. Empty for direct orders." + }, + { + name: "params.sizeDeltaUsd", + type: "i128", + decimals: "30", + description: "Position size change in USD. Positive for long, negative for short. 1 USD = 10^30 units." + }, + { + name: "params.collateralDeltaAmount", + type: "i128", + decimals: "token-native", + description: "Collateral amount change in token units. Use toProtocolAmount() to convert from decimal strings." + }, + { + name: "params.triggerPrice", + type: "i128", + decimals: "30", + description: "Stop/take-profit trigger price in USD (30-decimal). Set to 0 for a market order." + }, + { + name: "params.acceptablePrice", + type: "i128", + decimals: "30", + description: "Worst acceptable fill price in USD (30-decimal). Used for slippage protection." + }, + { + name: "params.executionFee", + type: "i128", + decimals: "stroops", + description: "Keeper execution fee in XLM stroops. Typical values are 1-50 million stroops (0.01–0.5 XLM)." + }, + { + name: "params.minOutputAmount", + type: "i128", + decimals: "token-native", + description: "Minimum output token amount for swaps. Set to 0 for position orders." + }, + { + name: "params.orderType", + type: "OrderType enum", + decimals: "-", + description: "One of: MarketSwap, LimitSwap, MarketIncrease, LimitIncrease, MarketDecrease, LimitDecrease, StopLossDecrease, Liquidation, StopIncrease." + }, + { + name: "params.isLong", + type: "boolean", + decimals: "-", + description: "True for long positions (profit if price rises), false for short." + } + ]} +/> + +**Returns:** An `OrderKey` (32-byte hex string) that uniquely identifies the order on-chain. Use this key to cancel the order or track it. + +**Errors:** +- `InsufficientCollateral`: Collateral amount is below minimum required. +- `InvalidOrderType`: Order type is not recognized. +- `PriceTooHigh` / `PriceTooLow`: Trigger price is outside acceptable range. +- `ExecutionFeeInsufficient`: Keeper fee is too low. + +### `cancel_order(caller: Address, key: OrderKey) -> ()` + +Cancel an existing order and return reserved collateral to the caller. + +**Authorization:** Requires the order's creator's signature. Only the original caller or a liquidator can cancel. + +**Parameters:** + +)", + decimals: "-", + description: "The order key returned by create_order()." + } + ]} +/> + +**Returns:** None. Collateral is transferred back to the caller's account. + +**Errors:** +- `OrderNotFound`: Order key does not exist. +- `UnauthorizedCaller`: Caller is not the order creator. +- `OrderAlreadyFilled`: Order has been executed and cannot be cancelled. + +### `claim_funding_fees(caller: Address, markets: Vec
, tokens: Vec
) -> i128` + +Claim accumulated funding fees for closed positions. Funding fees accrue continuously while a position is open and are paid out when the position closes. + +**Authorization:** Requires the caller's signature. + +**Parameters:** + +", + decimals: "-", + description: "List of market addresses for which to claim fees (e.g., TETH/TUSDC market)." + }, + { + name: "tokens", + type: "Vec
", + decimals: "-", + description: "List of token addresses that fees will be claimed in (usually TUSDC)." + } + ]} +/> + +**Returns:** Total amount of fees claimed in the token's native decimal units (e.g., TUSDC has 7 decimals). + +**Errors:** +- `MarketNotFound`: One of the market addresses does not exist. +- `NoFeesToClaim`: No accrued fees exist for the caller on the specified markets. +- `TokenMismatch`: One of the fee tokens does not match the market's collateral token. + +## Worked example: opening a long position + +This example opens a long position on TETH/TUSDC for 10 TUSDC collateral with 2x leverage, using the client from `packages/contracts`. + +```ts +import { ExchangeRouterClient, toProtocolAmount } from "@workspace/contracts" +import { walletKit } from "@/features/wallet/lib/wallet-kit" + +const client = new ExchangeRouterClient({ + contractId: "CBD6BQSQFROWIIT5QCYN7KL5LJJWUIH7CEWUSZIFMUJO6NPXE6CVGYNW", + rpcUrl: "https://soroban-testnet.stellar.org", + networkPassphrase: "Test SDF Network ; September 2015", +}) + +const collateralToken = "CBAN5YU3KRDKPTQ2H76D6S7HQFPRBGUD524F65BUM2RQCITPTRLKWKES" // TUSDC +const market = "CCBUUSYZJTGVA6PYUNQDFPZFHTBZ2QSHOUO7YAGRQVA46T3ZLSIYULS4" +const callerAddress = "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + +const params = { + receiver: callerAddress, + market, + initialCollateralToken: collateralToken, + swapPath: [], + sizeDeltaUsd: toProtocolAmount("20", 30), // 2x leverage on 10 USDC = 20 USD position + collateralDeltaAmount: toProtocolAmount("10", 7), // 10 TUSDC (7 decimals) + triggerPrice: 0n, // market order + acceptablePrice: toProtocolAmount("2500", 30), // accept up to 2500 USD/ETH + executionFee: 5_000_000n, // 0.05 XLM + minOutputAmount: 0n, + orderType: "MarketIncrease", + isLong: true, +} + +// Build transaction XDR +const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: "Test SDF Network ; September 2015", +}) + .addOperation( + new Operation.InvokeHostFunction({ + hostFunction: HostFunction.invokeContractFn({ + contractId: client.contractId, + functionName: "create_order", + args: createOrderArgs(callerAddress, params), + }), + footprint: simResult.footprint, + }), + ) + .setTimeout(300) + .build() + +// Sign with Freighter or Wallets Kit +const signed = await walletKit.signTransaction(tx.toXDR(), "testnet") + +// Submit to Stellar +const response = await fetch("https://horizon-testnet.stellar.org/transactions", { + method: "POST", + body: new URLSearchParams({ tx: signed }), +}) + +const result = await response.json() +console.log("Order placed:", result.hash) +``` + +The transaction succeeds if: +- Collateral is sufficient and approved. +- Leverage is within exchange limits (typically 1x to 50x). +- Prices are within oracle deviation bounds. + +Within seconds, a keeper submits the execution, and your position opens at the fill price. + +## Generated bindings caveat + +The bindings in `packages/contracts/src/generated/exchange-router/` are hand-adapted from `stellar contract bindings typescript`. The committed files use camelCase field names and export `*Args` and `*Val` helpers (like `createOrderArgs()` above) that the raw CLI output does not include. + +**Do not regenerate bindings casually.** Running `bun run contracts:gen:all` overwrites these adaptations and breaks the build. See [Architecture](/developers/architecture) for the full context. + +If you must regenerate (e.g., after an on-chain contract upgrade), re-apply the camelCase and helper changes immediately so the break is visible in CI, rather than silently breaking downstream code. + +## Next steps + +- See [Writing transactions](/developers/writing-transactions) for how to simulate, sign, and submit ExchangeRouter calls. +- Read [Contract clients](/developers/contract-clients) for browser vs. Node differences. +- Check [Contract addresses](/reference/contracts.generated) for deployed contract IDs. diff --git a/packages/contracts/contract-descriptions.json b/packages/contracts/contract-descriptions.json new file mode 100644 index 00000000..e1503db5 --- /dev/null +++ b/packages/contracts/contract-descriptions.json @@ -0,0 +1,9 @@ +{ + "exchange-router": "Entry point for all trading operations: opening and closing positions, placing and cancelling orders.", + "synthetics-reader": "Market snapshots including prices, open interest, funding rates, and oracle state.", + "data-store": "Persistent storage for market configuration, pool state, and position collateral.", + "order-vault": "Holds collateral until an order is filled or cancelled, protecting against withdrawal during execution.", + "test-faucet": "Testnet utility to distribute test tokens to new traders.", + "test-token": "Testnet representative Stellar asset (TUSDC); all test token contracts share this interface.", + "glv-router": "Generalized liquidity vault for multi-pool liquidity provision and risk management." +} diff --git a/scripts/generate-contracts-reference.ts b/scripts/generate-contracts-reference.ts new file mode 100644 index 00000000..dde68d82 --- /dev/null +++ b/scripts/generate-contracts-reference.ts @@ -0,0 +1,135 @@ +#!/usr/bin/env bun + +/** + * Generate the contracts reference page from contracts.json and contract descriptions. + * Output: apps/docs/content/reference/contracts.generated.mdx + * + * This ensures the contracts page always matches the deployed contract IDs + * and descriptions are never out of sync. CI verifies the generated file + * has not drifted. + */ + +import * as fs from "fs" +import * as path from "path" + +interface ContractBinding { + name: string + contractId: string | null + note?: string +} + +interface ContractsConfig { + network: { + name: string + rpcUrl: string + networkPassphrase: string + } + outputDir: string + bindings: ContractBinding[] +} + +async function generate() { + const contractsJsonPath = path.join( + process.cwd(), + "packages/contracts/contracts.json" + ) + const descriptionsPath = path.join( + process.cwd(), + "packages/contracts/contract-descriptions.json" + ) + const outputPath = path.join( + process.cwd(), + "apps/docs/content/reference/contracts.generated.mdx" + ) + + // Read contracts.json + let contractsConfig: ContractsConfig + try { + const content = fs.readFileSync(contractsJsonPath, "utf-8") + contractsConfig = JSON.parse(content) + } catch (err) { + console.error(`Failed to read ${contractsJsonPath}:`, err) + process.exit(1) + } + + // Read descriptions + let descriptions: Record = {} + try { + const content = fs.readFileSync(descriptionsPath, "utf-8") + descriptions = JSON.parse(content) + } catch (err) { + console.error(`Failed to read ${descriptionsPath}:`, err) + process.exit(1) + } + + // Generate MDX rows + const rows = contractsConfig.bindings + .map((binding) => { + const description = descriptions[binding.name] || "No description" + const network = contractsConfig.network.name + const explorerUrl = + binding.contractId && network === "testnet" + ? `https://stellar.expert/explorer/testnet/contract/${binding.contractId}` + : null + + if (!binding.contractId) { + return `| \`${binding.name}\` | Not deployed | — | ${description} |` + } + + const contractLink = explorerUrl + ? `` + : `\`${binding.contractId}\`` + + return `| \`${binding.name}\` | ${contractLink} | [explorer](${explorerUrl}) | ${description} |` + }) + .join("\n") + + // Generate MDX content + const mdxContent = `--- +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 | +|----------|-------|----------|------| +${rows} + +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/) + +## \` +\`\`\` + +Renders a copiable link to the contract on Stellar Expert. +` + + // Write the generated file + try { + fs.mkdirSync(path.dirname(outputPath), { recursive: true }) + fs.writeFileSync(outputPath, mdxContent, "utf-8") + console.log(`✓ Generated ${outputPath}`) + } catch (err) { + console.error(`Failed to write ${outputPath}:`, err) + process.exit(1) + } +} + +generate().catch((err) => { + console.error("Generation failed:", err) + process.exit(1) +})