feat(api,web): Phase 0 application-layer items G1–G4 (WS routes, V4 quote engine, Effect routing, default token list)#307
Conversation
- 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.
There was a problem hiding this comment.
Sorry @irfndi, your pull request is larger than the review limit of 150000 diff characters
WalkthroughThe 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. ChangesV4 quote and swap flow
Position service migration
Token-list flow
Web token search
WebSocket routing
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)V4 quote request flowsequenceDiagram
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
Token-list resolution flowsequenceDiagram
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
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.jsonTraceback (most recent call last): 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. Comment |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
💡 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".
| for (let k = 1; k <= scan; k += 1) { | ||
| candidates.push(tick - k * key.tickSpacing) | ||
| candidates.push(tick + k * key.tickSpacing) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| 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) |
There was a problem hiding this comment.
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 👍 / 👎.
| ? makeStateViewReaderLayer({ | ||
| rpcUrl, | ||
| stateViewAddress: stateViewAddress as `0x${string}`, | ||
| chainId, | ||
| tickScanEachSide: 64, | ||
| }) |
There was a problem hiding this comment.
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 👍 / 👎.
|
|
||
| // Load the default-list top tokens once, when the picker first opens. | ||
| useEffect(() => { | ||
| if (!isOpen || defaultsLoadedRef.current) return | ||
| defaultsLoadedRef.current = true |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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 winStale search responses can overwrite newer ones (no request cancellation).
searchTokensdoesn't pass anAbortSignalor 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. BothfetchTokenByAddress/fetchTokensalready accept asignal.🔧 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_REandHEX_ADDRESS_REare byte-for-byte identical. The rename left both constants in place, so validation now splits across two duplicate patterns (handlers use one,swapServiceLayerthe 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_REuses (Lines 101, 143) withHEX_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 winBound the tick-scan RPC fan-out
createPublicClient({ transport: http(config.rpcUrl) })sends eachreadContractas a separateeth_call, so the live path will burst2 * tickScanEachSidetick 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 winNo
chain_idin theliquidity_positionsfilter/schema.
listByUserfilters only onuser_address/is_active, andLiquidityPositionhas nochainIdfield. As per coding guidelines, "Data schemas and queries must includechain_idin composite keys and filters before indexing a second chain." GivenCHAIN_IDis already a binding and this PR is building multi-chain plumbing (ChainStateReader), addingchain_idnow — 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 winLeftover
verifiedfilter: declared/documented but not implemented. The AI summary confirmsverifiedquery 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=truefilters results.
apps/api/src/services/token-list.service.ts#L65-69: remove the unusedverifiedfield fromTokenSearchOptions(or implement it inmatchesQuery/applyOptionsif filtering is actually desired).apps/api/src/routes/tokens.ts#L49-51: update the JSDoc to drop theverified=trueexample 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
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (28)
apps/api/.gitignoreapps/api/package.jsonapps/api/src/db/queries.tsapps/api/src/index.tsapps/api/src/lib/token-list.tsapps/api/src/routes/positions.tsapps/api/src/routes/swap.tsapps/api/src/routes/tokens.tsapps/api/src/services/chain-state-reader.tsapps/api/src/services/index.tsapps/api/src/services/position.service.tsapps/api/src/services/quote-engine.tsapps/api/src/services/swap.service.tsapps/api/src/services/token-list.service.tsapps/api/test/db/queries.test.tsapps/api/test/services/chain-state-reader.test.tsapps/api/test/services/position.service.test.tsapps/api/test/services/quote-engine.test.tsapps/api/test/services/token-list.test.tsapps/api/test/shims/ethers-worker-shim.tsapps/api/test/ws-routing.test.tsapps/api/vitest.config.tsapps/api/worker-configuration.d.tsapps/api/wrangler.jsoncapps/web/src/components/TokenSearch.tsxapps/web/src/lib/api.tsapps/web/test/components/TokenSearch.test.tsxpackages/shared/src/schema.ts
💤 Files with no reviewable changes (2)
- apps/api/.gitignore
- apps/api/test/db/queries.test.ts
| "@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", |
There was a problem hiding this comment.
📐 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:
- 1: https://registry.npmjs.org/%40uniswap%2Fsdk-core
- 2: https://npmx.dev/package/@uniswap/sdk-core
- 3: https://npmx.dev/package/@uniswap/sdk-core/v/7.18.0
- 4: You must mention all peer dependencies in package.json Uniswap/sdks#227
- 5: Using ethers v6 providers results in error Uniswap/smart-order-router#383
- 6: can not import { Actions,V4Planner } from '@uniswap/v4-sdk' Uniswap/sdks#379
- 7: https://github.com/Uniswap/sdks/blob/9cf6edb2df79338ae58f7ea7ca979c35a8a9bd56/sdks/v4-sdk/src/utils/v4Planner.ts
- 8: feat: migrate v4-sdk and universal-router-sdk from ethers v5 to v6 Uniswap/sdks#564
🌐 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:
- 1: https://www.npmjs.com/package/jsbi
- 2: https://registry.npmjs.org/jsbi
- 3: https://www.npmjs.com/package/@uniswap/v3-sdk?activeTab=dependencies
- 4: You must mention all peer dependencies in package.json Uniswap/sdks#227
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
| 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 }), |
There was a problem hiding this comment.
🗄️ 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.
| 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.
| tokens.get("/", async (c) => { | ||
| const limit = Math.min(Number.parseInt(c.req.query("limit") ?? "100", 10), 500) |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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) | ||
| } |
There was a problem hiding this comment.
🩺 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:
- 1: feat: Adds bypassChecksum to Token constructor to stop checksum validation by default Uniswap/sdk-core#35
- 2: https://github.com/etcswap/v3-docs/blob/main/docs/sdk/core/reference/classes/Token.md
- 3: https://tessl.io/registry/tessl/npm-uniswap--sdk-core/7.7.0
🏁 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.tsRepository: 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 @@ | |||
| /** | |||
There was a problem hiding this comment.
🗄️ 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: addchain_idto theINSERT/ON CONFLICTtarget (requires a migration adding the column) so cross-chain writes don't collide onaddressalone.apps/api/src/services/token-list.service.ts#L171-180: filterreadD1Cache'sSELECTbydeps.chainIdso the D1 fallback can't return another chain's tokens.packages/shared/src/schema.ts#L42-53: add achainIdfield toTokenSchemaso 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
| 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(), | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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 } |
There was a problem hiding this comment.
🎯 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.
| 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.
* 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
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/:tokenAddress→WebSocketHubDO(single hub instance; live price fan-out — matches webPriceTicker/useWebSocket)GET /ws/orderbook/:poolId→OrderBookDOkeyed by pool id (poolId forwarded as query for snapshot load; 400 on invalid id)G2 — Real V4 tick-math quote engine (the critical correctness item)
services/quote-engine.ts: exact-input simulation on@uniswap/v4-sdkPoolover a chain-state snapshot (slot0sqrtPriceX96+ in-range liquidity + initialized ticks withliquidityNet) — swaps that cross ticks are stepped correctly (single- and multi-tick crossings), pool fee applied (V4SwapMathsemantics), raw price impactservices/chain-state-reader.ts:ChainStateReaderEffect service abstracts the on-chain read — mock + unconfigured layers (tests / pre-deployment) and a live StateView impl via viem (getSlot0+getLiquidity+ windowedgetTickLiquidityscan; replaced by the Phase-3 indexer). Addresses come only from env (STATE_VIEW_ADDRESS,POOL_MANAGER_ADDRESS,RPC_URL) — nothing hardcodedSwapServicerewritten: pool metadata viaPoolService, state viaChainStateReader;QUOTE_ENGINE_MODE(autodefault) keeps the legacy constant-product path as rollback per plan §9 (auto→legacy when on-chain reads unconfigured)./quoteHTTP shape unchanged1e18 → 5e17) plus an independent per-segmentSwapMathoracle for fee-3000 single-tick and 1-/2-tick crossings;liquidityNetapplication asserted via post-swap liquidityG3 — Effect routing verified/completed
PositionService(Context.Service + Layer.effect + SqlClient);/positionshandlers route through it — the old raw Effect queries for positions are removedPoolService(no rawdb.prepareleft 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 rejectedTokenListService(Effect): KV cache (6h) → fresh fetch/re-cache → D1 write-through cache fallback; the D1tokenstable is strictly a cache of the default list, never a curated source/tokens+/tokens/:addressserve throughTokenListService(env:TOKEN_LIST_URL,CHAIN_ID)TokenSchema/TokenListResponseSchema/TokenResponseSchema; webTokenSearchconsumes/tokens(default-list top tokens on open, debounced search, single-address lookup) — 4-token hardcode removedAlso in this PR
worker-configuration.d.ts(wrangler types) is now committed — apitypecheckwas red on main (all Cloudflare binding types unresolved); now greenethers@5ESM (circular named exports) — vitest CJS aliases + a small viem-backed shim (test/shims/) for the test/worker pipeline only; production still bundles the real packagesVerification matrix (all from worktree root, exit codes)
bun installbun run typecheck(root)cd apps/api && bun run typecheckcd apps/web && bun run typecheckbun run lint(Biome)bun run test(root: 9 files / 88 tests)cd apps/api && bun run test(7 files / 40 tests)cd apps/web && bun run buildcd apps/api && bun run build(wrangler dry-run)Follow-ups (not in scope)
STATE_VIEW_ADDRESS/POOL_MANAGER_ADDRESS/RPC_URL/ROUTER_ADDRESS/FACTORY_ADDRESSafter Sepolia deployment — the quote path automatically goes live withQUOTE_ENGINE_MODE=autoonce they are setchainIdfilter 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 taskQuotersimulation 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 landsgetTickLiquidityscan; Phase-2 on-chain position reconciliation (plan gap) still openSummary by CodeRabbit
New Features
Bug Fixes