Skip to content

feat(api,web): Phase 0 application-layer items G1–G4 (WS routes, V4 quote engine, Effect routing, default token list)#307

Merged
irfndi merged 5 commits into
mainfrom
feat/phase0-app
Jul 22, 2026
Merged

feat(api,web): Phase 0 application-layer items G1–G4 (WS routes, V4 quote engine, Effect routing, default token list)#307
irfndi merged 5 commits into
mainfrom
feat/phase0-app

Conversation

@irfndi

@irfndi irfndi commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Phase 0 — Foundation & validation gate (application-layer items)

Implements G1–G4 of the exploration plan §9 (Phase 0). G5 (deployment bindings — PoolManager/StateView/Router addresses + RPC) is intentionally NOT included — it is gated on the owner's RPC; all contract addresses are read from env/config so G5 only flips config. No changes to packages/contracts/src (G2.5, separate owner).

G1 — WebSocket DOs wired

  • GET /ws/prices/:tokenAddressWebSocketHubDO (single hub instance; live price fan-out — matches web PriceTicker/useWebSocket)
  • GET /ws/orderbook/:poolIdOrderBookDO keyed by pool id (poolId forwarded as query for snapshot load; 400 on invalid id)
  • DO logic untouched — transport plumbing only; SELF-based 101-upgrade + round-trip tests

G2 — Real V4 tick-math quote engine (the critical correctness item)

  • services/quote-engine.ts: exact-input simulation on @uniswap/v4-sdk Pool over a chain-state snapshot (slot0 sqrtPriceX96 + in-range liquidity + initialized ticks with liquidityNet) — swaps that cross ticks are stepped correctly (single- and multi-tick crossings), pool fee applied (V4 SwapMath semantics), raw price impact
  • services/chain-state-reader.ts: ChainStateReader Effect service abstracts the on-chain read — mock + unconfigured layers (tests / pre-deployment) and a live StateView impl via viem (getSlot0 + getLiquidity + windowed getTickLiquidity scan; replaced by the Phase-3 indexer). Addresses come only from env (STATE_VIEW_ADDRESS, POOL_MANAGER_ADDRESS, RPC_URL) — nothing hardcoded
  • SwapService rewritten: pool metadata via PoolService, state via ChainStateReader; QUOTE_ENGINE_MODE (auto default) keeps the legacy constant-product path as rollback per plan §9 (auto→legacy when on-chain reads unconfigured). /quote HTTP shape unchanged
  • Tests: exact expected outputs — hardcoded vectors (e.g. fee-0 unit-price 1e18 → 5e17) plus an independent per-segment SwapMath oracle for fee-3000 single-tick and 1-/2-tick crossings; liquidityNet application asserted via post-swap liquidity

G3 — Effect routing verified/completed

  • New PositionService (Context.Service + Layer.effect + SqlClient); /positions handlers route through it — the old raw Effect queries for positions are removed
  • Swap path routes pool metadata through PoolService (no raw db.prepare left in services/routes); auth was already KV-service-based; cron/queue background paths unchanged (not HTTP)

G4 — Uniswap default token list (no custom lists)

  • lib/token-list.ts: validates the canonical list (https://tokens.uniswap.org) — schema + EIP-55 checksums via viem (strict) + chainId filter; invalid entries dropped, malformed docs rejected
  • TokenListService (Effect): KV cache (6h) → fresh fetch/re-cache → D1 write-through cache fallback; the D1 tokens table is strictly a cache of the default list, never a curated source
  • /tokens + /tokens/:address serve through TokenListService (env: TOKEN_LIST_URL, CHAIN_ID)
  • shared: TokenSchema/TokenListResponseSchema/TokenResponseSchema; web TokenSearch consumes /tokens (default-list top tokens on open, debounced search, single-address lookup) — 4-token hardcode removed

Also in this PR

  • worker-configuration.d.ts (wrangler types) is now committed — api typecheck was red on main (all Cloudflare binding types unresolved); now green
  • Test pipeline fix: workerd cannot load ethers@5 ESM (circular named exports) — vitest CJS aliases + a small viem-backed shim (test/shims/) for the test/worker pipeline only; production still bundles the real packages

Verification matrix (all from worktree root, exit codes)

Check Exit
bun install 0
bun run typecheck (root) 0
cd apps/api && bun run typecheck 0
cd apps/web && bun run typecheck 0
bun run lint (Biome) 0 (0 errors, 0 warnings)
bun run test (root: 9 files / 88 tests) 0
cd apps/api && bun run test (7 files / 40 tests) 0
cd apps/web && bun run build 0
cd apps/api && bun run build (wrangler dry-run) 0

Follow-ups (not in scope)

  • G5: wire STATE_VIEW_ADDRESS/POOL_MANAGER_ADDRESS/RPC_URL/ROUTER_ADDRESS/FACTORY_ADDRESS after Sepolia deployment — the quote path automatically goes live with QUOTE_ENGINE_MODE=auto once they are set
  • Robinhood-Chain tokens: the Uniswap default list has no Robinhood Chain entries yet (chainId filter yields an empty list there) — mechanism + chainId filtering are in place; adding Robinhood-Chain tokens to the upstream default list is a follow-up data task
  • Hook pools that alter swap deltas would need a Quoter simulation instead of vanilla math (AetherDEX hook fees are on LP actions, so vanilla math is exact for our pools) — document when a delta-altering hook lands
  • Phase-3 indexer replaces the windowed getTickLiquidity scan; Phase-2 on-chain position reconciliation (plan gap) still open

Summary by CodeRabbit

  • New Features

    • Added real-time WebSocket price and order-book connections with validation for order-book requests.
    • Added canonical token-list loading, validation, caching, search, and address lookup.
    • Token selection now retrieves current results from the API instead of a fixed list.
    • Improved swap quotes using pool state and tick-aware simulation, with legacy fallback support.
    • Added support for viewing and recording liquidity positions through the updated service flow.
  • Bug Fixes

    • Improved handling of unavailable token data, invalid inputs, missing liquidity, and configuration errors.

irfndi added 5 commits July 22, 2026 23:45
- Unignore and commit worker-configuration.d.ts (wrangler types) so the
  api typecheck gate is green without a prebuild step (was red on main:
  all Cloudflare binding types unresolved).
- Add Phase-0 deployment env vars (never hardcoded in source):
  STATE_VIEW_ADDRESS, POOL_MANAGER_ADDRESS, RPC_URL (quote engine, G5
  wires them), QUOTE_ENGINE_MODE (auto|v4|legacy rollback switch),
  TOKEN_LIST_URL (canonical Uniswap default list).
- Add @cloudflare/workers-types + ethers/@ethersproject devDeps (test
  pipeline resolution; runtime uses the SDKs' own copies).
- GET /ws/prices/:tokenAddress upgrades to a single WebSocketHubDO
  instance (live price fan-out; matches web PriceTicker/useWebSocket).
- GET /ws/orderbook/:poolId upgrades to OrderBookDO keyed by pool id
  (poolId forwarded for snapshot load; 400 on invalid id).
- DO logic untouched — transport plumbing only.
- Test infra: vitest CJS aliases + viem-backed ethers shim so the
  workers test pipeline can load the Uniswap SDK barrels (workerd
  cannot link ethers@5 ESM); SELF-based 101-upgrade + DO round-trip
  tests for both routes.
… G2)

Replace the constant-product approximation (wrong for concentrated
liquidity) with real Uniswap V4 tick math:

- quote-engine: exact-input simulation on @uniswap/v4-sdk Pool over a
  pool-state snapshot (sqrtPriceX96 + in-range liquidity + initialized
  ticks), stepping across ticks with their recorded liquidityNet —
  single- and multi-tick crossings, fee handling, raw price impact.
- ChainStateReader Effect service abstracts the on-chain read: live
  implementation reads StateView (getSlot0 + getLiquidity + a windowed
  getTickLiquidity scan; replaced by the Phase-3 indexer) with
  addresses from env only (STATE_VIEW_ADDRESS/RPC_URL, wired in G5);
  mock + unconfigured layers for tests and pre-deployment fallback.
- SwapService composes PoolService (pool metadata) + ChainStateReader;
  QUOTE_ENGINE_MODE=auto falls back to the legacy approximation when
  on-chain reads are unconfigured (rollback path per plan §9). /quote
  HTTP shape unchanged.
- Tests: exact expected outputs (hardcoded vectors + an independent
  per-segment SwapMath oracle) for fee-0/fee-3000 single-tick and
  one-/two-tick crossings; liquidityNet application asserted via
  post-swap liquidity; reader mock/unconfigured paths.
- New PositionService (Context.Service + Layer.effect + SqlClient):
  listByUser + recordPosition; /positions handlers no longer call raw
 Effect queries directly.
- Drop the position queries from db/queries.ts (service owns them);
  swap path already routes pool metadata through PoolService — all
  HTTP data access now flows through Effect services.
…ase-0 G4)

Scope lock: no custom/curated token lists — tokens come only from the
canonical Uniswap default list (https://tokens.uniswap.org).

- lib/token-list: validate the list document (schema + EIP-55 address
  checksums via viem, strict mode) filtered by chainId; invalid
  entries dropped, malformed documents rejected.
- TokenListService (Effect): fetch → validate → cache. KV cache
  (6h TTL) first, fresh fetch re-caches, D1 tokens table as a
  write-through CACHE fallback only (never a curated source).
- /tokens + /tokens/:address routes serve through TokenListService;
  TOKEN_LIST_URL + CHAIN_ID from env.
- shared: TokenSchema/TokenListResponseSchema/TokenResponseSchema
  (typed contract end-to-end).
- web: TokenSearch consumes /tokens (default-list top tokens on open,
  debounced search + single-address lookup); 4-token hardcode removed.
- Tests: validation/checksum/chainId cases + service cache & fallback
  (mock fetcher, fake KV/SQL) + component tests against mocked API.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @irfndi, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The API adds Effect-based position, token-list, chain-state, and swap services; introduces V4 tick-aware quoting with fallback modes; adds WebSocket routes; connects the web token picker to token APIs; and expands shared schemas, Worker bindings, deployment variables, and test infrastructure.

Changes

V4 quote and swap flow

Layer / File(s) Summary
V4 quote engine and swap wiring
apps/api/src/services/chain-state-reader.ts, apps/api/src/services/quote-engine.ts, apps/api/src/services/swap.service.ts, apps/api/src/routes/swap.ts, apps/api/test/services/quote-engine.test.ts
On-chain pool state, initialized ticks, exact-input simulation, quote modes, legacy fallback, calldata construction, and route dependency layers are added and tested.

Position service migration

Layer / File(s) Summary
Position service migration
apps/api/src/services/position.service.ts, apps/api/src/routes/positions.ts, apps/api/src/db/queries.ts, apps/api/test/services/position.service.test.ts
Position listing and recording move from direct query helpers to PositionService with Effect and SqlClient wiring.

Token-list flow

Layer / File(s) Summary
Validated token-list service and API
apps/api/src/lib/token-list.ts, apps/api/src/services/token-list.service.ts, apps/api/src/routes/tokens.ts, packages/shared/src/schema.ts, apps/api/test/services/token-list.test.ts
Token lists are validated, filtered, cached in KV/D1, exposed through token routes, and represented by shared schemas.

Web token search

Layer / File(s) Summary
Web token API and search integration
apps/web/src/lib/api.ts, apps/web/src/components/TokenSearch.tsx, apps/web/test/components/TokenSearch.test.tsx
TokenSearch retrieves default, query, and address results through the API with updated response decoding and tests.

WebSocket routing

Layer / File(s) Summary
WebSocket endpoint routing
apps/api/src/index.ts, apps/api/test/ws-routing.test.ts
Price and orderbook WebSocket upgrades are routed to Durable Objects with pool ID validation and protocol coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

V4 quote request flow

sequenceDiagram
  participant SwapRoute
  participant SwapService
  participant PoolService
  participant ChainStateReader
  participant QuoteEngine
  SwapRoute->>SwapService: request quote
  SwapService->>PoolService: resolve pool metadata
  SwapService->>ChainStateReader: getPoolState
  ChainStateReader-->>SwapService: pool chain state
  SwapService->>QuoteEngine: simulateExactInputSwap
  QuoteEngine-->>SwapService: quote result
  SwapService-->>SwapRoute: formatted quote or fallback
Loading

Token-list resolution flow

sequenceDiagram
  participant TokenRoute
  participant TokenListService
  participant KVCache
  participant TokenListFetcher
  participant D1
  TokenRoute->>TokenListService: listTokens or getToken
  TokenListService->>KVCache: read validated token cache
  KVCache-->>TokenListService: cached tokens or cache miss
  TokenListService->>TokenListFetcher: fetch canonical list
  TokenListFetcher-->>TokenListService: validated upstream data
  TokenListService->>D1: write-through token rows
  TokenListService-->>TokenRoute: filtered token response
Loading

Poem

I’m a bunny with code in my paws,
Tick math now hops through new laws.
Tokens bloom from the API stream,
WebSockets ping in a rabbit’s dream.
Services layer, caches glow—
Merge this burrow; off we go!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main API and web changes, including WS routes, V4 quoting, Effect-based routing, and token list integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase0-app

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
apps/api/package.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 166c50c52f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +175 to +177
for (let k = 1; k <= scan; k += 1) {
candidates.push(tick - k * key.tickSpacing)
candidates.push(tick + k * key.tickSpacing)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Align tick probes to valid tick-spacing boundaries

When the active tick is not itself divisible by tickSpacing—the normal case—adding and subtracting whole spacing units preserves its remainder, so every candidate is an invalid initialized-tick boundary. For example, tick -219 with spacing 60 probes -279, -159, etc., instead of multiples such as -240 and -180; the reader consequently returns no liquidityNet changes and produces incorrect cross-tick quotes. Anchor the scan to a compressed/usable tick boundary and include the appropriate current boundary.

AGENTS.md reference: AGENTS.md:L87-L87

Useful? React with 👍 / 👎.

Comment on lines +307 to +312
return yield* quoteV4(params, amountIn, zeroForOne, meta).pipe(
Effect.catch((err) => {
// Rollback path: in auto mode, degrade to the legacy approximation
// when the V4 path cannot read on-chain state (e.g. undeployed pre-G5).
if (mode === "auto") {
return quoteLegacy(params, amountIn, meta)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict legacy fallback to an unconfigured reader

With the default auto mode, this catches every V4 quote error, not only the documented pre-G5 not_configured case. If the live reader reports zero liquidity or simulation fails after reading current state while D1 still contains stale positive liquidity, the endpoint returns a plausible constant-product quote for a swap that cannot execute. Preserve the on-chain error reason and only use the rollback approximation when reads are explicitly unconfigured.

AGENTS.md reference: AGENTS.md:L87-L87

Useful? React with 👍 / 👎.

Comment thread apps/api/src/index.ts
Comment on lines +127 to +129
app.get("/ws/prices/:tokenAddress", async (c) => {
const id = c.env.WEBSOCKET_HUB.idFromName("price-hub")
return c.env.WEBSOCKET_HUB.get(id).fetch(c.req.raw)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Publish refreshed prices to the WebSocket hub

A repo-wide search for WEBSOCKET_HUB, /price, and broadcastPrice shows that this new subscriber route is the only application call into the hub: the queue refresh path only writes prices to KV, and neither it nor cron invokes the DO's internal /price endpoint. Consequently PriceTicker can connect successfully but never receives a price update; wire the refresh producer to the hub rather than shipping a handshake-only route.

AGENTS.md reference: AGENTS.md:L324-L324

Useful? React with 👍 / 👎.

Comment thread apps/api/src/index.ts
Comment on lines +127 to +129
app.get("/ws/prices/:tokenAddress", async (c) => {
const id = c.env.WEBSOCKET_HUB.idFromName("price-hub")
return c.env.WEBSOCKET_HUB.get(id).fetch(c.req.raw)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the price WebSocket payload to its consumer

When a producer does call the hub, the routed DO sends { type: "price_update", data: { price0Usd, ... } } (websocket-hub-do.ts lines 163–165), while useWebSocket stores that whole envelope and PriceTicker reads data.price (PriceTicker.tsx lines 78–80). The first update therefore passes undefined to formatPrice and reaches undefined.toFixed(...), crashing the ticker; the route/protocol must select the requested token's price and emit or unwrap the shape the consumer expects.

Useful? React with 👍 / 👎.

Comment on lines +60 to +65
? makeStateViewReaderLayer({
rpcUrl,
stateViewAddress: stateViewAddress as `0x${string}`,
chainId,
tickScanEachSide: 64,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject quotes that leave the scanned tick window

The live path fetches only 64 tick-spacing steps on each side, but the simulation treats every tick outside that window as uninitialized and continues using the last known liquidity. For a large swap or a low-liquidity pool that moves beyond this window, omitted liquidityNet changes can make amountOut either too high or too low—the approximation is not conservative. Use the full bitmap/quoter, expand reads as the swap advances, or fail when the simulated price exits the verified window.

AGENTS.md reference: AGENTS.md:L87-L87

Useful? React with 👍 / 👎.

Comment on lines 100 to +104

// Load the default-list top tokens once, when the picker first opens.
useEffect(() => {
if (!isOpen || defaultsLoadedRef.current) return
defaultsLoadedRef.current = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow the default token list to retry after failure

The loaded flag is set before the request completes and is never reset. If the first fetch fails, or the picker closes while it is in flight so cleanup discards the response, every later open skips loading and the default token picker remains empty for the component's lifetime. Set the flag only after a successful retained response, or clear it on cancellation/error so reopening retries.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/components/TokenSearch.tsx (1)

119-140: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Stale search responses can overwrite newer ones (no request cancellation).

searchTokens doesn't pass an AbortSignal or cancel the previous in-flight call. The 300ms debounce only coalesces timers; if an earlier request resolves after a later one (e.g. due to network jitter), its results overwrite the newer, correct ones. Both fetchTokenByAddress/fetchTokens already accept a signal.

🔧 Proposed fix: abort superseded requests
+  const searchAbortRef = useRef<AbortController | null>(null)
+
   const searchTokens = useCallback(async (searchQuery: string) => {
     if (!searchQuery.trim()) {
       setResults([])
       setIsSearching(false)
       return
     }

+    searchAbortRef.current?.abort()
+    const controller = new AbortController()
+    searchAbortRef.current = controller
     setIsSearching(true)
     try {
       if (isValidAddress(searchQuery)) {
-        const found = await Effect.runPromise(fetchTokenByAddress(searchQuery))
+        const found = await Effect.runPromise(fetchTokenByAddress(searchQuery, controller.signal))
+        if (controller.signal.aborted) return
         setResults(found ? [toToken(found)] : [])
         return
       }
-      const res = await Effect.runPromise(fetchTokens({ query: searchQuery, limit: 50 }))
+      const res = await Effect.runPromise(fetchTokens({ query: searchQuery, limit: 50, signal: controller.signal }))
+      if (controller.signal.aborted) return
       setResults(res.tokens.map(toToken))
     } catch {
-      setResults([])
+      if (!controller.signal.aborted) setResults([])
     } finally {
-      setIsSearching(false)
+      if (!controller.signal.aborted) setIsSearching(false)
     }
   }, [])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/TokenSearch.tsx` around lines 119 - 140, Update the
searchTokens request flow to cancel the previous in-flight search whenever a new
query starts, using an AbortController and passing its signal to both
fetchTokenByAddress and fetchTokens. Ensure superseded aborts do not replace
newer results or surface as errors, while preserving the existing loading and
empty-result behavior.
🧹 Nitpick comments (4)
apps/api/src/routes/swap.ts (1)

33-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

ETH_ADDRESS_RE and HEX_ADDRESS_RE are byte-for-byte identical. The rename left both constants in place, so validation now splits across two duplicate patterns (handlers use one, swapServiceLayer the other) that can silently diverge later. Collapse to a single constant.

♻️ Suggested cleanup
-const ETH_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/
-const HEX_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/
+const HEX_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/

Then replace the remaining ETH_ADDRESS_RE uses (Lines 101, 143) with HEX_ADDRESS_RE.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/swap.ts` around lines 33 - 34, Remove the duplicate
ETH_ADDRESS_RE declaration and retain HEX_ADDRESS_RE as the single
address-validation pattern. Update the remaining ETH_ADDRESS_RE references in
the affected handlers to use HEX_ADDRESS_RE, including the validation paths near
lines 101 and 143, while preserving existing behavior.
apps/api/src/services/chain-state-reader.ts (1)

173-196: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Bound the tick-scan RPC fan-out
createPublicClient({ transport: http(config.rpcUrl) }) sends each readContract as a separate eth_call, so the live path will burst 2 * tickScanEachSide tick probes per quote (128 at the current config) plus the spot reads. Enable viem HTTP batching (http(config.rpcUrl, { batch: true })) and/or chunk the scan to reduce RPC rate-limit risk.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/services/chain-state-reader.ts` around lines 173 - 196, The
tick-scan path in the surrounding state-reader setup currently launches all
candidates through Promise.all, causing excessive concurrent RPC calls. Enable
viem HTTP batching in the createPublicClient transport using http(config.rpcUrl,
{ batch: true }) and chunk the candidates used by the tickResults probes into
bounded batches while preserving the existing getTickLiquidity calls and error
handling.
apps/api/src/services/position.service.ts (1)

51-60: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

No chain_id in the liquidity_positions filter/schema.

listByUser filters only on user_address/is_active, and LiquidityPosition has no chainId field. As per coding guidelines, "Data schemas and queries must include chain_id in composite keys and filters before indexing a second chain." Given CHAIN_ID is already a binding and this PR is building multi-chain plumbing (ChainStateReader), adding chain_id now — while the table is new — is far cheaper than retrofitting it later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/services/position.service.ts` around lines 51 - 60, The
liquidity-position model and persistence flow lack chain scoping. Update the
liquidity-position schema, LiquidityPosition mapping, and listByUser query to
include and filter by chain_id, using the existing CHAIN_ID binding and
composite user/chain criteria while preserving active-position ordering and
limits.

Source: Coding guidelines

apps/api/src/services/token-list.service.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Leftover verified filter: declared/documented but not implemented. The AI summary confirms verified query handling was intentionally removed, but the option is still declared in the service interface and still documented in the route's JSDoc, which will mislead API consumers into thinking ?verified=true filters results.

  • apps/api/src/services/token-list.service.ts#L65-69: remove the unused verified field from TokenSearchOptions (or implement it in matchesQuery/applyOptions if filtering is actually desired).
  • apps/api/src/routes/tokens.ts#L49-51: update the JSDoc to drop the verified=true example since the parameter is no longer read.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/services/token-list.service.ts` at line 1, Remove the unused
verified field from the TokenSearchOptions interface and delete the
verified=true example from the tokens route JSDoc. Keep matchesQuery and
applyOptions unchanged unless verified filtering is intentionally being
restored.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/package.json`:
- Around line 32-52: Update the dependency declarations in package.json to use
jsbi 4.3.2, and either remove the unnecessary ethers@5 and `@ethersproject/`*
entries or document an explicit compatibility exception for retaining them.

In `@apps/api/src/index.ts`:
- Around line 132-141: Canonicalize the validated poolId in the
/ws/orderbook/:poolId handler before using it for ORDER_BOOK.idFromName or
forwarding it as the poolId query parameter, preserving the 0x prefix and using
a consistent casing. Add a regression test with alphabetic hex that compares
differently cased URLs and verifies they select the same Durable Object and
snapshot key.

In `@apps/api/src/routes/tokens.ts`:
- Around line 52-53: Update the limit parsing in the tokens GET handler to
detect non-numeric or otherwise invalid parsed values before applying Math.min.
Fall back to the existing default limit of 100 for invalid input, while
preserving the 500 maximum for valid numeric limits and the existing
applyOptions flow.

In `@apps/api/src/services/chain-state-reader.ts`:
- Around line 126-130: Update poolIdOf to validate or normalize key.token0 and
key.token1 before constructing Token instances, and ensure malformed addresses
are handled as the expected OnChainReadError rather than escaping as constructor
exceptions. Preserve the existing Pool.getPoolId behavior for valid addresses.

In `@apps/api/src/services/token-list.service.ts`:
- Line 1: The token cache and shared TokenSchema lack chain scoping, allowing
tokens with the same address on different chains to collide or leak. Update the
token-list service INSERT/ON CONFLICT path to include chain_id using
deps.chainId, add the required migration and composite key support, and update
readD1Cache to filter by deps.chainId; also add chainId to TokenSchema so the
API contract preserves per-chain identity.
- Around line 84-97: Update the token cache flow around toTokenInfo, readCache,
and refresh so timestamps are captured once per cache refresh and reused on
every cached read. Store or propagate a shared refresh-time value alongside the
cached token data, then pass it into toTokenInfo instead of calling Date.now()
for each mapped token; preserve the existing timestamps for newly refreshed
tokens.

In `@apps/api/test/ws-routing.test.ts`:
- Around line 46-52: In the WebSocket test flow around nextMessage, create each
receive promise before sending its corresponding subscribe or ping message, then
await that promise for the subscribed and pong responses. Preserve the existing
response assertions while ensuring listeners are attached before ws.send
executes.

---

Outside diff comments:
In `@apps/web/src/components/TokenSearch.tsx`:
- Around line 119-140: Update the searchTokens request flow to cancel the
previous in-flight search whenever a new query starts, using an AbortController
and passing its signal to both fetchTokenByAddress and fetchTokens. Ensure
superseded aborts do not replace newer results or surface as errors, while
preserving the existing loading and empty-result behavior.

---

Nitpick comments:
In `@apps/api/src/routes/swap.ts`:
- Around line 33-34: Remove the duplicate ETH_ADDRESS_RE declaration and retain
HEX_ADDRESS_RE as the single address-validation pattern. Update the remaining
ETH_ADDRESS_RE references in the affected handlers to use HEX_ADDRESS_RE,
including the validation paths near lines 101 and 143, while preserving existing
behavior.

In `@apps/api/src/services/chain-state-reader.ts`:
- Around line 173-196: The tick-scan path in the surrounding state-reader setup
currently launches all candidates through Promise.all, causing excessive
concurrent RPC calls. Enable viem HTTP batching in the createPublicClient
transport using http(config.rpcUrl, { batch: true }) and chunk the candidates
used by the tickResults probes into bounded batches while preserving the
existing getTickLiquidity calls and error handling.

In `@apps/api/src/services/position.service.ts`:
- Around line 51-60: The liquidity-position model and persistence flow lack
chain scoping. Update the liquidity-position schema, LiquidityPosition mapping,
and listByUser query to include and filter by chain_id, using the existing
CHAIN_ID binding and composite user/chain criteria while preserving
active-position ordering and limits.

In `@apps/api/src/services/token-list.service.ts`:
- Line 1: Remove the unused verified field from the TokenSearchOptions interface
and delete the verified=true example from the tokens route JSDoc. Keep
matchesQuery and applyOptions unchanged unless verified filtering is
intentionally being restored.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ef916ecb-cac3-43f2-b522-854f0f093ac4

📥 Commits

Reviewing files that changed from the base of the PR and between e447ef4 and 166c50c.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • apps/api/.gitignore
  • apps/api/package.json
  • apps/api/src/db/queries.ts
  • apps/api/src/index.ts
  • apps/api/src/lib/token-list.ts
  • apps/api/src/routes/positions.ts
  • apps/api/src/routes/swap.ts
  • apps/api/src/routes/tokens.ts
  • apps/api/src/services/chain-state-reader.ts
  • apps/api/src/services/index.ts
  • apps/api/src/services/position.service.ts
  • apps/api/src/services/quote-engine.ts
  • apps/api/src/services/swap.service.ts
  • apps/api/src/services/token-list.service.ts
  • apps/api/test/db/queries.test.ts
  • apps/api/test/services/chain-state-reader.test.ts
  • apps/api/test/services/position.service.test.ts
  • apps/api/test/services/quote-engine.test.ts
  • apps/api/test/services/token-list.test.ts
  • apps/api/test/shims/ethers-worker-shim.ts
  • apps/api/test/ws-routing.test.ts
  • apps/api/vitest.config.ts
  • apps/api/worker-configuration.d.ts
  • apps/api/wrangler.jsonc
  • apps/web/src/components/TokenSearch.tsx
  • apps/web/src/lib/api.ts
  • apps/web/test/components/TokenSearch.test.tsx
  • packages/shared/src/schema.ts
💤 Files with no reviewable changes (2)
  • apps/api/.gitignore
  • apps/api/test/db/queries.test.ts

Comment thread apps/api/package.json
Comment on lines +32 to +52
"@uniswap/sdk-core": "^7.18.0",
"@uniswap/v3-sdk": "^3.31.0",
"@uniswap/v4-sdk": "^2.3.0",
"effect": "4.0.0-beta.100",
"hono": "^4.12.31",
"jsbi": "^3.2.5",
"siwe": "^3.0.0",
"viem": "^2.55.5"
},
"devDependencies": {
"@biomejs/biome": "^2.5.5",
"@cloudflare/vitest-pool-workers": "^0.18.7",
"@cloudflare/workers-types": "^5.20260722.1",
"@effect/vitest": "4.0.0-beta.100",
"@ethersproject/abi": "^5.8.0",
"@ethersproject/abstract-signer": "^5.8.0",
"@ethersproject/address": "^5.8.0",
"@ethersproject/solidity": "^5.8.0",
"@types/node": "^26.1.1",
"@vitest/coverage-istanbul": "4.1.10",
"ethers": "^5.8.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

latest version @uniswap/sdk-corenpm and does@uniswap/v4-sdk peer depend on ethers 5

💡 Result:

The latest version of @uniswap/sdk-core on npm as of July 22, 2026, is 7.18.0 [1][2][3]. Regarding @uniswap/v4-sdk, while older versions relied on ethers v5 and often caused compatibility issues due to direct imports (e.g., ethers/lib/utils) [4][5][6][7], the package has been migrated to support ethers v6 [8]. A pull request (feat: migrate v4-sdk and universal-router-sdk from ethers v5 to v6) removed the dependencies on @ethersproject/* packages and added ethers ^6.13.0 as a dependency [8]. As such, it does not peer depend on ethers v5 in its updated state [8].

Citations:


🌐 Web query:

latest npm version jsbi and @uniswap/v3-sdk peer dependencies ethers

💡 Result:

As of July 22, 2026, the latest version of the jsbi package on npm is 4.3.2 [1][2]. Regarding @uniswap/v3-sdk: 1. Peer Dependencies: The @uniswap/v3-sdk does not officially list ethers as a peer dependency in its package.json [3]. 2. Dependency Issues: The library has historically relied on ethers v5, and users have encountered issues when using it with ethers v6 due to breaking changes in the ethers library, such as the removal of specific internal paths (e.g., /lib/utils) [4]. Because ethers is not explicitly defined as a peer dependency, package managers may resolve it incorrectly or use incompatible versions if multiple versions exist in a project's dependency tree [4]. 3. Recommended Workarounds: Developers experiencing conflicts between @uniswap/v3-sdk and ethers v6 have utilized package manager features—such as overrides (npm), resolutions (yarn), or packageExtensions (pnpm)—to force the use of ethers version 5.7.2 to maintain compatibility [4].

Citations:


Update the dependency pins or document an explicit exception
jsbi is still on 3.2.5 while the current release is 4.3.2. The ethers@5 / @ethersproject/* pins also aren’t required by the published Uniswap SDK metadata, so if they stay for compatibility they should be called out as an explicit package exception.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/package.json` around lines 32 - 52, Update the dependency
declarations in package.json to use jsbi 4.3.2, and either remove the
unnecessary ethers@5 and `@ethersproject/`* entries or document an explicit
compatibility exception for retaining them.

Source: Coding guidelines

Comment thread apps/api/src/index.ts
Comment on lines +132 to +141
app.get("/ws/orderbook/:poolId", async (c) => {
const poolId = c.req.param("poolId")
if (!/^0x[a-fA-F0-9]{64}$/.test(poolId)) {
return c.json({ error: "Invalid poolId (must be 0x + 64 hex chars)" }, 400)
}
const url = new URL(c.req.url)
url.searchParams.set("poolId", poolId)
const id = c.env.ORDER_BOOK.idFromName(poolId)
return c.env.ORDER_BOOK.get(id).fetch(
new Request(url.toString(), { method: c.req.method, headers: c.req.raw.headers }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Canonicalize poolId before selecting the Durable Object.

The regex accepts mixed-case hex, but the raw spelling becomes both the DO name and forwarded snapshot key. The same bytes32 pool can therefore split into separate order books/snapshots by URL casing.

Proposed fix
   if (!/^0x[a-fA-F0-9]{64}$/.test(poolId)) {
     return c.json({ error: "Invalid poolId (must be 0x + 64 hex chars)" }, 400)
   }
+  const canonicalPoolId = poolId.toLowerCase()
   const url = new URL(c.req.url)
-  url.searchParams.set("poolId", poolId)
-  const id = c.env.ORDER_BOOK.idFromName(poolId)
+  url.searchParams.set("poolId", canonicalPoolId)
+  const id = c.env.ORDER_BOOK.idFromName(canonicalPoolId)

Add a regression test using alphabetic hex with different casing.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.get("/ws/orderbook/:poolId", async (c) => {
const poolId = c.req.param("poolId")
if (!/^0x[a-fA-F0-9]{64}$/.test(poolId)) {
return c.json({ error: "Invalid poolId (must be 0x + 64 hex chars)" }, 400)
}
const url = new URL(c.req.url)
url.searchParams.set("poolId", poolId)
const id = c.env.ORDER_BOOK.idFromName(poolId)
return c.env.ORDER_BOOK.get(id).fetch(
new Request(url.toString(), { method: c.req.method, headers: c.req.raw.headers }),
app.get("/ws/orderbook/:poolId", async (c) => {
const poolId = c.req.param("poolId")
if (!/^0x[a-fA-F0-9]{64}$/.test(poolId)) {
return c.json({ error: "Invalid poolId (must be 0x + 64 hex chars)" }, 400)
}
const canonicalPoolId = poolId.toLowerCase()
const url = new URL(c.req.url)
url.searchParams.set("poolId", canonicalPoolId)
const id = c.env.ORDER_BOOK.idFromName(canonicalPoolId)
return c.env.ORDER_BOOK.get(id).fetch(
new Request(url.toString(), { method: c.req.method, headers: c.req.raw.headers }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/index.ts` around lines 132 - 141, Canonicalize the validated
poolId in the /ws/orderbook/:poolId handler before using it for
ORDER_BOOK.idFromName or forwarding it as the poolId query parameter, preserving
the 0x prefix and using a consistent casing. Add a regression test with
alphabetic hex that compares differently cased URLs and verifies they select the
same Durable Object and snapshot key.

Comment on lines 52 to 53
tokens.get("/", async (c) => {
const limit = Math.min(Number.parseInt(c.req.query("limit") ?? "100", 10), 500)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Non-numeric limit silently yields an empty result set.

If limit is non-numeric, Number.parseInt returns NaN; Math.min(NaN, 500) is NaN, and the eventual .slice(0, NaN) in applyOptions (token-list.service.ts) coerces to .slice(0, 0) — the endpoint returns zero tokens instead of falling back to a sensible default.

🔧 Proposed fix
-  const limit = Math.min(Number.parseInt(c.req.query("limit") ?? "100", 10), 500)
+  const parsedLimit = Number.parseInt(c.req.query("limit") ?? "100", 10)
+  const limit = Math.min(Number.isNaN(parsedLimit) ? 100 : parsedLimit, 500)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tokens.get("/", async (c) => {
const limit = Math.min(Number.parseInt(c.req.query("limit") ?? "100", 10), 500)
const parsedLimit = Number.parseInt(c.req.query("limit") ?? "100", 10)
const limit = Math.min(Number.isNaN(parsedLimit) ? 100 : parsedLimit, 500)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/tokens.ts` around lines 52 - 53, Update the limit parsing
in the tokens GET handler to detect non-numeric or otherwise invalid parsed
values before applying Math.min. Fall back to the existing default limit of 100
for invalid input, while preserving the 500 maximum for valid numeric limits and
the existing applyOptions flow.

Comment on lines +126 to +130
export function poolIdOf(chainId: number, key: PoolKeyParams): string {
const token0 = new Token(chainId, key.token0, 18)
const token1 = new Token(chainId, key.token1, 18)
return Pool.getPoolId(token0, token1, key.fee, key.tickSpacing, key.hooks)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

@uniswap/sdk-core Token constructor address validation throw invalid address

💡 Result:

In the @uniswap/sdk-core library, the Token constructor validates the provided address to ensure it is a valid hexadecimal string [1][2]. If you are encountering an "invalid address" error or unexpected validation behavior, consider the following: 1. Checksum Validation: By default, the Token constructor validates that the address is a properly checksummed hex string [1]. If your address is not checksummed (i.e., it contains mixed-case letters that do not follow EIP-55 checksumming), the library will throw an error [1]. 2. Bypass Checksum: If you need to use an address that does not meet standard checksum requirements, you can pass true as the final argument (bypassChecksum) to the Token constructor [3][2]. This disables the strict checksum validation and performs only a "soft check" (verifying that the address string starts with "0x", has a length of 42, and contains valid hexadecimal characters) [1][2]. Constructor signature: new Token(chainId, address, decimals, symbol?, name?, bypassChecksum?) [3][2] If you continue to see errors, ensure the address string provided is exactly 42 characters long, starts with 0x, and contains valid hexadecimal digits (0-9, a-f, A-F) [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== chain-state-reader outline ==\n'
ast-grep outline apps/api/src/services/chain-state-reader.ts --view expanded || true

printf '\n== relevant symbols in chain-state-reader ==\n'
rg -n "poolIdOf|PoolKeyParams|token0|token1|new Token|OnChainReadError|Effect.sync|Effect.gen" apps/api/src/services/chain-state-reader.ts

printf '\n== PoolKeyParams definition and call sites ==\n'
rg -n "type PoolKeyParams|interface PoolKeyParams|PoolKeyParams" apps/api/src -g '!**/dist/**' -g '!**/build/**'

printf '\n== Token/address normalization helpers ==\n'
rg -n "toChecksumAddress|isAddress|getAddress|address.*normalize|normalize.*address|checksummed|lowercased" apps/api/src -g '!**/dist/**' -g '!**/build/**'

Repository: irfndi/AetherDEX

Length of output: 4392


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "token0|token1" apps/api/src -g '!**/dist/**' -g '!**/build/**' -g '!**/*.d.ts'

Repository: irfndi/AetherDEX

Length of output: 5264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== PoolKeyParams declaration ==\n'
rg -n -C 4 "PoolKeyParams" apps/api/src

printf '\n== chain-state-reader.ts around poolIdOf ==\n'
sed -n '1,220p' apps/api/src/services/chain-state-reader.ts

printf '\n== any address validation in this service ==\n'
rg -n -C 3 "isAddress|getAddress|assert.*address|validate.*address|checksumm" apps/api/src/services/chain-state-reader.ts

Repository: irfndi/AetherDEX

Length of output: 16347


Guard invalid pool token addresses
poolIdOf builds Tokens directly from key.token0/key.token1; malformed addresses will throw before the effect can map the failure to OnChainReadError. Validate or normalize both inputs first, or catch the constructor error here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/services/chain-state-reader.ts` around lines 126 - 130, Update
poolIdOf to validate or normalize key.token0 and key.token1 before constructing
Token instances, and ensure malformed addresses are handled as the expected
OnChainReadError rather than escaping as constructor exceptions. Preserve the
existing Pool.getPoolId behavior for valid addresses.

@@ -0,0 +1,218 @@
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

D1 token cache and shared TokenSchema have no chain scoping. The tokens D1 table's write/read paths and the new shared TokenSchema all omit chain_id, even though deps.chainId is already available in the service. Per coding guidelines, schemas and queries must include chain_id in composite keys/filters before indexing a second chain — today, with address as the sole D1 key, two different chains' tokens sharing an address would overwrite/leak into each other's cache.

  • apps/api/src/services/token-list.service.ts#L126-145: add chain_id to the INSERT/ON CONFLICT target (requires a migration adding the column) so cross-chain writes don't collide on address alone.
  • apps/api/src/services/token-list.service.ts#L171-180: filter readD1Cache's SELECT by deps.chainId so the D1 fallback can't return another chain's tokens.
  • packages/shared/src/schema.ts#L42-53: add a chainId field to TokenSchema so the API contract reflects per-chain token identity end-to-end.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/services/token-list.service.ts` at line 1, The token cache and
shared TokenSchema lack chain scoping, allowing tokens with the same address on
different chains to collide or leak. Update the token-list service INSERT/ON
CONFLICT path to include chain_id using deps.chainId, add the required migration
and composite key support, and update readD1Cache to filter by deps.chainId;
also add chainId to TokenSchema so the API contract preserves per-chain
identity.

Source: Coding guidelines

Comment on lines +84 to +97
function toTokenInfo(token: ValidatedToken): TokenInfo {
return {
address: token.address,
symbol: token.symbol,
name: token.name,
decimals: token.decimals,
logoUrl: token.logoURI,
isVerified: true,
isNative: false,
totalSupply: null,
createdAt: Date.now(),
updatedAt: Date.now(),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cached tokens get fabricated timestamps on every read.

toTokenInfo sets createdAt/updatedAt to Date.now() at call time. Since readCache re-maps the same cached ValidatedToken[] (which carries no timestamp) through toTokenInfo on every request, every KV-served response reports "now" as both created/updated time, not when the token was actually cached or last refreshed. This misleads any client sorting/filtering on freshness.

🕒 Proposed fix: stamp the refresh time once, reuse for cached reads
-function toTokenInfo(token: ValidatedToken): TokenInfo {
+function toTokenInfo(token: ValidatedToken, asOf: number): TokenInfo {
   return {
     address: token.address,
     symbol: token.symbol,
     name: token.name,
     decimals: token.decimals,
     logoUrl: token.logoURI,
     isVerified: true,
     isNative: false,
     totalSupply: null,
-    createdAt: Date.now(),
-    updatedAt: Date.now(),
+    createdAt: asOf,
+    updatedAt: asOf,
   }
 }

Store asOf alongside the KV payload (or derive it once per refresh() call) and thread it through both readCache and refresh.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/services/token-list.service.ts` around lines 84 - 97, Update the
token cache flow around toTokenInfo, readCache, and refresh so timestamps are
captured once per cache refresh and reused on every cached read. Store or
propagate a shared refresh-time value alongside the cached token data, then pass
it into toTokenInfo instead of calling Date.now() for each mapped token;
preserve the existing timestamps for newly refreshed tokens.

Comment on lines +46 to +52
ws.send(JSON.stringify({ type: "subscribe", poolId: POOL_ID }))
const subscribed = (await nextMessage(ws)) as { type: string; data: { poolId: string } }
expect(subscribed.type).toBe("subscribed")
expect(subscribed.data.poolId).toBe(POOL_ID)

ws.send(JSON.stringify({ type: "ping" }))
const pong = (await nextMessage(ws)) as { type: string }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Install each message listener before sending.

A response can arrive before nextMessage attaches its listener, leaving the test waiting indefinitely. Create the receive promise first for both frames.

Proposed fix
-    ws.send(JSON.stringify({ type: "subscribe", poolId: POOL_ID }))
-    const subscribed = (await nextMessage(ws)) as { type: string; data: { poolId: string } }
+    const subscribedMessage = nextMessage(ws)
+    ws.send(JSON.stringify({ type: "subscribe", poolId: POOL_ID }))
+    const subscribed = (await subscribedMessage) as { type: string; data: { poolId: string } }
@@
-    ws.send(JSON.stringify({ type: "ping" }))
-    const pong = (await nextMessage(ws)) as { type: string }
+    const pongMessage = nextMessage(ws)
+    ws.send(JSON.stringify({ type: "ping" }))
+    const pong = (await pongMessage) as { type: string }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ws.send(JSON.stringify({ type: "subscribe", poolId: POOL_ID }))
const subscribed = (await nextMessage(ws)) as { type: string; data: { poolId: string } }
expect(subscribed.type).toBe("subscribed")
expect(subscribed.data.poolId).toBe(POOL_ID)
ws.send(JSON.stringify({ type: "ping" }))
const pong = (await nextMessage(ws)) as { type: string }
const subscribedMessage = nextMessage(ws)
ws.send(JSON.stringify({ type: "subscribe", poolId: POOL_ID }))
const subscribed = (await subscribedMessage) as { type: string; data: { poolId: string } }
expect(subscribed.type).toBe("subscribed")
expect(subscribed.data.poolId).toBe(POOL_ID)
const pongMessage = nextMessage(ws)
ws.send(JSON.stringify({ type: "ping" }))
const pong = (await pongMessage) as { type: string }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/test/ws-routing.test.ts` around lines 46 - 52, In the WebSocket test
flow around nextMessage, create each receive promise before sending its
corresponding subscribe or ping message, then await that promise for the
subscribed and pong responses. Preserve the existing response assertions while
ensuring listeners are attached before ws.send executes.

@irfndi
irfndi merged commit 5e8d65a into main Jul 22, 2026
9 of 16 checks passed
@irfndi
irfndi deleted the feat/phase0-app branch July 22, 2026 17:09
irfndi added a commit that referenced this pull request Jul 23, 2026
* fix: Phase-0 review follow-up for merged PRs #306 + #307

Contracts (PR #306 review):
- AetherHook.getTwapTick: floor negative, non-divisible avg ticks toward
  negative infinity instead of truncating toward zero (-70000/90 = -778,
  not -777), removing the upward TWAP bias that could spuriously cross
  TP/SL thresholds
- AetherHook._tickToPriceX18: compute the reciprocal at full precision
  directly from sqrtPriceX96 (Q96^2*1e18/sqrtP^2) instead of inverting the
  already-rounded direct quote; reverse-direction keeper reads stay
  accurate and representable even when the direct price rounds to zero
- Tests: floor expectations, full-precision reciprocal helper, reciprocal
  readable at extreme negative ticks, corrected stale docstring
- Invariant fuzz handler tracks the oldest RETAINED ring observation and
  clamps the TWAP window to it, so ring overflow no longer fails the
  invariant on buffer bounds instead of TWAP correctness

Apps (PR #307 review):
- api tokens route: non-numeric limit falls back to the default (100)
  instead of NaN -> slice(0,0) -> empty result
- api ws routing: canonicalize poolId (toLowerCase) before DO selection
  and snapshot key so mixed-case ids map to one order book
- api token-list: chain-scoped tokens table (migration 0003, composite PK
  (chain_id, address)) + chain_id in INSERT/ON CONFLICT, D1 read filter,
  TokenSchema.chainId, chain-scoped cron refresh
- api token-list: stamp timestamps once per refresh and reuse on cached
  reads (no fabricated per-request Date.now())
- api chain-state-reader: validate/EIP-55-normalize pool addresses before
  Token construction (invalid_pool_key), anchor the tick scan to
  tickSpacing boundaries and expose the verified tick window
- api quote-engine: reject simulations that exit the verified tick window
  (price_out_of_range) instead of quoting unverified territory
- api swap.service: legacy rollback only when the reader is explicitly
  not_configured, never after an authoritative V4 quote failure
- api websocket hub: per-token price_update payload {tokenAddress, price,
  updatedAt} published by the price-refresh queue pipeline (was a
  handshake-only route); web PriceTicker unwraps the envelope and filters
  by token (no more undefined.toFixed crash)
- web TokenSearch: mark default list loaded only on a retained success so
  failed/cancelled loads retry on reopen
- api: document jsbi@3 + ethers@5 test-pool dependency exceptions

* fix(api): harden R1 review findings on PR #308

- migration 0003: rebuild FK dependents children-first (transactions,
  liquidity_positions, pools) before dropping tokens, so the table swap
  cannot fail under SQLite foreign-key enforcement; PRAGMA guards +
  foreign_key_check; verified with d1 migrations apply --local
- token.service: scope every tokens-table read to the current chain via
  TokenServiceDeps { chainId } (getToken can no longer pick another
  chain's row nondeterministically)
- queue-handler: only broadcast prices > 0 — a failed CoinGecko lookup
  (0 sentinel) must not overwrite a ticker's last good value with $0
- tokens route + token-list service: treat negative limit as invalid
  (default 100) — slice(0, -n) would silently drop trailing entries
- tests: timestamp-stability test proves the cache path via fetcher.calls;
  negative-limit regression; TokenServiceDeps provided in storage tests

* fix(api): restore pool_id FKs post-migration + hibernation-safe hub fan-out

- migration 0003 (R2 follow-up): second pass rebuilds transactions and
  liquidity_positions AFTER pools exists again, restoring their
  FOREIGN KEY (pool_id) REFERENCES pools(pool_id) — the FKs are only
  dropped mid-swap, not permanently; final schema keeps the full
  integrity graph minus the intentionally retired pools->tokens(address)
  FK (tokens PK is now (chain_id, address))
- websocket-hub-do: rehydrate the in-memory subscriber map from
  ctx.getWebSockets() before broadcasts and message handling, so price
  fan-out survives DO hibernation between refreshes (sockets stay
  connected but the map does not); watched keys are persisted in the
  socket attachment and restored on hydration
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant