From 3eef20d5835ac9e83802f1b6b4c292d552699f9a Mon Sep 17 00:00:00 2001 From: Jaydbrown <175232057+Jaydbrown@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:29:44 +0100 Subject: [PATCH 1/2] fix: contract ID single-source, route health smoke test, README setup, and price-tick motion --- README.md | 63 ++++-- apps/web/.env.example | 15 +- apps/web/.env.mainnet | 15 +- apps/web/.env.testnet | 35 +--- apps/web/setup-tests.ts | 22 +- apps/web/src/app/config/contracts.test.ts | 54 +++++ apps/web/src/app/config/contracts.ts | 63 +----- apps/web/src/app/config/env.test.ts | 5 - apps/web/src/app/config/env.ts | 107 +--------- .../components/orderbook/DepthLadder.test.tsx | 63 ++++++ .../components/orderbook/DepthLadder.tsx | 104 ++++++++-- apps/web/src/features/trade/lib/oracle.ts | 11 +- apps/web/test/config-harness.ts | 8 - apps/web/tsconfig.json | 1 + apps/web/vitest.config.ts | 63 ------ e2e/route-health.spec.ts | 194 ++++++++++++++++++ packages/contracts/contract-descriptions.json | 16 +- packages/contracts/contracts.json | 126 +++++++++++- packages/contracts/src/deployments.test.ts | 51 +++++ packages/contracts/src/deployments.ts | 47 +++++ packages/contracts/src/index.ts | 6 + packages/contracts/tsconfig.json | 1 + packages/ui/src/styles/globals.css | 36 ++++ 23 files changed, 760 insertions(+), 346 deletions(-) create mode 100644 apps/web/src/app/config/contracts.test.ts create mode 100644 e2e/route-health.spec.ts create mode 100644 packages/contracts/src/deployments.test.ts create mode 100644 packages/contracts/src/deployments.ts diff --git a/README.md b/README.md index 7b3faf20..7ce34900 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ SO4 is a unified-liquidity perpetuals DEX built on Stellar/Soroban. Deep order b - [Available Scripts](#available-scripts) - [Testing Guide](#testing-guide) - [Architecture](#architecture) +- [Troubleshooting](#troubleshooting) - [Contributing](#contributing) — see also [`CONTRIBUTING.md`](./CONTRIBUTING.md) and [`AGENTS.md`](./AGENTS.md) - [Changelog](#changelog) - [License](#license) @@ -130,55 +131,61 @@ Each feature under `src/features//` follows the same convention: | [Bun](https://bun.sh) | ≥ 1.3 | | [Node.js](https://nodejs.org) | ≥ 20 | -### Installation +### 1. Clone and Install ```bash # Clone the repository -git clone https://github.com/SO4-Markets/so4-monorepo.git -cd so4-monorepo +git clone https://github.com/SO4-Markets/interface.git +cd interface # Install all workspace dependencies bun install ``` -### Full local stack (contracts + indexer + web) +### 2. Configure Environment -If you need the indexer and contracts running against the web app: +Copy the example environment file to `.env.local` for local development: ```bash -# Generate indexer types and build (src/types is gitignored) +cp apps/web/.env.example apps/web/.env.local +``` + +> **Contract IDs:** All Soroban contract addresses are source-of-truth committed in `packages/contracts/contracts.json`. You do not need to configure any contract addresses in your environment variables. + +### 3. (Optional) Run the SubQuery Indexer Locally + +By default, the web app connects to Stellar testnet directly via Soroban RPC and falls back gracefully when the indexer is offline. If you wish to run the historical indexer locally: + +```bash +# Generate types and build indexer bun run --cwd apps/s03-indexer codegen bun run --cwd apps/s03-indexer build -# Sync the contract manifest from a local contracts checkout -SO4_CONTRACTS_REPO=/path/to/contracts \ - bun run --cwd apps/s03-indexer sync:contracts:local - # Start the indexer stack (requires Docker) bun run --cwd apps/s03-indexer start ``` -Run `bun run check:integration` before pushing to mirror the CI matrix. +Ensure `VITE_INDEXER_GRAPHQL_URL=http://localhost:3000` is enabled in `apps/web/.env.local` if using the local indexer. -### Running the development server +### 4. Run the Development Server ```bash -# Start all apps in watch mode +# Start all packages in development mode bun dev # Or start only the web app -cd apps/web && bun dev +bun run --cwd apps/web dev ``` -The app will be available at [http://localhost:3000](http://localhost:3000). +The application will be running at [http://localhost:3000](http://localhost:3000). -### Building for production +### 5. Building for Production ```bash -bun build +bun run build ``` -Output is written to `apps/web/.output/`. +Production assets are compiled by Turborepo and output to `apps/web/dist/`. --- @@ -189,7 +196,7 @@ Run any of these from the **repository root**: | Command | Description | |---|---| | `bun dev` | Start all packages in development mode | -| `bun build` | Build all packages for production | +| `bun run build` | Build all packages for production | | `bun lint` | Lint all packages with ESLint | | `bun format` | Format all files with Prettier | | `bun typecheck` | Run TypeScript type checks across all packages | @@ -260,6 +267,24 @@ The theme provider writes `dark` or `light` as a class on ``. A blocking i --- +## Troubleshooting + +### `Missing env var: VITE_*` + +If the web app fails to start or pages crash with `Missing env var: VITE_`: + +1. **Verify `.env.local` existence:** Vite in development mode automatically loads `apps/web/.env.local`. Create it by copying the template: + ```bash + cp apps/web/.env.example apps/web/.env.local + ``` +2. **Required variables:** Only the core network variables are required by `apps/web/src/app/config/env.ts`: + - `VITE_NETWORK` (e.g. `testnet`) + - `VITE_RPC_URL` (e.g. `https://soroban-testnet.stellar.org`) + - `VITE_HORIZON_URL` (e.g. `https://horizon-testnet.stellar.org`) +3. **Contract addresses:** Contract IDs are committed directly in `packages/contracts/contracts.json` and resolved via `@workspace/contracts`. Do not define `VITE_CONTRACT_*` in your `.env` files. + +--- + ## Contributing Contributions are welcome. **Read [`CONTRIBUTING.md`](./CONTRIBUTING.md) before diff --git a/apps/web/.env.example b/apps/web/.env.example index 2756ac3f..1b9b7d55 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,6 +1,7 @@ # ────────────────────────────────────────────────────────────────────────────── # SO4 — Environment variables -# Copy this file to .env.local (for local dev) and fill in real values. +# Copy this file to .env.local (for local dev) and customize if needed. +# Contract addresses are committed in packages/contracts/contracts.json. # All VITE_ vars are inlined by Vite at build time and visible in the bundle. # Never put private keys or secrets in VITE_ variables. # ────────────────────────────────────────────────────────────────────────────── @@ -29,16 +30,4 @@ VITE_HORIZON_URL=https://horizon-testnet.stellar.org # SubQuery GraphQL endpoint for indexed historical data. # Optional — when not set, indexer is disabled and app uses degraded fallback behavior. # Local development default: http://localhost:3000 -# Testnet default: (add testnet indexer URL when available) # VITE_INDEXER_GRAPHQL_URL=http://localhost:3000 - -# ── Contract addresses ──────────────────────────────────────────────────────── -# All Soroban contract IDs (C... StrKey addresses) for the active network. -VITE_CONTRACT_EXCHANGE_ROUTER=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_SYNTHETICS_READER=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_DATA_STORE=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_ORDER_VAULT=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_STAKING_ROUTER=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_GLV_ROUTER=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_VESTING_ROUTER=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_REFERRAL_STORAGE=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX diff --git a/apps/web/.env.mainnet b/apps/web/.env.mainnet index f478d97c..08bd7463 100644 --- a/apps/web/.env.mainnet +++ b/apps/web/.env.mainnet @@ -1,6 +1,7 @@ # ────────────────────────────────────────────────────────────────────────────── # SO4 — Mainnet environment # Load with: vite build --mode mainnet +# Contract addresses are committed in packages/contracts/contracts.json. # # Use a dedicated RPC provider for production. # Provider list: https://developers.stellar.org/docs/data/apis/rpc/providers @@ -15,12 +16,8 @@ VITE_HORIZON_URL=https://horizon.stellar.org # Optional — remove to fall back to GMX infra # VITE_ORACLE_URL=https://arbitrum-api.gmxinfra.io -# Mainnet contract addresses — replace with deployed IDs -VITE_CONTRACT_EXCHANGE_ROUTER=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_SYNTHETICS_READER=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_DATA_STORE=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_ORDER_VAULT=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_STAKING_ROUTER=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_GLV_ROUTER=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_VESTING_ROUTER=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -VITE_CONTRACT_REFERRAL_STORAGE=CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +# Pyth Hermes endpoint for on-chain price attestations +# VITE_PYTH_HERMES_URL=https://hermes.pyth.network + +# SubQuery GraphQL endpoint for indexed historical data +# VITE_INDEXER_GRAPHQL_URL=https://your-mainnet-indexer.example.com diff --git a/apps/web/.env.testnet b/apps/web/.env.testnet index 6aa7ef61..fb6f5bfd 100644 --- a/apps/web/.env.testnet +++ b/apps/web/.env.testnet @@ -1,7 +1,7 @@ # ────────────────────────────────────────────────────────────────────────────── # SO4 — Testnet environment # Load with: vite build --mode testnet (or vite dev --mode testnet) -# Deployed: 2026-06-05 Admin: GAUHMCMUP5FZO5675W3ISZ6E6CNYJGXBUW5WANE2JR4TGAARYCTSCBKI +# Contract addresses are committed in packages/contracts/contracts.json. # ────────────────────────────────────────────────────────────────────────────── VITE_NETWORK=testnet @@ -12,33 +12,8 @@ VITE_HORIZON_URL=https://horizon-testnet.stellar.org # Optional — remove to fall back to GMX infra # VITE_ORACLE_URL=https://arbitrum-api.gmxinfra.io -# ── Core contracts ───────────────────────────────────────────────────────────── -VITE_CONTRACT_EXCHANGE_ROUTER=CBD6BQSQFROWIIT5QCYN7KL5LJJWUIH7CEWUSZIFMUJO6NPXE6CVGYNW -VITE_CONTRACT_SYNTHETICS_READER=CC6OZUHF3LVO6PNP3V2EB36ORB3YSVYSH3LWD3RFLO4NUO3BYCXSWSYC -VITE_CONTRACT_DATA_STORE=CCZ3VKBEDLNBO2JM3EXL3SNBDJOV5BTN52FVQPER7F6D5GCE53PITQ3J -VITE_CONTRACT_MARKET_FACTORY=CBGX3EJFI3JRHSN5B533O2L5P57JFPTCRS55IPWFS5BNDXLJLXDWA5Z2 -VITE_CONTRACT_DEPOSIT_HANDLER=CDWOFIP4YQJGMCYAOWLSRBAWN2OTJUG2I5WOFC32O2TX2SRU56RWBE5C -VITE_CONTRACT_WITHDRAWAL_HANDLER=CBRWM6PNRRFL5RSTJH6HWEXBTMWCGLQRO45NTRDB6BBABWXZ4ZE7DGTO -VITE_CONTRACT_ORDER_VAULT=CAXZRTYJHTAEEHDJ5WH4MVS2BGRCNNXLJ7BH356RULEZGEV2RKAG3QON -VITE_CONTRACT_REFERRAL_STORAGE=CDHTPQO4RRJ6OUBIW3GDXTIVLVOMIKPJC65PGDJH2G5OLDJRE5KTROWK +# Pyth Hermes endpoint for on-chain price attestations +# VITE_PYTH_HERMES_URL=https://hermes.pyth.network -# ── Infrastructure contracts needed by the Reader ───────────────────────────── -VITE_CONTRACT_ORACLE=CBEMTV23SIJJBIST3V5HTMWHR4MHYGHNBIG4M26U4LGUJTWZXTFSVQEY -VITE_CONTRACT_ORDER_HANDLER=CC35OFZVWUTAZPV3B6UKSDVAVORZEWUUMOMTHO33H4YR4C5FKPEFODKY - -# ── Testnet faucet and tokens ───────────────────────────────────────────────── -VITE_FAUCET=CCWXXBKXHHP5DXC6TYVIL22XUNHD5A75O6WM5D2KM5PY45IOV5VDMARJ -VITE_TOKEN_TUSDC=CBAN5YU3KRDKPTQ2H76D6S7HQFPRBGUD524F65BUM2RQCITPTRLKWKES -VITE_TOKEN_TWBTC=CCFTOPHUPSUDO2MB4X5D3XYJ2HRJ7NJPAW4UVPAVN7ZLE63EZLSMXDUO -VITE_TOKEN_TETH=CAJ6BZKGFT47ALGMVFZZGAOXBV2RWIVYVCU4WJCQIURKRNXU346RWVAU -VITE_TOKEN_TXLM=CAHNXBBSXVMGI6G3FUBY3OTNWKQ7434FDDEEE7ZT733WIW6NUZL4ONU6 - -# ── Bootstrapped GM market tokens ───────────────────────────────────────────── -VITE_MARKET_TOKEN_TWBTC_TUSDC=CDDVSLBGGDV2UOFN5W72R4LW7ABYL7H7ZWVSFHGMXXB3D52ZYANC5G3L -VITE_MARKET_TOKEN_TETH_TUSDC=CCBUUSYZJTGVA6PYUNQDFPZFHTBZ2QSHOUO7YAGRQVA46T3ZLSIYULS4 -VITE_MARKET_TOKEN_TXLM_TUSDC=CDIBR7BDCDWGAG3CC6PBKRSLMISPYKNDGE57DCZO5TMTLZK34TMGKFQQ - -# ── Not yet deployed on testnet (leave blank — optional features disabled) ───── -VITE_CONTRACT_STAKING_ROUTER= -VITE_CONTRACT_GLV_ROUTER= -VITE_CONTRACT_VESTING_ROUTER= +# SubQuery GraphQL endpoint for indexed historical data +# VITE_INDEXER_GRAPHQL_URL=http://localhost:3000 diff --git a/apps/web/setup-tests.ts b/apps/web/setup-tests.ts index fbb1a7db..6819006b 100644 --- a/apps/web/setup-tests.ts +++ b/apps/web/setup-tests.ts @@ -22,24 +22,4 @@ afterAll(() => server.close()); process.env.VITE_NETWORK = 'testnet'; process.env.VITE_RPC_URL = 'https://soroban-testnet.stellar.org'; process.env.VITE_HORIZON_URL = 'https://horizon-testnet.stellar.org'; -process.env.VITE_CONTRACT_EXCHANGE_ROUTER = 'CAASUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP7'; -process.env.VITE_CONTRACT_SYNTHETICS_READER = 'CABCUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSYU'; -process.env.VITE_CONTRACT_DATA_STORE = 'CABSUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDKN'; -process.env.VITE_CONTRACT_ORDER_VAULT = 'CACCUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7WT'; -process.env.VITE_CONTRACT_STAKING_ROUTER = 'CACSUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEK'; -process.env.VITE_CONTRACT_GLV_ROUTER = 'CADCUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4TB'; -process.env.VITE_CONTRACT_VESTING_ROUTER = 'CADSUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABNBY'; -process.env.VITE_CONTRACT_REFERRAL_STORAGE = 'CAECUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANKM'; -process.env.VITE_CONTRACT_MARKET_FACTORY = 'CAESUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4YV'; -process.env.VITE_CONTRACT_DEPOSIT_HANDLER = 'CAFCUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOP6'; -process.env.VITE_CONTRACT_WITHDRAWAL_HANDLER = 'CAFSUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB75H'; -process.env.VITE_CONTRACT_ORACLE = 'CAGCUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBZ'; -process.env.VITE_CONTRACT_ORDER_HANDLER = 'CAGSUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASTA'; -process.env.VITE_FAUCET = 'CAHCUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAEL'; -process.env.VITE_TOKEN_TUSDC = 'CAHSUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABRWS'; -process.env.VITE_TOKEN_TWBTC = 'CAICUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABATC'; -process.env.VITE_TOKEN_TETH = 'CAISUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABRB3'; -process.env.VITE_TOKEN_TXLM = 'CAJCUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADWQ'; -process.env.VITE_MARKET_TOKEN_TWBTC_TUSDC = 'CAJSUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASEJ'; -process.env.VITE_MARKET_TOKEN_TETH_TUSDC = 'CAKCUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOYX'; -process.env.VITE_MARKET_TOKEN_TXLM_TUSDC = 'CAKSUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB7KO'; + diff --git a/apps/web/src/app/config/contracts.test.ts b/apps/web/src/app/config/contracts.test.ts new file mode 100644 index 00000000..efa2369c --- /dev/null +++ b/apps/web/src/app/config/contracts.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import { CONTRACTS } from "./contracts" +import { + getContractAddresses, + CONTRACT_DEPLOYMENTS, + type SupportedNetwork, +} from "@workspace/contracts" + +describe("Contract addresses resolution (#860)", () => { + it("resolves all required contracts for testnet", () => { + const contracts = getContractAddresses("testnet") + assert.ok(contracts.exchangeRouter, "exchangeRouter must be defined") + assert.ok(contracts.syntheticsReader, "syntheticsReader must be defined") + assert.ok(contracts.dataStore, "dataStore must be defined") + assert.ok(contracts.marketFactory, "marketFactory must be defined") + assert.ok(contracts.depositHandler, "depositHandler must be defined") + assert.ok(contracts.withdrawalHandler, "withdrawalHandler must be defined") + assert.ok(contracts.orderVault, "orderVault must be defined") + assert.ok(contracts.referralStorage, "referralStorage must be defined") + assert.ok(contracts.oracle, "oracle must be defined") + assert.ok(contracts.orderHandler, "orderHandler must be defined") + assert.ok(contracts.faucet, "faucet must be defined") + assert.ok(contracts.tokens.tusdc, "tokens.tusdc must be defined") + assert.ok(contracts.tokens.twbtc, "tokens.twbtc must be defined") + assert.ok(contracts.tokens.teth, "tokens.teth must be defined") + assert.ok(contracts.tokens.txlm, "tokens.txlm must be defined") + assert.ok(contracts.marketTokens.twbtcTusdc, "marketTokens.twbtcTusdc must be defined") + assert.ok(contracts.marketTokens.tethTusdc, "marketTokens.tethTusdc must be defined") + assert.ok(contracts.marketTokens.txlmTusdc, "marketTokens.txlmTusdc must be defined") + }) + + it("resolves contracts object for mainnet", () => { + const contracts = getContractAddresses("mainnet") + assert.ok(contracts, "mainnet contracts object must exist") + assert.equal(typeof contracts.exchangeRouter, "string") + assert.equal(typeof contracts.syntheticsReader, "string") + assert.equal(typeof contracts.dataStore, "string") + }) + + it("throws error for unknown network", () => { + assert.throws( + () => getContractAddresses("unknown" as SupportedNetwork), + /Unknown network "unknown" in contracts\.json/, + ) + }) + + it("exports active CONTRACTS matching testnet deployment", () => { + assert.equal(CONTRACTS.exchangeRouter, CONTRACT_DEPLOYMENTS.testnet.exchangeRouter) + assert.equal(CONTRACTS.syntheticsReader, CONTRACT_DEPLOYMENTS.testnet.syntheticsReader) + assert.equal(CONTRACTS.dataStore, CONTRACT_DEPLOYMENTS.testnet.dataStore) + assert.equal(CONTRACTS.tokens.tusdc, CONTRACT_DEPLOYMENTS.testnet.tokens.tusdc) + }) +}) diff --git a/apps/web/src/app/config/contracts.ts b/apps/web/src/app/config/contracts.ts index b13445fe..07caa56e 100644 --- a/apps/web/src/app/config/contracts.ts +++ b/apps/web/src/app/config/contracts.ts @@ -2,7 +2,7 @@ * apps/web/src/app/config/contracts.ts * * All Soroban contract addresses in one place, keyed by role. - * Values are read from ENV (which in turn reads VITE_ env vars). + * Sourced directly from packages/contracts/contracts.json via @workspace/contracts. * * Rule: feature code MUST import addresses from CONTRACTS, never * directly from ENV or import.meta.env. @@ -12,63 +12,10 @@ * if (!CONTRACTS.stakingRouter) throw new Error("Staking not deployed") */ +import { getContractAddresses, type ContractAddresses } from "@workspace/contracts" import { ENV } from "./env" -export type ContractAddresses = { - // ── Core trading contracts ───────────────────────────────────────────────── - exchangeRouter: string - syntheticsReader: string - dataStore: string - marketFactory: string - depositHandler: string - withdrawalHandler: string - orderVault: string - referralStorage: string - faucet: string - tokens: { - tusdc: string - twbtc: string - teth: string - txlm: string - } - marketTokens: { - twbtcTusdc: string - tethTusdc: string - txlmTusdc: string - } - // ── Infrastructure contracts used by Reader view functions ──────────────── - oracle: string - orderHandler: string - // ── Optional — empty string when not deployed ───────────────────────────── - stakingRouter: string - glvRouter: string - vestingRouter: string -} +export type { ContractAddresses } + +export const CONTRACTS: ContractAddresses = getContractAddresses(ENV.NETWORK) -export const CONTRACTS: ContractAddresses = { - exchangeRouter: ENV.CONTRACTS.EXCHANGE_ROUTER, - syntheticsReader: ENV.CONTRACTS.SYNTHETICS_READER, - dataStore: ENV.CONTRACTS.DATA_STORE, - marketFactory: ENV.CONTRACTS.MARKET_FACTORY, - depositHandler: ENV.CONTRACTS.DEPOSIT_HANDLER, - withdrawalHandler: ENV.CONTRACTS.WITHDRAWAL_HANDLER, - orderVault: ENV.CONTRACTS.ORDER_VAULT, - referralStorage: ENV.CONTRACTS.REFERRAL_STORAGE, - faucet: ENV.CONTRACTS.FAUCET, - tokens: { - tusdc: ENV.CONTRACTS.TOKENS.TUSDC, - twbtc: ENV.CONTRACTS.TOKENS.TWBTC, - teth: ENV.CONTRACTS.TOKENS.TETH, - txlm: ENV.CONTRACTS.TOKENS.TXLM, - }, - marketTokens: { - twbtcTusdc: ENV.CONTRACTS.MARKET_TOKENS.TWBTC_TUSDC, - tethTusdc: ENV.CONTRACTS.MARKET_TOKENS.TETH_TUSDC, - txlmTusdc: ENV.CONTRACTS.MARKET_TOKENS.TXLM_TUSDC, - }, - oracle: ENV.CONTRACTS.ORACLE, - orderHandler: ENV.CONTRACTS.ORDER_HANDLER, - stakingRouter: ENV.CONTRACTS.STAKING_ROUTER, - glvRouter: ENV.CONTRACTS.GLV_ROUTER, - vestingRouter: ENV.CONTRACTS.VESTING_ROUTER, -} diff --git a/apps/web/src/app/config/env.test.ts b/apps/web/src/app/config/env.test.ts index 2840f2e2..b1425cc8 100644 --- a/apps/web/src/app/config/env.test.ts +++ b/apps/web/src/app/config/env.test.ts @@ -41,11 +41,6 @@ describe("ENV startup validation (#243)", () => { assert.equal(result.message, "Missing env var: VITE_HORIZON_URL") }) - it("throws the exact error when a required contract ID is missing", () => { - const result = loadEnv(envWithout("VITE_CONTRACT_DATA_STORE")) - assert.equal(result.ok, false) - assert.equal(result.message, "Missing env var: VITE_CONTRACT_DATA_STORE") - }) it("loads successfully and derives values when every required var is present", () => { const result = loadEnv(VALID_ENV) diff --git a/apps/web/src/app/config/env.ts b/apps/web/src/app/config/env.ts index 50649de8..ccff9820 100644 --- a/apps/web/src/app/config/env.ts +++ b/apps/web/src/app/config/env.ts @@ -9,7 +9,7 @@ * Usage: * import { ENV } from "@/app/config/env" * console.log(ENV.NETWORK) // "testnet" | "mainnet" - * console.log(ENV.CONTRACTS.DATA_STORE) // contract address + * console.log(ENV.RPC_URL) // RPC URL endpoint */ // ───────────────────────────────────────────────────────────────────────────── @@ -31,44 +31,13 @@ function optionalEnv(value: string | undefined, fallback = ""): string { export type Network = "testnet" | "mainnet" -export type EnvContracts = { - // ── Core trading contracts ───────────────────────────────────────────────── - EXCHANGE_ROUTER: string - SYNTHETICS_READER: string - DATA_STORE: string - MARKET_FACTORY: string - DEPOSIT_HANDLER: string - WITHDRAWAL_HANDLER: string - ORDER_VAULT: string - REFERRAL_STORAGE: string - FAUCET: string - TOKENS: { - TUSDC: string - TWBTC: string - TETH: string - TXLM: string - } - MARKET_TOKENS: { - TWBTC_TUSDC: string - TETH_TUSDC: string - TXLM_TUSDC: string - } - // ── Infrastructure required by Reader views ─────────────────────────────── - ORACLE: string - ORDER_HANDLER: string - // ── Optional — not deployed on all networks yet ─────────────────────────── - STAKING_ROUTER: string // "" when not deployed - GLV_ROUTER: string // "" when not deployed - VESTING_ROUTER: string // "" when not deployed -} - export type Env = { NETWORK: Network RPC_URL: string HORIZON_URL: string ORACLE_URL: string PYTH_HERMES_URL: string - CONTRACTS: EnvContracts + INDEXER_GRAPHQL_URL?: string } // ───────────────────────────────────────────────────────────────────────────── @@ -92,71 +61,9 @@ export const ENV: Env = { "https://hermes.pyth.network", ), - CONTRACTS: { - EXCHANGE_ROUTER: requireEnv( - import.meta.env.VITE_CONTRACT_EXCHANGE_ROUTER, - "VITE_CONTRACT_EXCHANGE_ROUTER", - ), - SYNTHETICS_READER: requireEnv( - import.meta.env.VITE_CONTRACT_SYNTHETICS_READER, - "VITE_CONTRACT_SYNTHETICS_READER", - ), - DATA_STORE: requireEnv( - import.meta.env.VITE_CONTRACT_DATA_STORE, - "VITE_CONTRACT_DATA_STORE", - ), - MARKET_FACTORY: requireEnv( - import.meta.env.VITE_CONTRACT_MARKET_FACTORY, - "VITE_CONTRACT_MARKET_FACTORY", - ), - DEPOSIT_HANDLER: requireEnv( - import.meta.env.VITE_CONTRACT_DEPOSIT_HANDLER, - "VITE_CONTRACT_DEPOSIT_HANDLER", - ), - WITHDRAWAL_HANDLER: requireEnv( - import.meta.env.VITE_CONTRACT_WITHDRAWAL_HANDLER, - "VITE_CONTRACT_WITHDRAWAL_HANDLER", - ), - ORDER_VAULT: requireEnv( - import.meta.env.VITE_CONTRACT_ORDER_VAULT, - "VITE_CONTRACT_ORDER_VAULT", - ), - REFERRAL_STORAGE: requireEnv( - import.meta.env.VITE_CONTRACT_REFERRAL_STORAGE, - "VITE_CONTRACT_REFERRAL_STORAGE", - ), - ORACLE: requireEnv( - import.meta.env.VITE_CONTRACT_ORACLE, - "VITE_CONTRACT_ORACLE", - ), - ORDER_HANDLER: requireEnv( - import.meta.env.VITE_CONTRACT_ORDER_HANDLER, - "VITE_CONTRACT_ORDER_HANDLER", - ), - FAUCET: requireEnv(import.meta.env.VITE_FAUCET, "VITE_FAUCET"), - TOKENS: { - TUSDC: requireEnv(import.meta.env.VITE_TOKEN_TUSDC, "VITE_TOKEN_TUSDC"), - TWBTC: requireEnv(import.meta.env.VITE_TOKEN_TWBTC, "VITE_TOKEN_TWBTC"), - TETH: requireEnv(import.meta.env.VITE_TOKEN_TETH, "VITE_TOKEN_TETH"), - TXLM: requireEnv(import.meta.env.VITE_TOKEN_TXLM, "VITE_TOKEN_TXLM"), - }, - MARKET_TOKENS: { - TWBTC_TUSDC: requireEnv( - import.meta.env.VITE_MARKET_TOKEN_TWBTC_TUSDC, - "VITE_MARKET_TOKEN_TWBTC_TUSDC", - ), - TETH_TUSDC: requireEnv( - import.meta.env.VITE_MARKET_TOKEN_TETH_TUSDC, - "VITE_MARKET_TOKEN_TETH_TUSDC", - ), - TXLM_TUSDC: requireEnv( - import.meta.env.VITE_MARKET_TOKEN_TXLM_TUSDC, - "VITE_MARKET_TOKEN_TXLM_TUSDC", - ), - }, - // Optional — empty string signals "not deployed yet" - STAKING_ROUTER: optionalEnv(import.meta.env.VITE_CONTRACT_STAKING_ROUTER), - GLV_ROUTER: optionalEnv(import.meta.env.VITE_CONTRACT_GLV_ROUTER), - VESTING_ROUTER: optionalEnv(import.meta.env.VITE_CONTRACT_VESTING_ROUTER), - }, + INDEXER_GRAPHQL_URL: optionalEnv( + import.meta.env.VITE_INDEXER_GRAPHQL_URL, + "", + ), } + diff --git a/apps/web/src/features/trade/components/orderbook/DepthLadder.test.tsx b/apps/web/src/features/trade/components/orderbook/DepthLadder.test.tsx index df4c264f..100c7a5f 100644 --- a/apps/web/src/features/trade/components/orderbook/DepthLadder.test.tsx +++ b/apps/web/src/features/trade/components/orderbook/DepthLadder.test.tsx @@ -427,4 +427,67 @@ describe("DepthLadder", () => { expect(rows.length).toBeGreaterThan(2) // At least header + some data rows }) }) + + describe("price-tick flash animation (#873)", () => { + it("renders flash-up overlay when row size increases", () => { + const { rerender, container } = render() + + // Initial render - no flash overlay + expect(container.querySelector(".so4-flash-up-overlay")).not.toBeInTheDocument() + + // Increase size on first bid + mockUseOrderBook.mockReturnValue(createMockState({ + bids: [ + { price: 50000, size: 2.5, total: 2.5, depth: 0.6 }, + { price: 49950, size: 2.3, total: 4.8, depth: 1.0 }, + ], + })) + + rerender() + + expect(container.querySelector(".so4-flash-up-overlay")).toBeInTheDocument() + }) + + it("renders flash-down overlay when row size decreases", () => { + const { rerender, container } = render() + + // Decrease size on first bid + mockUseOrderBook.mockReturnValue(createMockState({ + bids: [ + { price: 50000, size: 0.5, total: 0.5, depth: 0.3 }, + { price: 49950, size: 2.3, total: 2.8, depth: 1.0 }, + ], + })) + + rerender() + + expect(container.querySelector(".so4-flash-down-overlay")).toBeInTheDocument() + }) + + it("renders flash-up overlay on reference price when price increases", () => { + const { rerender, container } = render() + + // Higher mid price + mockUseOrderBook.mockReturnValue(createMockState({ + midPrice: 50050, + })) + + rerender() + + expect(container.querySelector(".so4-flash-up-overlay")).toBeInTheDocument() + }) + + it("renders flash-down overlay on reference price when price decreases", () => { + const { rerender, container } = render() + + // Lower mid price + mockUseOrderBook.mockReturnValue(createMockState({ + midPrice: 49900, + })) + + rerender() + + expect(container.querySelector(".so4-flash-down-overlay")).toBeInTheDocument() + }) + }) }) diff --git a/apps/web/src/features/trade/components/orderbook/DepthLadder.tsx b/apps/web/src/features/trade/components/orderbook/DepthLadder.tsx index 79d6acd8..432ffae7 100644 --- a/apps/web/src/features/trade/components/orderbook/DepthLadder.tsx +++ b/apps/web/src/features/trade/components/orderbook/DepthLadder.tsx @@ -1,4 +1,4 @@ -import { useState } from "react" +import { useEffect, useRef, useState } from "react" import { cn } from "@workspace/ui/lib/utils" import { useOrderBook } from "../../hooks/useOrderBook" import { useRecentTrades } from "../../hooks/useRecentTrades" @@ -23,6 +23,33 @@ function DepthRow({ level, side, compact, onHover }: RowProps) { const isBid = side === "bid" const depthPct = `${(level.depth * 100).toFixed(1)}%` + // OB-011 / issue 873: 300ms price-tick flash on level size/price update + const [flashDirection, setFlashDirection] = useState<"up" | "down" | null>(null) + const prevSizeRef = useRef(level.size) + const prevPriceRef = useRef(level.price) + const flashKeyRef = useRef(0) + + useEffect(() => { + const sizeChanged = prevSizeRef.current !== level.size + const priceChanged = prevPriceRef.current !== level.price + + if (sizeChanged || priceChanged) { + const isUp = priceChanged + ? level.price > prevPriceRef.current + : level.size > prevSizeRef.current + + prevSizeRef.current = level.size + prevPriceRef.current = level.price + flashKeyRef.current += 1 + setFlashDirection(isUp ? "up" : "down") + + const timer = setTimeout(() => { + setFlashDirection(null) + }, 300) + return () => clearTimeout(timer) + } + }, [level.size, level.price]) + return (
+ {/* 300ms price-tick flash overlay — zero layout shift, opacity only */} + {flashDirection && ( +