fix: Phase-0 review follow-up for merged PRs #306 + #307#308
Conversation
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
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Warning Review limit reached
Next review available in: 16 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
WalkthroughThis change scopes token data by chain, adds token-based price WebSocket updates, constrains V4 quotes to verified tick windows, normalizes orderbook keys, and revises contract TWAP rounding, reciprocal pricing, and invariant modeling. ChangesAPI data and quote flows
Contract TWAP precision
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant QueueHandler
participant WebSocketHubDO
participant PriceTicker
QueueHandler->>WebSocketHubDO: POST token price update
WebSocketHubDO->>PriceTicker: price_update envelope
PriceTicker->>PriceTicker: filter matching token address
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9eb47a05f4
ℹ️ 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".
| (chain_id, address, symbol, name, decimals, logo_url, is_verified, is_native, total_supply, created_at, updated_at) | ||
| SELECT 1, address, symbol, name, decimals, logo_url, is_verified, is_native, total_supply, created_at, updated_at | ||
| FROM tokens; | ||
| DROP TABLE tokens; |
There was a problem hiding this comment.
Rebuild foreign-key dependents before dropping tokens
When this migration runs against a database containing any pool row, pools.token0_address and token1_address still reference tokens(address), so with foreign-key enforcement enabled this DROP TABLE tokens fails immediately with FOREIGN KEY constraint failed; the later pools_v2 rebuild is never reached. The migration therefore cannot upgrade a non-empty D1 database and needs a foreign-key-safe/deferred rebuild of the dependency graph.
Useful? React with 👍 / 👎.
| INSERT INTO tokens (chain_id, address, symbol, name, decimals, logo_url, is_verified, is_native, total_supply, created_at, updated_at) | ||
| VALUES (${token.chainId}, ${token.address}, ${token.symbol}, ${token.name}, ${token.decimals}, ${token.logoUrl}, ${token.isVerified ? 1 : 0}, ${token.isNative ? 1 : 0}, ${token.totalSupply}, ${Date.now()}, ${Date.now()}) | ||
| ON CONFLICT(chain_id, address) DO UPDATE SET |
There was a problem hiding this comment.
Add chain filters to every TokenService read
Once the new composite key stores the same address for two chains, this chain-scoped upsert can create both rows, but getToken, listTokens, searchTokens, and getVerifiedTokens still query without chain_id and the service has no current-chain dependency. Consequently getToken(address) can select either chain nondeterministically and list/search calls mix chains; scope every read by an explicit chain ID just as the write is scoped.
AGENTS.md reference: AGENTS.md:L24-L24
Useful? React with 👍 / 👎.
| await hub.fetch( | ||
| new Request("http://price-hub/price", { | ||
| method: "POST", | ||
| body: JSON.stringify({ tokenAddress: token, price: priceUsd, updatedAt }), |
There was a problem hiding this comment.
Reject failed price lookups before broadcasting
When CoinGecko returns a non-2xx response, lacks the token, or throws—and for the configured Sepolia environment the helper still queries the Ethereum endpoint—fetchTokenPrice returns 0 rather than failing. This new broadcast therefore replaces a connected ticker's last valid value with $0.00000000; validate priceUsd > 0 or propagate the fetch failure before publishing the update.
AGENTS.md reference: AGENTS.md:L18-L18
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
apps/api/src/workers/queue-handler.ts (1)
74-97: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winSequential, un-timed-out hub notification adds latency risk per token.
Each token iteration now does 3 sequential network calls (price fetch, KV put,
hub.fetch), and the newhub.fetchcall has no timeout/AbortSignal. If the Durable Object stalls, this loop blocks the whole queue message formsg.tokens.lengthiterations instead of failing fast.♻️ Suggested guard
await hub.fetch( new Request("http://price-hub/price", { method: "POST", body: JSON.stringify({ tokenAddress: token, price: priceUsd, updatedAt }), + signal: AbortSignal.timeout(5_000), }), )🤖 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/workers/queue-handler.ts` around lines 74 - 97, Update the token-processing loop around hub.fetch so the Durable Object notification uses a bounded timeout via an AbortSignal, failing fast if the hub stalls. Preserve the existing per-token error handling and price/KV updates, while ensuring a timed-out notification does not block subsequent tokens for the full queue message.apps/api/src/durable-objects/websocket-hub-do.ts (1)
111-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPool-flavored naming leftover for token subscriptions.
watchedKeys/knownTokenswere renamed to reflect the token model, but the wire protocol still usesmsg.poolId,"list_pools", and"pool_list"— confusing for anyone subscribing to this price hub with a token address under a field/message literally named "pool".🤖 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/durable-objects/websocket-hub-do.ts` around lines 111 - 125, Update the websocket subscription protocol in the subscribe/unsubscribe and list_pools handlers to use token-oriented naming consistently: replace msg.poolId and poolId payload fields with the established token identifier name, and rename the list request/response message types from pool-based names to token-based names. Keep watchedKeys and knownTokens behavior unchanged.
🤖 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/migrations/0003_chain_scoped_tokens.sql`:
- Around line 27-28: Update the migration’s table-rebuild sequence around
tokens_chain_scoped and the dependent pools, transactions, and
liquidity_positions tables so foreign-key dependencies are handled before
dropping referenced tables. Either rebuild dependent tables first or explicitly
disable enforcement for the atomic migration, then run foreign_key_check before
committing and preserve the intended final schema.
In `@apps/api/src/routes/tokens.ts`:
- Around line 53-56: Reject or default negative token limits before they reach
slicing: update the parsed-limit handling in tokens.ts to treat values below
zero like invalid input, and add the same non-negative lower-bound enforcement
in the token-list service limit handling for non-HTTP callers. Apply the change
at apps/api/src/routes/tokens.ts lines 53-56 and
apps/api/src/services/token-list.service.ts lines 199-204.
In `@apps/api/src/workers/cron-handler.ts`:
- Around line 79-86: Move the database and queue operations in refreshTopPools()
and enqueuePriceRefresh() behind the existing Db and WorkerEnv Effect services,
replacing direct env.DB.prepare(...) and env.PRICE_QUEUE.send(...) usage. Run
these Effect workflows with Layer.provide, and preserve failures through
structured Effect error handling rather than unhandled raw promise errors.
In `@apps/api/test/services/token-list.test.ts`:
- Around line 164-174: Update the “stamps timestamps once per refresh” test to
assert fetcher.calls() equals 1 after both listTokens reads, proving the second
read used the KV cache; keep the existing timestamp assertions unchanged.
---
Nitpick comments:
In `@apps/api/src/durable-objects/websocket-hub-do.ts`:
- Around line 111-125: Update the websocket subscription protocol in the
subscribe/unsubscribe and list_pools handlers to use token-oriented naming
consistently: replace msg.poolId and poolId payload fields with the established
token identifier name, and rename the list request/response message types from
pool-based names to token-based names. Keep watchedKeys and knownTokens behavior
unchanged.
In `@apps/api/src/workers/queue-handler.ts`:
- Around line 74-97: Update the token-processing loop around hub.fetch so the
Durable Object notification uses a bounded timeout via an AbortSignal, failing
fast if the hub stalls. Preserve the existing per-token error handling and
price/KV updates, while ensuring a timed-out notification does not block
subsequent tokens for the full queue message.
🪄 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: 4f6db915-50ac-457a-8a1d-57b74840bd7a
📒 Files selected for processing (24)
apps/api/README.mdapps/api/migrations/0003_chain_scoped_tokens.sqlapps/api/src/db/queries.tsapps/api/src/db/schema.tsapps/api/src/durable-objects/websocket-hub-do.tsapps/api/src/index.tsapps/api/src/routes/tokens.tsapps/api/src/services/chain-state-reader.tsapps/api/src/services/quote-engine.tsapps/api/src/services/swap.service.tsapps/api/src/services/token-list.service.tsapps/api/src/services/token.service.tsapps/api/src/workers/cron-handler.tsapps/api/src/workers/queue-handler.tsapps/api/test/services/token-list.test.tsapps/api/test/ws-routing.test.tsapps/web/src/components/PriceTicker.tsxapps/web/src/components/TokenSearch.tsxapps/web/test/components/TokenSearch.test.tsxpackages/contracts/src/hook/AetherHook.solpackages/contracts/test/fuzz/AetherHookInvariants.t.solpackages/contracts/test/unit/AetherHook.t.solpackages/contracts/test/unit/AetherHookTwap.t.solpackages/shared/src/schema.ts
| // Refresh verified tokens every 5 minutes — scoped to this chain: tokens are keyed | ||
| // by (chain_id, address) and another chain's tokens must not be refreshed here. | ||
| const chainId = Number.parseInt(env.CHAIN_ID, 10) | ||
| const tokens = await env.DB.prepare("SELECT address FROM tokens WHERE is_verified = 1 AND chain_id = ? LIMIT 200") | ||
| .bind(Number.isNaN(chainId) ? 1 : chainId) | ||
| .all<{ | ||
| address: string | ||
| }>() |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file outline =="
ast-grep outline apps/api/src/workers/cron-handler.ts --view expanded || true
echo "== relevant lines =="
cat -n apps/api/src/workers/cron-handler.ts | sed -n '1,180p'
echo "== git diff stat =="
git diff --stat || true
echo "== diff for file =="
git diff -- apps/api/src/workers/cron-handler.ts | sed -n '1,260p' || true
echo "== Effect imports/services in workers/api layer =="
rg -n "Effect\.catch|Class\.Service|Context\.Service|Layer\.effect|`@effect`|env\.DB\.prepare|`@hono/effect`" apps/api/src/workers apps/api/src -g '*.ts' | head -n 200 || trueRepository: irfndi/AetherDEX
Length of output: 11067
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== SQL client/service examples =="
for f in apps/api/src/db/client.ts apps/api/src/services/pool.service.ts apps/api/src/services/token.service.ts apps/api/src/services/position.service.ts apps/api/src/services/swap.service.ts; do
echo "--- $f"
wc -l "$f"
sed -n '1,180p' "$f"
done
echo "== route imports and environment binding =="
rg -n "CronEnv|handleScheduled|WorkerEnv|Db|Layer.effect|Context.Service<.*Db>|Service<.*Db>" apps/api/src -g '*.ts' | sed -n '1,260p'Repository: irfndi/AetherDEX
Length of output: 24938
Move the cron refresh queries into Effect services.
refreshTopPools() and enqueuePriceRefresh() still use raw env.DB.prepare(...)/env.PRICE_QUEUE.send(...) in a non-Effect worker implementation. These are backend data operations, so move them behind Db/WorkerEnv Effect services via Layer.provide and structured error handling.
🤖 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/workers/cron-handler.ts` around lines 79 - 86, Move the database
and queue operations in refreshTopPools() and enqueuePriceRefresh() behind the
existing Db and WorkerEnv Effect services, replacing direct env.DB.prepare(...)
and env.PRICE_QUEUE.send(...) usage. Run these Effect workflows with
Layer.provide, and preserve failures through structured Effect error handling
rather than unhandled raw promise errors.
Source: Coding guidelines
- 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
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/migrations/0003_chain_scoped_tokens.sql`:
- Around line 18-20: Update the rebuilt transactions and liquidity_positions
schemas to restore foreign keys from pool_id to pools(pool_id), while retaining
the existing users(address) foreign keys. If necessary, stage child data during
the pools rebuild, then ensure the final table materialization enforces both
relationships and passes foreign_key_check.
🪄 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 Plus
Run ID: ac73d74c-2353-4fd1-a5c4-f81a87aa6d88
📒 Files selected for processing (7)
apps/api/migrations/0003_chain_scoped_tokens.sqlapps/api/src/routes/tokens.tsapps/api/src/services/token-list.service.tsapps/api/src/services/token.service.tsapps/api/src/workers/queue-handler.tsapps/api/test/services/storage-errors.test.tsapps/api/test/services/token-list.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/api/src/routes/tokens.ts
- apps/api/src/workers/queue-handler.ts
- apps/api/test/services/token-list.test.ts
- apps/api/src/services/token-list.service.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fd3088aa88
ℹ️ 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".
| await hub.fetch( | ||
| new Request("http://price-hub/price", { | ||
| method: "POST", | ||
| body: JSON.stringify({ tokenAddress: token, price: priceUsd, updatedAt }), | ||
| }), | ||
| ) |
There was a problem hiding this comment.
Rehydrate hibernating WebSocket subscribers before broadcasting
When the hub hibernates between the five-minute refreshes, this POST wakes a new Durable Object instance whose in-memory subscribers map is empty, although the sockets accepted through ctx.acceptWebSocket remain connected. The broadcast is therefore sent to nobody and existing PriceTicker clients stop receiving updates after hibernation; reconstruct subscribers from ctx.getWebSockets() (and persisted attachments for watched keys) before fan-out.
AGENTS.md reference: AGENTS.md:L104-L105
Useful? React with 👍 / 👎.
| block_timestamp INTEGER NOT NULL, | ||
| status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'confirmed' | 'failed' | ||
| created_at INTEGER NOT NULL, | ||
| FOREIGN KEY (user_address) REFERENCES users(address) |
There was a problem hiding this comment.
Preserve pool foreign keys after rebuilding dependents
After this migration, transactions.pool_id and liquidity_positions.pool_id permanently lose the foreign keys present in 0001_initial_schema.sql; only the user foreign key is recreated. Consequently, future inserts can reference nonexistent pools and deleting a pool can leave orphaned transactions and positions, while the final foreign_key_check cannot detect these future violations. Rebuild the dependent tables again with FOREIGN KEY (pool_id) REFERENCES pools(pool_id) after the parent rebuild is complete.
Useful? React with 👍 / 👎.
…an-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
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ab2116b61
ℹ️ 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".
|
|
||
| // The hub broadcasts ALL token prices (it is a single shared instance); keep the | ||
| // latest payload for THIS ticker's token, unwrapped out of the frame envelope. | ||
| const [data, setData] = useState<PricePayload | null>(null) |
There was a problem hiding this comment.
Clear the retained price when the token changes
When the chart route is reused with a different tokenAddress, this local state retains the previous token's payload; the effect ignores the old envelope because it no longer matches, but never resets data. The ticker therefore displays the old token's price until the new token receives its next refresh, potentially several minutes later. Reset the retained payload when tokenAddress or chainId changes.
AGENTS.md reference: AGENTS.md:L18-L18
Useful? React with 👍 / 👎.
| ); | ||
| INSERT OR IGNORE INTO tokens_chain_scoped | ||
| (chain_id, address, symbol, name, decimals, logo_url, is_verified, is_native, total_supply, created_at, updated_at) | ||
| SELECT 1, address, symbol, name, decimals, logo_url, is_verified, is_native, total_supply, created_at, updated_at |
There was a problem hiding this comment.
Preserve the deployed chain when migrating token rows
This rewrites every existing token as chain 1, although the repository's default and staging deployments use Sepolia (CHAIN_ID=11155111). When upgrading a Sepolia D1 database with cached rows, those rows become invisible to the newly chain-filtered Sepolia reads, so the D1 fallback fails during an upstream/KV outage; if the database is later read as mainnet, the mislabeled metadata can also be served for the wrong chain. The migration must assign the source database's actual chain or deliberately discard and repopulate this cache rather than hard-code mainnet.
AGENTS.md reference: AGENTS.md:L24-L24
Useful? React with 👍 / 👎.
Follow-up PR addressing the review findings on the merged PR #306 (G2.5 TWAP oracle) and PR #307 (Phase-0 apps G1–G4).
#306 contracts
getTwapTick(e.g. -70000/90 → -778); tests updated to floor expectations incl. the -778 vectorQ96²·1e18/sqrtP²straight fromsqrtPriceX96; representable + accurate even when the direct price floors to zero (test rewritten accordingly)#307 apps
limit→ empty resultNumber.isFiniteclamp in the service)(chain_id, address); chain_id in INSERT/ON CONFLICT;WHERE chain_idon D1 fallback + cron refresh;TokenSchema.chainIdasOfstamped once per refresh, stored in the KV envelope, reused on reads (regression test added)isAddress+getAddressbeforeToken; constructor failures map toOnChainReadError("invalid_pool_key")floor(tick/spacing)·spacing ± k·spacing, anchor included)not_configured; authoritative failures propagatePOST /priceafter each KV write{tokenAddress, price, updatedAt}envelope; PriceTicker unwraps + filters by token (noundefined.toFixedcrash)verifiedTickWindowin state; engine rejects out-of-window simulations (price_out_of_range)loadedflag set only on retained successtoLowerCase()before DO selection + forwarded key (+ regression test)jsbi@3/ethers@5documented as explicit test-pool exceptions in apps/api/README.mdVerification (all exit 0)
forge build,forge test— 134 passingbun run typecheck,bun run lint,bun run test— 88 passingtypecheck,test— 44 passing,build(wrangler dry-run)build,typecheckSummary by CodeRabbit
New Features
Bug Fixes
limitparsing to prevent NaN/negative pagination issues.Documentation
Tests