Skip to content

fix(api): gate candles routes with the refresh worker - #215

Merged
Miracle656 merged 2 commits into
Miracle656:mainfrom
Wang1rrr:fix/190-wire-candles
Oct 19, 2026
Merged

Miracle656 merged 2 commits into
Miracle656:mainfrom
Wang1rrr:fix/190-wire-candles

Conversation

@Wang1rrr

@Wang1rrr Wang1rrr commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Description

Connect the existing candles reader and refresh worker behind one explicit opt-in. A valid OHLC_REFRESH_INTERVAL_MS enables both only for a single enabled network matching STELLAR_NETWORK; unset, invalid, zero, API-only mode and incompatible network configurations leave the route unmounted and the worker stopped. The OpenAPI generator uses the same gate, so the default specification omits candles.

Remove the public refresh endpoint. Refreshes belong to the worker, whose overlap guard and Promise.allSettled handling prevent a failed bucket from allowing another refresh to start before the remaining queries finish. Shutdown retains and calls the worker's stop callback.

The read route uses the existing parameter schemas, binds SQL values separately, and sanitizes database failures. A separate commit retains Prisma operational warnings while disabling raw query/error stdout logging; this keeps the application-wide logging change independently reviewable.

Closes #190.

Validation

Validated on 2026-10-09:

  • npm run db:generate, npm run typecheck and npm run build: passed.
  • npm test: 57 suites / 707 tests passed after rebasing on current main, including the configured coverage thresholds.
  • Regressions exercise the real application mount, disabled/invalid settings, API-only mode, incompatible network configurations, rejection of another network, absence of the public refresh route, startup/shutdown and overlapping refreshes.
  • OpenAPI tests check enabled/disabled configurations and absence of the refresh operation. The current main's spec coverage checks pass, and npm run docs:openapi also passed with the default disabled setting.
  • git diff --check: passed.

The database integration suite was not run for this follow-up; HTTP and worker regressions use mocked database/RPC boundaries.

Existing database prerequisite

The operator must install working OHLC tables/functions and ensure the database contains transfers from only the configured network before opting in. The legacy aggregates have no network dimension, and the existing sql/001_ohlc_aggregates.sql requires separate repair and deployment; this PR does not install or execute it. Restricting the current process configuration cannot certify the contents of an existing database. README and .env.example explain the prerequisite and how to regenerate OpenAPI with the deployment's settings.

AI assistance was used for this review follow-up; the changes were checked against the review and the full unit test suite.

Copilot AI balanced review requested due to automatic review settings September 29, 2026 23:45
@drips-wave

drips-wave Bot commented Sep 29, 2026

Copy link
Copy Markdown

@Wang1rrr 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! 🚀

Learn more about application limits

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

A lot here is genuinely careful and I want to credit it before the objections. The overlap guard in startOhlcRefreshWorker (the running flag) and the switch to Promise.allSettled so a failed bucket can't leave queries pending into the next tick are both the right instincts. The interval validation is unusually thorough — rejecting non-integers, zero and blank, and explicitly rejecting > 2^31-1 because Node clamps an oversized delay to 1 ms is the kind of thing most people find out in production. Disabled by default, wired into the shutdown path, and SKIP_INDEXER respected. And the $queryRawUnsafe parameter fix in queryCandlesFromAggregate is a real bug caught: main passed [contractId, limit, offset] as a single positional argument, so $1/$2/$3 never bound. The ${table} interpolation is safe because bucket comes out of a z.enum before it's used — I checked.

Verified locally on the merge with main: tsc --noEmit and tsc -p tsconfig.test.json both clean, and all three new suites pass — 41 tests across ohlcRefreshWorker, ohlcStartup and candles-route.

Three things I can't merge past.

1. POST /candles/refresh is mounted with no authentication (src/api/candles.ts:98, mounted at src/api.ts:199)

This is a new, anonymous, mutating endpoint that runs ohlc.refresh_candles_1m/1h/1d() — three full aggregate rebuilds over the transfers table — against the production database. The only thing in front of it is the global rate limiter at src/api.ts:65, which is 60 requests/minute per IP. Sixty full aggregate rebuilds a minute per IP, from anyone, on a Postgres instance this deployment shares. openapi.json now advertises it too.

