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

@james2177

Copy link
Copy Markdown
Contributor

resolve conflicts

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

Labels

None yet

Projects

None yet

2 participants