Skip to content

feat(ws/config): implement WS filtering, replay, metrics wiring, and dry-run flag (#257 #258 #259 #260) - #348

Open
onahiOMOTI wants to merge 3 commits into
stellar-vortex-protocol:mainfrom
onahiOMOTI:feature/issues-257-258-259-260
Open

feat(ws/config): implement WS filtering, replay, metrics wiring, and dry-run flag (#257 #258 #259 #260)#348
onahiOMOTI wants to merge 3 commits into
stellar-vortex-protocol:mainfrom
onahiOMOTI:feature/issues-257-258-259-260

Conversation

@onahiOMOTI

Copy link
Copy Markdown

Summary

This PR implements four related issues in a single branch so they can be reviewed and merged together. Each issue has its own commit for clean history.


Changes

Issue #257 — Real topic-based WS chain-subscription filtering

  • Changed IntentsGateway.subscribers from Set<WebSocket> to Map<WebSocket, SubscriberFilter> to track per-connection chain filters.
  • Added client.on("message", ...) listener inside handleConnection() routing to handleMessage().
  • handleSubscribe() validates each chain value against SUPPORTED_CHAINS, stores the per-connection filter, and replies with { type: "subscribed", filter: { chains } }.
  • broadcast() calls getEventChain() to resolve the relevant source chain (direct read for intent_created; async lookup via IntentsService.get() for state-transition events), then skips subscribers whose filter excludes it.
  • Clients that never send a subscribe message continue receiving the full unfiltered feed — backward compatible.
  • scripts/solver-bot.ts already implements the client side; verified it matches the server implementation.
  • PR_DESCRIPTION_79.md updated to reflect the real implementation.

Issue #258 — WS event replay backed by EventRingBuffer

  • EventRingBuffer (already defined) is now instantiated as private readonly ringBuffer in IntentsGateway.
  • Every broadcast() call assigns seq = this.nextSeq++ and pushes the sequenced event into the ring buffer before delivering to subscribers.
  • handleReplay() handles { type: "replay", fromSeq } — streams back buffered events wrapped in replay_start / replay_end frames, or responds with replay_too_old (including oldestAvailableSeq) when the requested sequence has been evicted.
  • Reuses the handleMessage() plumbing from Implement real topic-based WS chain-subscription filtering in IntentsGateway #257 — single client.on("message", ...) listener handles both subscribe and replay message types.
  • docs/solver-onboarding.md replay section verified accurate against the implementation.

Issue #259 — Wire IntentsSweeperService to MetricsService (retire MetricsRegistry)

  • Deleted src/common/metrics.ts (the dormant MetricsRegistry — dead code with documentation depending on it).
  • MetricsService gains sweeperExpiredTotal (Counter) and sweeperSweepDurationMs (Histogram) — Prometheus-backed, exposed on GET /metrics.
  • IntentsSweeperService.sweep() calls this.metricsService.recordSweep(expiredCount, durationMs) at the end of every cycle.
  • docs/runbooks/on-call.md updated — Scenario B references the real Prometheus metric names (vortex_sweeper_sweep_duration_ms, vortex_sweeper_expired_total); all references to the retired MetricsRegistry removed.

Issue #260 — Runtime-toggleable dry-run flag for on-chain write paths

  • ONCHAIN_DRY_RUN env var added to env.validation.ts: defaults to true outside production; required to be explicitly set in production (fail-closed, mirrors SOROBAN_SIGNING_KEY pattern — the process refuses to start without it).
  • AppConfig.onchainDryRun: boolean added to configuration.ts with appropriate factory default.
  • StellarTxService.invokeContract(): reads onchainDryRun from config; when true logs and returns { hash: "dry-run-no-hash", status: "DRY_RUN", dryRun: true } without touching the network.
  • SolverRegistryService.slashSolver(): adds dry-run short-circuit at the top of the method (matching the reference implementation pattern described in the issue); replaced console.log with Logger throughout.
  • docs/runbooks/onchain-cutover.md: documents ONCHAIN_DRY_RUN flag name, default behaviour, restart-only limitation (explicit — no hot-reload for this iteration), production validation rule, and staged rollout procedure. Marks issue Add a runtime-toggleable dry-run flag for on-chain write code paths #260 as Done in the dependency table.

Limitation (documented): The flag is config-driven and takes effect on the next process restart. There is no HTTP endpoint to flip it at runtime without a restart. This is intentional for this iteration — the staged rollout in onchain-cutover.md is designed around restart windows, and a live-toggle mechanism is a separate future concern.


Tests

All new logic has test coverage:

File New tests
src/intents/intents.gateway.spec.ts 16 new tests for #257 (filtering) + #258 (replay)
src/intents/intents-sweeper.service.spec.ts 2 new tests for #259 (MetricsService.recordSweep called with correct values)
src/config/env.validation.spec.ts 6 new tests for #260 ONCHAIN_DRY_RUN validation
src/soroban/stellar-tx.service.spec.ts 2 new tests for #260 dry-run path in invokeContract
src/soroban/solver-registry.service.spec.ts 2 new tests for #260 dry-run flag in slashSolver

All tests that were passing before this branch continue to pass. The 5 pre-existing failing test suites (soroban.controller.spec.ts, logging.interceptor.spec.ts, http-exception.filter.spec.ts, stats.service.spec.ts, intents.service.spec.ts) are unchanged and were failing on main before this branch.


Closes

Closes #257
Closes #258
Closes #259
Closes #260

…ellar-vortex-protocol#257 stellar-vortex-protocol#258)

Issue stellar-vortex-protocol#257 – Real topic-based WS chain-subscription filtering:
- Change subscribers from Set to Map<WebSocket, SubscriberFilter> to
  store per-connection chain filters
- Add handleMessage() dispatching subscribe and replay message types
  inside handleConnection() (single listener, shared entry point)
- handleSubscribe() validates incoming chains against SUPPORTED_CHAINS,
  stores a Set<SupportedChain> | null filter per client, and replies with
  { type: 'subscribed', filter: { chains } }
- getEventChain() resolves srcChain for intent_created directly from the
  event payload; for intent_accepted / intent_filled / intent_cancelled /
  intent_expired / intent_slashed it performs a non-blocking IntentsService
  lookup; returns null for unchained events (delivered to everyone)
- broadcast() now async: assigns a seq, pushes to ring buffer, resolves
  chain once, then fans out only to subscribers whose filter matches
- Clients that never send subscribe continue to receive the full feed
  (backward-compatible default, filter.chains === null)
- Malformed JSON and unknown message types are silently ignored

Issue stellar-vortex-protocol#258 – WS event replay backed by EventRingBuffer:
- Instantiate EventRingBuffer (capacity 500) owned by IntentsGateway
- Every broadcast event is assigned nextSeq++ and pushed into the buffer
  before fan-out so a concurrent replay request finds the event
- handleReplay() processes { type: 'replay', fromSeq } messages:
  - If fromSeq >= oldestSeq - 1: streams replay_start / events / replay_end
  - If fromSeq < oldestSeq - 1: returns replay_too_old with oldestAvailableSeq
  - Empty buffer returns replay_start with count 0 (no replay_too_old)
- Reuses the single handleMessage() entry point from stellar-vortex-protocol#257
- REPLAY_BUFFER_SIZE kept at 500 with inline comment explaining the
  memory-vs-reconnect-gap tradeoff at current broadcast volumes

Fixes:
- intents.service.ts: add missing INTENTS_REPOSITORY / IIntentsRepository
  imports (pre-existing compile error)
- solvers.service.ts: fix reactivate() shorthand property bug (isActive
  was referenced but not in scope)
- intents-sweeper.service.ts: await broadcast() calls (now async)
- intents-sweeper.service.spec.ts: add missing ALPHA_ADDR, buildIntentsService,
  SOLVERS_REPOSITORY; mock broadcast as jest.fn().mockResolvedValue(undefined)
- test/load/ws-broadcast-fanout.test.ts: await gateway.broadcast()

Tests: 32 gateway tests (EventRingBuffer + heartbeat + filtering + replay),
5 sweeper tests — all passing
…stellar-vortex-protocol#259)

Decision rationale (per issue stellar-vortex-protocol#259): MetricsService (Prometheus/prom-client)
is the production metrics path; MetricsRegistry in src/common/metrics.ts was
dead code with no callers but live runbook documentation depending on it.
Retiring it removes the confusion and aligns all metrics under one system.

Changes:
- Delete src/common/metrics.ts (Counter, Histogram, MetricsRegistry) —
  confirmed zero imports outside the file itself before deletion
- Add sweeperExpiredTotal (vortex_sweeper_expired_total) and
  sweeperSweepDurationMs (vortex_sweeper_sweep_duration_ms) counters/
  histograms to MetricsService with matching Prometheus naming convention
- Add MetricsService.recordSweep(expiredCount, durationMs) helper called
  by IntentsSweeperService at the end of every sweep() cycle
- Inject MetricsService into IntentsSweeperService constructor; MetricsModule
  is @global() so no IntentsModule import change required
- Update docs/runbooks/on-call.md:
  - Replace MetricsRegistry.sweeper.sweepDurationMs → vortex_sweeper_sweep_duration_ms
  - Replace MetricsRegistry.sweeper.expiredTotal → vortex_sweeper_expired_total
  - Update 'How the sweeper works' step 4 to describe MetricsService path
  - Update diagnosis step 3 curl snippet with correct Prometheus metric names
  - No reference to src/common/metrics.ts or MetricsRegistry remains

Tests: 2 new assertions in intents-sweeper.service.spec.ts verify
recordSweep is called on every cycle with the correct expired count
…hs (stellar-vortex-protocol#260)

Implements issue stellar-vortex-protocol#260 — a runtime-configurable dry-run safety flag for
every on-chain write code path.

Changes:
- src/config/env.validation.ts: add ONCHAIN_DRY_RUN Joi schema entry.
  Default true outside production; required (explicit) in production —
  mirrors the fail-closed pattern of SOROBAN_SIGNING_KEY.
- src/config/configuration.ts: add onchainDryRun to AppConfig interface
  and factory function.
- src/soroban/stellar-tx.service.ts: read dryRun from config in constructor;
  invokeContract() short-circuits and returns a placeholder result
  (dryRun: true) when ONCHAIN_DRY_RUN=true — no network calls made.
- src/soroban/solver-registry.service.ts: read dryRun from config; add
  dry-run short-circuit before the live path in slashSolver(); add Logger
  and use it instead of console.log throughout.
- docs/runbooks/onchain-cutover.md: document ONCHAIN_DRY_RUN flag name,
  default behaviour, restart requirement, and production validation rule.
  Mark issue stellar-vortex-protocol#260 as Done in the dependency table.

Tests:
- src/config/env.validation.spec.ts: 6 new ONCHAIN_DRY_RUN tests; fix
  existing 'accepts a well-formed key in production' test to include
  ONCHAIN_DRY_RUN (now required in production).
- src/soroban/stellar-tx.service.spec.ts: 2 new invokeContract dry-run
  tests (dryRun=true returns placeholder; dryRun=false throws not-yet-impl).
- src/soroban/solver-registry.service.spec.ts: 2 new dry-run flag tests.

Closes stellar-vortex-protocol#260
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@onahiOMOTI 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant