Cursor pagination, buy rate limiting, body validation, and volume leaderboard - #789
Merged
Chucks1093 merged 1 commit intoAug 26, 2026
Conversation
…me leaderboard Resolves four backend hardening/discovery issues: - Add cursor-based (keyset) pagination to GET /creators/:id/holders alongside the existing offset mode, using the wallet address as the cursor via the existing signed-cursor utility. - Add a per-wallet sliding-window rate limiter (5 req / 10s, Redis-backed) to the key purchase endpoint, with a 429 + Retry-After response, breach logging, and an internal-service bypass header. - Add a centralized validateBody Zod middleware and apply it to the auth register, key purchase, post creation, and profile update routes so invalid bodies are rejected with a structured 422 before reaching business logic and unknown fields are stripped. - Add GET /creators/leaderboard/volume returning the top 20 creator keys by total trading volume over a rolling window, cached in Redis (5 min TTL) and invalidated whenever the indexer records a new trade. Closes accesslayerorg#778 Closes accesslayerorg#779 Closes accesslayerorg#780 Closes accesslayerorg#785
|
@JamesVictor-O Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves four backend hardening/discovery issues assigned in this wave:
Add cursor-based pagination to the key holders list endpoint #778 — Cursor-based pagination on the key holders endpoint.
GET /api/v1/creators/:id/holdersnow acceptscursoralongside the existinglimit/offset. Whencursoris supplied, the endpoint switches to keyset pagination on the(ownerAddress, creatorId)unique index and returns a signed, tamper-checkednextCursor(reusing the existingencodeCursor/decodeCursorutility already used by the creator feed). Existing offset-mode behavior and response shape are unchanged.Add rate limiting middleware to the key purchase endpoint to prevent rapid-fire buy requests #779 — Rate limiting on the key purchase endpoint. Added a per-wallet sliding-window limiter (
src/middlewares/wallet-rate-limit.middleware.ts), 5 requests / 10s, backed by a Redis sorted set (ZADD/ZREMRANGEBYSCORE/ZCARD) so the window slides continuously rather than resetting on a fixed boundary. Exceeding the limit returns429with aRetry-Afterheader and logs the breach (wallet + timestamp). Anx-internal-service-keyheader matching the optionalINTERNAL_SERVICE_KEYenv var bypasses the limit for internal calls. Fails open (allows the request) if Redis is unreachable, so a cache outage can't block purchases.Add input sanitization and schema validation middleware to all request bodies using Zod #780 — Centralized Zod body validation. Added
validateBody(schema)(src/middlewares/validate-body.middleware.ts): runsschema.safeParse(req.body), strips unknown fields (Zod's defaultz.object()behavior), and returns a structured422listing every invalid field before the request reaches the controller. Applied to the four real route groups in this codebase that accept a body: auth registration, key purchase (buy), post creation, and profile update.Note: the issue's acceptance criteria mention "key sale" as one of four groups, but this codebase has no sell/key-sale endpoint yet (only buy and multi-buy). I covered buy, post creation, profile update, and auth registration instead — the middleware is generic, so a future sell endpoint can adopt it in one line. Profile update's body-validation status code changes from 400 → 422 for consistency with the rest of the API (
creator-profile-update.integration.test.tsupdated accordingly — path-param validation on that route stays 400).Add a leaderboard endpoint returning the top 20 creator keys ranked by total trading volume #785 — Volume leaderboard endpoint. Added
GET /api/v1/creators/leaderboard/volume, returning the top 20 creator keys ranked by total trading volume (buys + sells combined) over a rolling window (default 7 days,LEADERBOARD_VOLUME_WINDOW_DAYS). Volume is aggregated from theActivityread model (amount * price_at_tradeonKEY_BOUGHT/KEY_SOLDrows). Results are cached in Redis forLEADERBOARD_VOLUME_CACHE_TTL_SECONDS(default 5 minutes) and the cache is invalidated whenever the indexer records a new trade. Response fields match the issue spec:rank,keyId,creatorName,avatarUrl,totalVolume,priceChange24h(the last computed from the creator's existing price snapshot). Every Redis operation is time-bounded (1s) so a Redis outage degrades to "compute live" / "skip invalidation" instead of hanging a request or the indexer pipeline.Test plan
pnpm tsc --noEmit— cleanpnpm eslinton all touched files — cleanmainbaseline to confirm no regressions: main has 98 failing suites / 288 failing tests (pre-existing, mostly DB-integration tests that need a live Postgres instance unavailable in this sandbox); this branch has 93 failing / 287 failing with 25 more tests total — no new failures introducedindexer-pipeline.integration.test.tsdidn't mock Redis and would hang once the pipeline started invalidating the leaderboard cache — added a Redis mock there, matching the pattern already used bysubscription.integration.test.tsCloses #778
Closes #779
Closes #780
Closes #785