The irony is that this PR makes the endpoint unnecessary: startOhlcRefreshWorker already does exactly this on a timer. Either drop the public route entirely and let the worker own refreshes, or put it behind the same bearer-token check #216 just added to /offramp. Dropping it is cleaner.

2. The read route answers for a network it then ignores

networkMiddleware is mounted app-wide at src/api.ts:155, so GET /candles/1h/C…?network=mainnet validates and resolves the network — and then queryCandlesFromAggregate never uses it. Your own README text says why: "These legacy aggregates do not separate networks, so they must not be used with a database containing transfers from multiple networks." Thank you for writing that down honestly, but a README note doesn't stop the route from returning 200 with mainnet and testnet candles blended together while the caller believes they asked for one. That's the failure mode src/middleware/network.ts is written against in so many words — "silently conflating them is how a dashboard ends up confidently showing zero."

Since the tables also aren't installed by any current startup path (again, your README), the route 500s on every request on every deployment today. Gate the mount behind the same opt-in that gates the worker: if OHLC_REFRESH_INTERVAL_MS is unset, don't mount /candles and don't register it in openapi.json. Then an operator who installs the aggregates and accepts the single-network constraint opts into both together, and nobody else gets a documented endpoint that only ever 500s.

3. src/db.ts:39 — log: [] silences Prisma for the whole application

Going from [query, warn, error] in development and [warn, error] in production to [] everywhere is a global observability change that has nothing to do with mounting candles routes. The motivation is right — this repo's rule is that nothing logs a raw database error or connection string — but it's worth its own commit and its own reasoning, because as written it also drops Prisma's connection-pool and deprecation warnings, which don't carry query text. log: ["warn"] would satisfy the rule and keep those. Either way, please split it out.

One smaller note, no action required: catch { console.error("[candles] Query failed"); } now discards the error object completely. Not leaking the message is right; losing the error class means a missing relation and a connection timeout produce byte-identical logs. Something like err instanceof Error ? err.name : "unknown" keeps it diagnosable without leaking anything.

Solid work — the worker is in good shape. It's the public surface that needs tightening.

https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB

@Wang1rrr
Wang1rrr force-pushed the fix/190-wire-candles branch from 19826df to afd8074 Compare October 9, 2026 03:42
@Wang1rrr Wang1rrr changed the title fix(api): mount candles routes and start refresh worker fix(api): gate candles routes with the refresh worker Oct 9, 2026
Wang1rrr and others added 2 commits October 9, 2026 11:51
Keep connection-pool and deprecation warnings available while handling raw database errors at the application boundary. Query and error stdout logging can expose database details before handlers sanitize failures.
@Wang1rrr
Wang1rrr force-pushed the fix/190-wire-candles branch from afd8074 to c79ed01 Compare October 9, 2026 03:52
@Miracle656
Miracle656 merged commit 962d91d into Miracle656:main Oct 19, 2026
1 check passed
@Miracle656

Copy link
Copy Markdown
Owner

Merged. The gate is the right shape and the default is the right default.

What convinced me: the legacy OHLC aggregates have no network dimension, so in a multi-network process the route would have served candles mixed across testnet and mainnet with nothing in the response to admit it. Off-by-default beats confidently-wrong, and your networks.length !== 1 || networks[0] !== currentNetwork() check is what makes that honest rather than hopeful. I also checked the blast radius before agreeing to unmount it: nothing consumes this endpoint — veil doesn't call /candles anywhere, and Lens has its own /candles served from its own tables. So there's no client to break.

Good catch on the interval ceiling. Node clamps a setInterval delay above 2^31-1 to 1ms, so a careless OHLC_REFRESH_INTERVAL_MS=999999999999 wouldn't have been a slow refresh, it would have been a tight loop against the database — the opposite of what the operator asked for. That one is easy to not think of.

Removing the public refresh endpoint is also right. A refresh belongs to the worker because the worker is what holds the overlap guard; an HTTP caller could start a second pass while the first one's queries were still outstanding.

One hunk I reverted

src/db.ts narrowed the Prisma log from ["warn", "error"] to ["warn"]. I put it back, with the reasoning in a comment.

Prisma's error log goes to stdout, not into a response — the response-side leak was closed separately in #200 — and the route handlers answer 500 without logging anything themselves. So that line is currently the only place a failed write on a money-moving route becomes visible at all. Dropping it would have made us blind on the one path where blindness costs money, which I'd rather not trade for a leak that's already fixed elsewhere.

I kept query development-only, which I think was the real intent and is a genuine improvement over printing every statement in production.

You did flag this as a deliberately separate commit for reviewability, which I appreciate — but an application-wide logging change still wants its own PR rather than riding along with a candles gate. If you think error should go, open that one and make the argument there; I'll read it properly.

Merge notes

README conflicted: main's environment table has since gained the per-network RPC rows (SOROBAN_RPC_URL_TESTNET/_MAINNET). Kept main's table and inserted your OHLC_REFRESH_INTERVAL_MS row into it.

Verified on the merge result rather than your branch: tsc --noEmit silent, 908 tests across 62 suites green. Closes #190.

Also noted your point about the database prerequisite — sql/001_ohlc_aggregates.sql still needing repair and deployment, and the process config not being able to certify what's already in the database. That's recorded; it stays an operator step, not a merge blocker.

JemimahEkong added a commit to JemimahEkong/wraith that referenced this pull request Oct 19, 2026
…t schema

Conflicts in src/openapi/build.ts and src/openapi/schemas.ts: this
branch added /fees/report while main added /fees (Miracle656#215 area) and the
gated /candles route at the same place. Both kept.

openapi.json regenerated from the merged source in a clean environment
(npm run docs:openapi): identical to main's plus /fees/report.

clients/react-query/src/schema.d.ts regenerated from it. The branch had
changed openapi.json without regenerating the client, which fails the
"Generate, typecheck & test" job (git diff --exit-code src/schema.d.ts).

Verified: typecheck, tsc --noEmit, build; jest 63 suites / 961 tests
(the openapiCoverage failure noted in the PR description is gone);
clients/react-query generate (no diff), typecheck, test, build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvL6Yz2Zfg7spM1F9DXXtz
Miracle656 added a commit that referenced this pull request Oct 20, 2026
`validateEnv()` now refuses to start when `DATABASE_URL` or
`DIRECT_DATABASE_URL` is missing, or when mainnet is enabled with no mainnet
RPC URL, naming what is missing. Previously those surfaced much later as
connection errors that said nothing about configuration.

`validateEnv` itself is a pure function taking `env` as a parameter, which is
what makes it testable without touching the process environment.

Three changes while merging:

- **`src/index.ts` conflicted.** The PR converted every top-level `import` to
  `const x = require(...) as typeof import(...)` so `validateEnv()` could run
  before module side effects. Kept `main`'s imports instead: the require block
  predated #215 and omitted `startOhlcRefreshWorker` and
  `getOhlcRefreshIntervalMs`, so taking it would have silently unwired the OHLC
  worker. In a `module: commonjs` build an `import` is a sequential `require`
  anyway, so ordering is achievable without rewriting the entrypoint's style.
- **`validateEnv()` moved into `main()`** rather than module scope. At module
  scope it threw whenever anything imported the entrypoint, which broke
  `ohlcStartup.test.ts` — that suite boots the real `index.ts`. It still runs
  before any listener opens, which is the stated goal.
- **`docker-compose.yml` had drifted from `.env.example` by 36 keys**, which
  `dockerCompose.test.ts` caught. Added them as unset passthroughs
  (`KEY: ${KEY:-}`), so a compose run matches a host run and no secret value is
  committed.

`ohlcStartup.test.ts` now sets the database URLs and a mainnet RPC in its
`beforeEach`, because it starts the real entrypoint and a real start requires
them. Nothing connects to either — every module that would is mocked.

Verified on the merge result: `tsc --noEmit` silent, 998 tests pass across 65
suites.

Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
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.

Mount the candles router and start its refresh worker

3 participants