diff --git a/.changelog/unreleased/996-contracts-smoke-readme-motion.md b/.changelog/unreleased/996-contracts-smoke-readme-motion.md new file mode 100644 index 00000000..7b109ae1 --- /dev/null +++ b/.changelog/unreleased/996-contracts-smoke-readme-motion.md @@ -0,0 +1,8 @@ +--- +type: changed +area: trade +pr: 996 +breaking: false +--- + +Added price-tick visual flashes in the order book and spread reference price, made contracts.json the single source of truth for contract addresses, added route health browser smoke tests, and improved local setup documentation. 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 && ( +