Skip to content

fix(rate-limit): extend Redis rate-limit guard to Horizon-facing endpoints - #256

Open
teslims2 wants to merge 2 commits into
Afro-Pay:mainfrom
teslims2:fix/issue-204-horizon-rate-limiting
Open

fix(rate-limit): extend Redis rate-limit guard to Horizon-facing endpoints#256
teslims2 wants to merge 2 commits into
Afro-Pay:mainfrom
teslims2:fix/issue-204-horizon-rate-limiting

Conversation

@teslims2

Copy link
Copy Markdown
Contributor

Summary

Closes #204.

AnchorService.getFxRate() and the wallet balance-fetch path proxy to Stellar Horizon / the anchor with no per-user or per-IP rate limiting at the API level, so a single caller could exhaust Horizon's shared-IP rate limit for everyone. This wraps those routes in the existing Redis sliding-window @RateLimit() guard (apps/api/src/rate-limit/rate-limit.guard.redis.ts), and — critically — registers RateLimitModule in AppModule, since it was defined but never imported, so no @RateLimit()-decorated route (including the pre-existing transactions:send limit) was actually being enforced.

Changes

  • app.module.ts: import RateLimitModule so RateLimitGuardRedis is active as a global APP_GUARD.
  • GET /wallet/balances: per-user limit (WALLET_BALANCES_RATE_LIMIT_MAX / WALLET_BALANCES_RATE_LIMIT_WINDOW_MS, default 10/min).
  • GET /anchor/fx-rate: per-user limit (ANCHOR_FX_RATE_RATE_LIMIT_MAX / ANCHOR_FX_RATE_RATE_LIMIT_WINDOW_MS, default 10/min).
  • GET /anchor/auth/challenge and POST /anchor/auth/token (unauthenticated SEP-10 endpoints, no JWT so the guard naturally keys on IP): per-IP limit (ANCHOR_AUTH_RATE_LIMIT_MAX / ANCHOR_AUTH_RATE_LIMIT_WINDOW_MS, default 20/min).
  • All limits return HTTP 429 with a Retry-After header (existing guard behavior).
  • Documented every rate-limit env var (new and pre-existing) in docs/environment-variables.md.
  • Extended rate-limit.guard.spec.ts with coverage for the new key prefixes: per-user vs. per-IP keying, 429 + Retry-After on exceeding the limit, and env-var override behavior. Also fixed a pre-existing gap in the spec's MockRedis test double (missing quit()) that made the suite fail to tear down.

Acceptance criteria

  • Per-user rate limit (configurable) applied to balance-fetch and FX-rate endpoints
  • Per-IP rate limit (configurable) applied to unauthenticated anchor-auth endpoints
  • Exceeding the limit returns HTTP 429 with Retry-After header
  • rate-limit.guard.spec.ts extended with tests for the new endpoints
  • Limits configurable via environment variables, documented in docs/environment-variables.md

Testing

npx jest src/rate-limit src/anchor src/wallet

All rate-limit and anchor-service suites pass. tsc --noEmit shows no new errors introduced by this change (verified by diffing before/after).

Out of scope (pre-existing, unrelated)

While touching these files I found WalletService is currently missing findByUserId, findByPublicKey, and getBalances (and wallet.controller.ts calls enableMultiSignature, which doesn't exist either — it's enableMultisig), and AnchorModule/AnchorAuthController aren't registered in AppModule yet. These predate this change and look like fallout from other in-flight work; left untouched here to keep this PR focused on rate limiting. The rate-limit decorators are already in place on those routes so they'll be enforced once that wiring lands.

🤖 Generated with Claude Code

…oints

Wraps outbound Stellar Horizon / anchor calls with the existing
Redis sliding-window rate-limit infrastructure so a single caller
can't exhaust Horizon's shared-IP rate limit for everyone.

- Register RateLimitModule in AppModule so the RateLimitGuardRedis
  global APP_GUARD is actually active (it existed but was never
  wired up, so no @ratelimit()-decorated route, including the
  pre-existing transactions:send limit, was enforced).
- Add a per-user @ratelimit() to GET /wallet/balances (Horizon
  balance fetch), configurable via WALLET_BALANCES_RATE_LIMIT_MAX /
  WALLET_BALANCES_RATE_LIMIT_WINDOW_MS.
- Add a per-user @ratelimit() to GET /anchor/fx-rate, configurable
  via ANCHOR_FX_RATE_RATE_LIMIT_MAX / ANCHOR_FX_RATE_RATE_LIMIT_WINDOW_MS.
- Add a per-IP @ratelimit() to the unauthenticated SEP-10 endpoints
  GET /anchor/auth/challenge and POST /anchor/auth/token, configurable
  via ANCHOR_AUTH_RATE_LIMIT_MAX / ANCHOR_AUTH_RATE_LIMIT_WINDOW_MS.
- Document all new/existing rate-limit env vars in
  docs/environment-variables.md.
- Extend rate-limit.guard.spec.ts with coverage for the new
  keyPrefixes: per-user vs per-IP keying, 429 + Retry-After on
  exceeding the limit, and env-var overrides. Also fixes a
  pre-existing gap in the spec's MockRedis double (missing quit())
  that made the suite fail to tear down.

Closes Afro-Pay#204

Note: WalletService is currently missing findByUserId,
findByPublicKey, and getBalances (and wallet.controller.ts calls
enableMultiSignature, which doesn't exist either), and AnchorModule /
AnchorAuthController aren't registered in AppModule yet. These are
pre-existing gaps from other in-flight work, out of scope here; the
rate-limit decorators are added to the routes now so limiting is
already in place once those are wired up.
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

@teslims2 is attempting to deploy a commit to the milah's projects Team on Vercel.

A member of the Team first needs to authorize it.

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.

Rate-limiting: extend Redis sliding-window guard to cover Stellar Horizon proxy endpoints

1 participant