Skip to content

Conversation

@premiumjibles
Copy link
Contributor

@premiumjibles premiumjibles commented Aug 19, 2025

Summary

As the title says this introduces a new market data socket to our proxy server. This needs to be a proxy as coincap charges per socket connection, so this allows us to have one socket connection and as many subscribers as we want.

Coincap also doesn't seem to charge based on how many tokens you subscribe to. So we basically subscribe to all of them and let the listener pick which token updates they care about.

Only subscribes to coincap if we have clients for the socket

For this to be useful to the frontend we need to upgrade the coincap adapter (shapeshift/web#10312) and see if we can bring in more assets. If even after than it's not enough we'll need to look at different providers

How to test

  1. Run docker compose up -d in the unchained directory
  2. Run docker compose up in node/proxy/api
  3. use wscat to subscribe to the websocket `wscat -c "ws://api.proxy.localhost"
  4. Send a subscription message like {"subscriptionId":"test-123","method":"subscribe","data":{"assets":["bitcoin","ethereum"]}}
  5. You should see some updates coming through
image

I imagine we'll need to do some devopsy stuff as well to get this working in prod if we're happy with it.

Summary by CodeRabbit

  • New Features

    • Real-time market-data WebSocket with per-connection asset subscriptions and CoinCap feed.
    • /metrics endpoint exposing Prometheus metrics.
  • Chores

    • WebSocket now shares the existing HTTP server.
    • New Prometheus package added; dependencies updated across services.
    • WebSocket client and connection lifecycle improved for more reliable reconnection and subscription handling.
  • Documentation

    • Sample env updated with COINCAP_API_KEY.

✏️ Tip: You can customize this high-level summary in your review settings.

@premiumjibles premiumjibles requested a review from a team as a code owner August 19, 2025 23:01
@coderabbitai
Copy link

coderabbitai bot commented Aug 19, 2025

📝 Walkthrough

Walkthrough

Adds a websocket-based market-data subsystem (connection handler, CoinCap client), a websocket base package with heartbeat/routing, a new @shapeshiftoss/prometheus package, updates many coinstack apps to import Prometheus from the new package, and wires WS support into the proxy API using COINCAP_API_KEY.

Changes

Cohort / File(s) Summary
Proxy API — app & env
node/proxy/api/src/app.ts, node/proxy/sample.env
Switches from direct app.listen to const server = app.listen(...), attaches ws.Server to the HTTP server, reads COINCAP_API_KEY, constructs CoincapWebsocketClient, instantiates Prometheus, exposes /metrics, and starts MarketDataConnectionHandler per WS connection.
Proxy API — market data
node/proxy/api/src/marketData.ts, node/proxy/api/src/coincap.ts
Adds MarketDataConnectionHandler, MarketDataClient contract, message/subscribe types, and CoincapWebsocketClient which manages provider subscriptions and forwards filtered price updates to registered connections.
Websocket base package
node/packages/websocket/src/connectionHandler.ts, node/packages/websocket/src/websocket.ts, node/packages/websocket/src/index.ts, node/packages/websocket/package.json
Introduces BaseConnectionHandler with heartbeat, routing, publish/sendError; renames/refactors client to BaseWebsocketClient/AddressSubscriptionWebsocketClient, makes socket optional, renames initialize()connect(), and re-exports connection handler.
Common API websocket & registry
node/coinstacks/common/api/src/websocket.ts, node/coinstacks/common/api/src/registry.ts, node/coinstacks/common/api/src/middleware.ts, node/coinstacks/common/api/src/index.ts
Refactors ConnectionHandler to extend BaseConnectionHandler, switches to address-subscription client types, adds onSubscribe/onUnsubscribe/onClose, removes legacy publish/payload interfaces, and changes registry.publish to (subscriptionId, { address, data }); updates Prometheus re-export removal and middleware import.
Blockbook & coinstack websocket clients
node/packages/blockbook/src/websocket.ts, node/coinstacks/solana/api/src/websocket.ts
Update implementations to extend AddressSubscriptionWebsocketClient, call super.connect() on init, and use optional chaining for socket sends.
Coinstacks app Prometheus import changes
node/coinstacks/*/api/src/app.ts, node/coinstacks/*/api/package.json
Adds @shapeshiftoss/prometheus to many coinstack manifests and moves Prometheus imports from @shapeshiftoss/common-api to @shapeshiftoss/prometheus.
New Prometheus package
node/packages/prometheus/package.json, node/packages/prometheus/src/index.ts, node/packages/prometheus/tsconfig.json
Adds @shapeshiftoss/prometheus package scaffold, re-exporting prometheus module, declares prom-client dependency and build scripts.
Registry/publish payload shape
node/coinstacks/common/api/src/registry.ts, node/coinstacks/common/api/src/websocket.ts
Changes registry.publish payload from (subscriptionId, address, tx) to (subscriptionId, { address, data: tx }) and adapts ConnectionHandler routing to new payload shape.
Top-level/package adjustments
package.json, node/packages/websocket/package.json
Removes top-level prom-client dependency; adds @shapeshiftoss/prometheus to websocket package dependencies.
Misc: coinstack package manifests & wiring
node/coinstacks/*/api/package.json, many node/coinstacks/*/api/src/app.ts
Adds @shapeshiftoss/prometheus to many coinstack manifests and updates imports in app.ts files to import Prometheus from the new package.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client as Client (WS)
  participant API as Proxy API (HTTP + WS)
  participant Conn as MarketDataConnectionHandler
  participant Coincap as CoincapWebsocketClient
  participant Prom as Prometheus

  Client->>API: WS upgrade → market-data endpoint
  API->>Conn: MarketDataConnectionHandler.start(ws, coincapClient, prometheus, logger)
  Conn->>Coincap: subscribe(clientId, subscriptionId, assets)
  alt first subscription for provider
    Coincap->>Coincap: connect(provider URL w/ API key)
  end
  Coincap-->>Conn: raw price update
  Conn->>Client: publish(subscriptionId, price_update filtered)
  Conn->>Prom: update websocket metrics (connect/close/message)
  Client->>Conn: unsubscribe / close
  Conn->>Coincap: unsubscribe(clientId, subscriptionId)
  alt no remaining subscriptions
    Coincap->>Coincap: close connection
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I nibble bytes where prices hop,
I hold the ping and send the prop—
Subscriptions sprout and softly close,
I route the ticks where market goes.
Little rabbit counts the rows. 🐇📈

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding a new market data socket with Coincap provider for live price updates, which aligns with the PR's primary objective.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 10283_coincap_poc

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3dc993e and e0fb8bb.

📒 Files selected for processing (2)
  • node/proxy/api/src/app.ts (2 hunks)
  • node/proxy/api/src/marketData.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
node/proxy/api/src/marketData.ts (2)
go/pkg/api/middleware.go (1)
  • Logger (30-63)
node/proxy/api/src/app.ts (1)
  • logger (14-17)
node/proxy/api/src/app.ts (1)
node/proxy/api/src/marketData.ts (1)
  • MarketDataWebSocket (46-291)
🔇 Additional comments (1)
node/proxy/api/src/marketData.ts (1)

240-263: Parse-assets behavior: empty assets means no updates.

This is fine if intentional. If the desired behavior is “send all” when assets is omitted, we can easily switch to that. Confirm product intent.

I can update parseAssetsFromUrl to return a sentinel like ['ALL'] and alter broadcastToClients accordingly if needed.

@kaladinlight
Copy link
Collaborator

Looks good overall. Some suggestions:

  • There is already a websocket client abstract class that we can leverage
  • We also have a connection handler class that could be abstracted to leverage existing heartbeats, reconnect/retries, and maintain consistent subscribe interfaces
  • Path based websocket routing is non standard, and it would be nice to maintain a normal subscribe/unsubscribe interface (see connection handler above)
  • The websocket client abstraction allows for easy class creation per provider versus the provider config strategy
  • Option for a mapping of asset -> clients for broadcasting messages a bit more efficiently

I will push up a draft for reference

@premiumjibles
Copy link
Contributor Author

premiumjibles commented Aug 30, 2025

@kaladinlight wow this is super extensive thanks for that. Cleanup looks good as well. I'll test this out and try and build on top the asset selection

Edit: I've pulled in your PR and implemented the client subscription and asset tracking. How does that look?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
node/coinstacks/common/api/src/middleware.ts (1)

61-68: Reduce Prometheus label cardinality; avoid querystrings and cast statusCode to string.

Using originalUrl/url includes query params, exploding series cardinality. Prefer req.path and pass label values as strings.

Apply:

-    res.on('finish', () => {
-      const route = req.originalUrl ?? req.url
+    res.on('finish', () => {
+      const route = req.path
       if (!route.startsWith('/api/v1/') || res.statusCode === 404) return
-      prometheus.metrics.httpRequestCounter.inc({ method: req.method, route, statusCode: res.statusCode }, 1)
-      end({ method: req.method, route, statusCode: res.statusCode })
+      const labels = { method: req.method, route, statusCode: String(res.statusCode) }
+      prometheus.metrics.httpRequestCounter.inc(labels, 1)
+      end(labels)
     })
🧹 Nitpick comments (8)
node/coinstacks/solana/api/src/app.ts (2)

77-78: Include port in startup log for quicker ops triage.

-const server = app.listen(PORT, () => logger.info('Server started'))
+const server = app.listen(PORT, () => logger.info({ port: PORT }, 'Server started'))

72-75: Rename helius to provider-agnostic wsClient for clarity.

The variable name implies a specific provider; the type is generic.

-const helius = new WebsocketClient(WS_URL, {
+const wsClient = new WebsocketClient(WS_URL, {
   apiKey: WS_API_KEY,
   transactionHandler: registry.onTransaction.bind(registry),
 })
 ...
-wsServer.on('connection', (connection) => {
-  ConnectionHandler.start(connection, registry, helius, prometheus, logger)
-})
+wsServer.on('connection', (connection) => {
+  ConnectionHandler.start(connection, registry, wsClient, prometheus, logger)
+})

Also applies to: 81-81

node/coinstacks/common/api/src/middleware.ts (3)

52-54: Align logger filter with metrics and use path-based check.

Minor inconsistency: logger checks '/api/v1' vs metrics '/api/v1/'. Use req.path and the same prefix to avoid accidental mismatches.

-export const requestLogger = morgan('short', {
-  skip: (req, res) => !req.url?.startsWith('/api/v1') || res.statusCode === 404,
-})
+export const requestLogger = morgan('short', {
+  skip: (req, res) => !req.path?.startsWith('/api/v1/') || res.statusCode === 404,
+})

56-71: Option: account for aborted requests (client disconnects).

Only listening to finish misses aborted responses. If you care about those, handle close once and guard against double-recording.

 export const metrics =
   (prometheus: Prometheus) =>
   (req: Request, res: Response, next: NextFunction): void => {
     const end = prometheus.metrics.httpRequestDurationSeconds.startTimer()
 
-    res.on('finish', () => {
+    let recorded = false
+    const record = () => {
+      if (recorded) return
+      recorded = true
       const route = req.path
       if (!route.startsWith('/api/v1/') || res.statusCode === 404) return
-      prometheus.metrics.httpRequestCounter.inc({ method: req.method, route, statusCode: res.statusCode }, 1)
-      end({ method: req.method, route, statusCode: res.statusCode })
-    })
+      const labels = { method: req.method, route, statusCode: String(res.statusCode) }
+      prometheus.metrics.httpRequestCounter.inc(labels, 1)
+      end(labels)
+    }
+    res.once('finish', record)
+    res.once('close', record)
 
     next()
   }

21-25: Prefer instanceof for custom error detection.

If ApiError is a class, instanceof ApiError is clearer and safer than comparing constructor names.

-  if (err.constructor.name === ApiError.prototype.constructor.name) {
+  if (err instanceof ApiError) {
     const e = err as ApiError
     console.error(e)
     return res.status(e.statusCode ?? 500).json({ message: e.message })
   }
node/packages/websocket/src/connectionHandler.ts (1)

51-59: Make close path idempotent to prevent double cleanup on error→close

onerror calls close(), and onclose also calls close(). Guard to avoid invoking onClose twice.

 export abstract class BaseConnectionHandler {
   public readonly clientId: string
@@
   private readonly pingIntervalMs = 10000
+  private closed = false
@@
   private close(interval: NodeJS.Timeout): void {
-    this.pingTimeout && clearTimeout(this.pingTimeout)
-    clearInterval(interval)
-    this.onClose()
-    this.subscriptionIds.clear()
+    if (this.closed) return
+    this.closed = true
+    if (this.pingTimeout) clearTimeout(this.pingTimeout)
+    clearInterval(interval)
+    this.onClose()
+    this.subscriptionIds.clear()
   }
node/proxy/api/src/coincap.ts (2)

10-12: Preserve apiKey in base args if needed

You’re dropping apiKey from Args when passing to super. If BaseWebsocketClient uses it (e.g., auth headers), forward it.

-    super(url, { logger: args.logger }, opts)
+    super(url, { logger: args.logger, apiKey: args.apiKey }, opts)

30-37: Avoid console.log in library code

Use the injected logger for consistency and to respect log levels.

(Handled in the larger diff above.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 75c16ef and efe8cbd.

📒 Files selected for processing (47)
  • node/coinstacks/arbitrum-nova/api/package.json (1 hunks)
  • node/coinstacks/arbitrum-nova/api/src/app.ts (1 hunks)
  • node/coinstacks/arbitrum/api/package.json (1 hunks)
  • node/coinstacks/arbitrum/api/src/app.ts (1 hunks)
  • node/coinstacks/avalanche/api/package.json (1 hunks)
  • node/coinstacks/avalanche/api/src/app.ts (1 hunks)
  • node/coinstacks/base/api/package.json (1 hunks)
  • node/coinstacks/base/api/src/app.ts (1 hunks)
  • node/coinstacks/bitcoin/api/package.json (1 hunks)
  • node/coinstacks/bitcoin/api/src/app.ts (1 hunks)
  • node/coinstacks/bitcoincash/api/package.json (1 hunks)
  • node/coinstacks/bitcoincash/api/src/app.ts (1 hunks)
  • node/coinstacks/bnbsmartchain/api/package.json (1 hunks)
  • node/coinstacks/bnbsmartchain/api/src/app.ts (1 hunks)
  • node/coinstacks/common/api/package.json (1 hunks)
  • node/coinstacks/common/api/src/index.ts (0 hunks)
  • node/coinstacks/common/api/src/middleware.ts (1 hunks)
  • node/coinstacks/common/api/src/registry.ts (1 hunks)
  • node/coinstacks/common/api/src/websocket.ts (1 hunks)
  • node/coinstacks/dogecoin/api/package.json (1 hunks)
  • node/coinstacks/dogecoin/api/src/app.ts (1 hunks)
  • node/coinstacks/ethereum/api/package.json (1 hunks)
  • node/coinstacks/ethereum/api/src/app.ts (1 hunks)
  • node/coinstacks/gnosis/api/package.json (1 hunks)
  • node/coinstacks/gnosis/api/src/app.ts (1 hunks)
  • node/coinstacks/litecoin/api/package.json (1 hunks)
  • node/coinstacks/litecoin/api/src/app.ts (1 hunks)
  • node/coinstacks/optimism/api/package.json (1 hunks)
  • node/coinstacks/optimism/api/src/app.ts (1 hunks)
  • node/coinstacks/polygon/api/package.json (1 hunks)
  • node/coinstacks/polygon/api/src/app.ts (1 hunks)
  • node/coinstacks/solana/api/package.json (1 hunks)
  • node/coinstacks/solana/api/src/app.ts (1 hunks)
  • node/coinstacks/solana/api/src/websocket.ts (4 hunks)
  • node/packages/blockbook/src/websocket.ts (4 hunks)
  • node/packages/prometheus/package.json (1 hunks)
  • node/packages/prometheus/src/index.ts (1 hunks)
  • node/packages/prometheus/tsconfig.json (1 hunks)
  • node/packages/websocket/package.json (1 hunks)
  • node/packages/websocket/src/connectionHandler.ts (1 hunks)
  • node/packages/websocket/src/index.ts (1 hunks)
  • node/packages/websocket/src/websocket.ts (3 hunks)
  • node/proxy/api/src/app.ts (2 hunks)
  • node/proxy/api/src/coincap.ts (1 hunks)
  • node/proxy/api/src/marketData.ts (1 hunks)
  • node/proxy/sample.env (1 hunks)
  • package.json (0 hunks)
💤 Files with no reviewable changes (2)
  • node/coinstacks/common/api/src/index.ts
  • package.json
🚧 Files skipped from review as they are similar to previous changes (38)
  • node/coinstacks/bnbsmartchain/api/package.json
  • node/coinstacks/ethereum/api/src/app.ts
  • node/coinstacks/gnosis/api/package.json
  • node/packages/websocket/package.json
  • node/coinstacks/bitcoincash/api/package.json
  • node/packages/websocket/src/index.ts
  • node/coinstacks/bitcoin/api/src/app.ts
  • node/coinstacks/avalanche/api/src/app.ts
  • node/coinstacks/litecoin/api/package.json
  • node/packages/prometheus/tsconfig.json
  • node/coinstacks/litecoin/api/src/app.ts
  • node/coinstacks/dogecoin/api/package.json
  • node/coinstacks/gnosis/api/src/app.ts
  • node/coinstacks/ethereum/api/package.json
  • node/coinstacks/common/api/src/registry.ts
  • node/coinstacks/bnbsmartchain/api/src/app.ts
  • node/coinstacks/dogecoin/api/src/app.ts
  • node/coinstacks/arbitrum-nova/api/package.json
  • node/packages/prometheus/package.json
  • node/coinstacks/optimism/api/package.json
  • node/coinstacks/polygon/api/package.json
  • node/coinstacks/base/api/src/app.ts
  • node/coinstacks/common/api/package.json
  • node/coinstacks/polygon/api/src/app.ts
  • node/coinstacks/optimism/api/src/app.ts
  • node/coinstacks/solana/api/package.json
  • node/coinstacks/arbitrum/api/package.json
  • node/packages/prometheus/src/index.ts
  • node/proxy/sample.env
  • node/proxy/api/src/marketData.ts
  • node/coinstacks/solana/api/src/websocket.ts
  • node/coinstacks/bitcoincash/api/src/app.ts
  • node/packages/websocket/src/websocket.ts
  • node/coinstacks/base/api/package.json
  • node/coinstacks/arbitrum-nova/api/src/app.ts
  • node/packages/blockbook/src/websocket.ts
  • node/proxy/api/src/app.ts
  • node/coinstacks/arbitrum/api/src/app.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-20T06:35:01.854Z
Learnt from: premiumjibles
PR: shapeshift/unchained#1153
File: node/proxy/api/src/marketData.ts:33-33
Timestamp: 2025-08-20T06:35:01.854Z
Learning: premiumjibles confirmed that CoinCap v3 uses wss.coincap.io as the host for WebSocket connections, not ws.coincap.io as in previous versions.

Applied to files:

  • node/proxy/api/src/coincap.ts
📚 Learning: 2025-08-20T06:35:03.599Z
Learnt from: premiumjibles
PR: shapeshift/unchained#1153
File: node/proxy/api/src/marketData.ts:104-107
Timestamp: 2025-08-20T06:35:03.599Z
Learning: For WebSocket integrations with specific financial data APIs like CoinCap, the message payloads are typically JSON strings, so simple .toString() handling is usually sufficient rather than comprehensive type checking for all possible ws message payload types.

Applied to files:

  • node/proxy/api/src/coincap.ts
🧬 Code graph analysis (3)
node/packages/websocket/src/connectionHandler.ts (1)
node/packages/prometheus/src/prometheus.ts (1)
  • Prometheus (7-35)
node/proxy/api/src/coincap.ts (2)
node/proxy/api/src/marketData.ts (3)
  • MarketDataClient (22-30)
  • MarketDataConnectionHandler (32-83)
  • MarketDataMessage (7-12)
node/packages/websocket/src/websocket.ts (2)
  • Args (16-19)
  • Options (21-25)
node/coinstacks/common/api/src/websocket.ts (2)
node/coinstacks/common/api/src/registry.ts (1)
  • Registry (25-161)
node/packages/prometheus/src/prometheus.ts (1)
  • Prometheus (7-35)
🪛 Biome (2.1.2)
node/packages/websocket/src/connectionHandler.ts

[error] 87-87: The function should not return a value because its return type is void.

The function is here:

'void' signals the absence of value. The returned value is likely to be ignored by the caller.

(lint/correctness/noVoidTypeReturn)


[error] 90-90: The function should not return a value because its return type is void.

The function is here:

'void' signals the absence of value. The returned value is likely to be ignored by the caller.

(lint/correctness/noVoidTypeReturn)


[error] 92-92: The function should not return a value because its return type is void.

The function is here:

'void' signals the absence of value. The returned value is likely to be ignored by the caller.

(lint/correctness/noVoidTypeReturn)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: check
🔇 Additional comments (3)
node/coinstacks/solana/api/src/app.ts (1)

3-3: Prometheus import migration approved. Confirmed solana/api depends on @shapeshiftoss/prometheus@^10.0.0, no lingering @shapeshiftoss/common-api Prometheus imports, and the new package exports Prometheus.

node/coinstacks/avalanche/api/package.json (1)

16-17: Prometheus migration dependency added — approve; confirm no lingering common-api Prometheus usage

Change matches the migration pattern. Ensure code imports reference @shapeshiftoss/prometheus and not @shapeshiftoss/common-api.

Use the verification script provided in the bitcoin-api comment; it covers this package as well.

node/coinstacks/common/api/src/websocket.ts (1)

88-95: LGTM on cleanup flow

Unsubscribing all and resubscribing aggregate addresses on close is consistent with the registry model.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
node/proxy/api/src/coincap.ts (3)

41-43: Deduplicate subscription assets at add time.

Prevents redundant per-message lookups and needless growth.

-    clientSubscriptions.set(subscriptionId, assets)
+    clientSubscriptions.set(subscriptionId, Array.from(new Set(assets)))

74-93: Support ALL/empty-assets broadcast and avoid needless filtering.

Honor “ALL” (or empty) to pass-through the provider payload.

-        // Send updates for each subscription
-        for (const [subscriptionId, assets] of clientSubscriptions) {
-          // Filter data to only include assets this subscription requested
-          const filteredData: Record<string, string> = {}
-          for (const asset of assets) {
-            if (message[asset] !== undefined) {
-              filteredData[asset] = message[asset]
-            }
-          }
+        // Send updates for each subscription
+        for (const [subscriptionId, assets] of clientSubscriptions) {
+          const wantsAll = !assets?.length || assets.includes('ALL')
+          const filteredData: Record<string, string> = wantsAll ? message : {}
+          if (!wantsAll) {
+            for (const asset of assets) {
+              if (message[asset] !== undefined) {
+                filteredData[asset] = message[asset]
+              }
+            }
+          }

7-9: Keep connection and subscriptions in one structure to avoid drift.

Optional: store { connection, subscriptions } per clientId to ensure Maps never get out of sync.

node/proxy/api/src/marketData.ts (1)

45-58: Normalize/dedupe assets before delegating to client.

Reduces downstream filtering misses due to casing/whitespace and avoids duplicates.

-    this.subscriptionIds.add(subscriptionId)
-    this.client.subscribe(this.clientId, subscriptionId, this, data.assets)
+    this.subscriptionIds.add(subscriptionId)
+    const uniqueAssets = Array.from(new Set(data.assets.map((a) => a.trim().toLowerCase())))
+    this.client.subscribe(this.clientId, subscriptionId, this, uniqueAssets)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between efe8cbd and bbf2cb0.

📒 Files selected for processing (2)
  • node/proxy/api/src/coincap.ts (1 hunks)
  • node/proxy/api/src/marketData.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-20T06:35:01.854Z
Learnt from: premiumjibles
PR: shapeshift/unchained#1153
File: node/proxy/api/src/marketData.ts:33-33
Timestamp: 2025-08-20T06:35:01.854Z
Learning: premiumjibles confirmed that CoinCap v3 uses wss.coincap.io as the host for WebSocket connections, not ws.coincap.io as in previous versions.

Applied to files:

  • node/proxy/api/src/coincap.ts
  • node/proxy/api/src/marketData.ts
📚 Learning: 2025-08-20T06:35:03.599Z
Learnt from: premiumjibles
PR: shapeshift/unchained#1153
File: node/proxy/api/src/marketData.ts:104-107
Timestamp: 2025-08-20T06:35:03.599Z
Learning: For WebSocket integrations with specific financial data APIs like CoinCap, the message payloads are typically JSON strings, so simple .toString() handling is usually sufficient rather than comprehensive type checking for all possible ws message payload types.

Applied to files:

  • node/proxy/api/src/coincap.ts
  • node/proxy/api/src/marketData.ts
🧬 Code graph analysis (2)
node/proxy/api/src/coincap.ts (2)
node/proxy/api/src/marketData.ts (3)
  • MarketDataClient (22-30)
  • MarketDataConnectionHandler (32-78)
  • MarketDataMessage (7-12)
node/packages/websocket/src/websocket.ts (2)
  • Args (16-19)
  • Options (21-25)
node/proxy/api/src/marketData.ts (2)
node/packages/websocket/src/connectionHandler.ts (1)
  • MessageResponse (18-20)
node/packages/prometheus/src/prometheus.ts (1)
  • Prometheus (7-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: check
🔇 Additional comments (2)
node/proxy/api/src/coincap.ts (1)

68-96: Publish uses subscriptionId (not clientId) — LGTM.

This fixes the earlier contract violation noted in review. Good.

node/proxy/api/src/marketData.ts (1)

7-12: Message type definition is clear and specific — good.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
node/packages/websocket/src/connectionHandler.ts (2)

45-49: Start and arm heartbeat immediately (and on each ping send).

Arm the timeout right after the initial ping and on each interval tick so dead connections without a first pong are terminated.

     this.websocket = websocket
-    this.websocket.ping()
+    this.websocket.ping()
+    this.heartbeat()
 
     const pingInterval = setInterval(() => {
-      this.websocket.ping()
+      this.websocket.ping()
+      this.heartbeat()
     }, this.pingIntervalMs)

80-93: Fix Biome error: don't return values from a void method.

Remove expression returns in switch cases.

   private onMessage(event: WebSocket.MessageEvent): void {
     try {
       const payload: RequestPayload = JSON.parse(event.data.toString())

       switch (payload.method) {
         // browsers do not support ping/pong frame, handle message instead
         case 'ping': {
-          return this.websocket.send('pong')
+          this.websocket.send('pong')
+          return
         }
         case 'subscribe':
-          return this.onSubscribe(payload.subscriptionId, payload.data)
+          this.onSubscribe(payload.subscriptionId, payload.data)
+          return
         case 'unsubscribe':
-          return this.onUnsubscribe(payload.subscriptionId)
+          this.onUnsubscribe(payload.subscriptionId)
+          return
       }
node/proxy/api/src/marketData.ts (1)

18-20: Harden SubscribePayload guard.

Validate array shape and element types to protect the boundary.

-function isSubscribePayload(data: unknown): data is SubscribePayload {
-  return data !== null && typeof data === 'object' && 'assets' in data
-}
+function isSubscribePayload(data: unknown): data is SubscribePayload {
+  if (!data || typeof data !== 'object') return false
+  const assets = (data as any).assets
+  return Array.isArray(assets) && assets.length > 0 && assets.every((a) => typeof a === 'string' && a.length > 0)
+}
🧹 Nitpick comments (3)
node/packages/websocket/src/connectionHandler.ts (2)

51-54: Terminate socket on error; rely on onclose for cleanup.

Avoid manual close path on error; let 'close' event handle metrics and teardown.

-    this.websocket.onerror = (error) => {
-      this.logger.error({ clientId: this.clientId, error, fn: 'ws.onerror' }, 'websocket error')
-      this.close(pingInterval)
-    }
+    this.websocket.onerror = (error) => {
+      this.logger.error({ clientId: this.clientId, error, fn: 'ws.onerror' }, 'websocket error')
+      // Force shutdown; onclose will run cleanup and metrics dec.
+      this.websocket.terminate()
+    }

60-62: Count inbound client pings as liveness too.

Update heartbeat on 'ping' from client to avoid false timeouts when client drives heartbeats.

-    this.websocket.on('ping', () => this.websocket.pong())
+    this.websocket.on('ping', () => {
+      this.websocket.pong()
+      this.heartbeat()
+    })
node/proxy/api/src/marketData.ts (1)

45-58: Prevent duplicate subscriptionIds per client (idempotency/clarity).

Avoid double wiring the same subscription; either reject or treat as idempotent.

   onSubscribe(subscriptionId: string, data?: unknown): void {
     if (!subscriptionId) {
       this.sendError('subscriptionId required', subscriptionId)
       return
     }
 
     if (!isSubscribePayload(data)) {
       this.sendError(`invalid subscription payload, no assets provided`, subscriptionId)
       return
     }
 
-    this.subscriptionIds.add(subscriptionId)
+    if (this.subscriptionIds.has(subscriptionId)) {
+      this.sendError('subscriptionId already active', subscriptionId)
+      return
+    }
+    this.subscriptionIds.add(subscriptionId)
     this.client.subscribe(this.clientId, subscriptionId, this, data.assets)
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bbf2cb0 and 9cbf0e1.

📒 Files selected for processing (2)
  • node/packages/websocket/src/connectionHandler.ts (1 hunks)
  • node/proxy/api/src/marketData.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: premiumjibles
PR: shapeshift/unchained#1153
File: node/proxy/api/src/marketData.ts:33-33
Timestamp: 2025-08-20T06:35:01.854Z
Learning: premiumjibles confirmed that CoinCap v3 uses wss.coincap.io as the host for WebSocket connections, not ws.coincap.io as in previous versions.
📚 Learning: 2025-08-20T06:35:01.854Z
Learnt from: premiumjibles
PR: shapeshift/unchained#1153
File: node/proxy/api/src/marketData.ts:33-33
Timestamp: 2025-08-20T06:35:01.854Z
Learning: premiumjibles confirmed that CoinCap v3 uses wss.coincap.io as the host for WebSocket connections, not ws.coincap.io as in previous versions.

Applied to files:

  • node/proxy/api/src/marketData.ts
📚 Learning: 2025-08-20T06:35:03.599Z
Learnt from: premiumjibles
PR: shapeshift/unchained#1153
File: node/proxy/api/src/marketData.ts:104-107
Timestamp: 2025-08-20T06:35:03.599Z
Learning: For WebSocket integrations with specific financial data APIs like CoinCap, the message payloads are typically JSON strings, so simple .toString() handling is usually sufficient rather than comprehensive type checking for all possible ws message payload types.

Applied to files:

  • node/proxy/api/src/marketData.ts
🧬 Code graph analysis (2)
node/proxy/api/src/marketData.ts (3)
node/packages/websocket/src/connectionHandler.ts (1)
  • MessageResponse (18-20)
node/packages/prometheus/src/prometheus.ts (1)
  • Prometheus (7-35)
node/proxy/api/src/app.ts (1)
  • logger (18-21)
node/packages/websocket/src/connectionHandler.ts (1)
node/packages/prometheus/src/prometheus.ts (1)
  • Prometheus (7-35)
🪛 Biome (2.1.2)
node/packages/websocket/src/connectionHandler.ts

[error] 87-87: The function should not return a value because its return type is void.

The function is here:

'void' signals the absence of value. The returned value is likely to be ignored by the caller.

(lint/correctness/noVoidTypeReturn)


[error] 90-90: The function should not return a value because its return type is void.

The function is here:

'void' signals the absence of value. The returned value is likely to be ignored by the caller.

(lint/correctness/noVoidTypeReturn)


[error] 92-92: The function should not return a value because its return type is void.

The function is here:

'void' signals the absence of value. The returned value is likely to be ignored by the caller.

(lint/correctness/noVoidTypeReturn)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: check
🔇 Additional comments (2)
node/proxy/api/src/marketData.ts (2)

60-68: Unsubscribe semantics look good.

Supports single-subscription and bulk unsubscribe paths cleanly.


70-72: Good: cleanup on connection close.

Ensures provider-side teardown when client disconnects.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Fix all issues with AI Agents 🤖
In @node/coinstacks/common/api/src/evm/moralisService.ts:
- Line 46: MoralisService implements AddressSubscriptionClient but its
subscribeAddresses/unsubscribeAddresses signatures don't match
AddressSubscriptionWebsocketClient; update MoralisService.subscribeAddresses and
MoralisService.unsubscribeAddresses to accept (currentAddresses: string[],
addressesToAdd: string[]) and (currentAddresses: string[], addressesToRemove:
string[]) respectively, adjust internal logic to use the two-parameter form
(e.g., compute adds/removes based on provided arrays), and update any local
usages inside the class (and tests if present) to the new signatures so the
class satisfies the interface.

In @node/proxy/api/src/app.ts:
- Around line 24-28: The middleware is being initialized without the Prometheus
instance so HTTP metrics aren't recorded; update the app initialization to pass
the created Prometheus instance into the common middleware by calling
middleware.common(prometheus) where app.use(...middleware.common()) is invoked
(ensure you're using the same Prometheus variable created by new Prometheus({
coinstack: 'proxy' }) so the middleware can register metrics).
- Line 17: COINCAP_API_KEY is read from env but not validated, so the WebSocket
URL later (where COINCAP_API_KEY is interpolated) can contain "undefined"; add a
guard right after the declaration of COINCAP_API_KEY to validate it is non-empty
and fail fast with a clear error (throw an Error or log and process.exit)
referencing COINCAP_API_KEY; ensure the message clearly states the missing
COINCAP_API_KEY and stops startup so the WebSocket construction/connection code
(the spot that builds the WS URL) never runs with an undefined key.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
node/packages/blockbook/src/websocket.ts (1)

84-97: Method signatures don't match AddressSubscriptionWebsocketClient abstract methods.

The base class AddressSubscriptionWebsocketClient (in node/packages/websocket/src/websocket.ts lines 111-112) declares:

abstract subscribeAddresses(currentAddresses: Array<string>, addressesToAdd: Array<string>): void
abstract unsubscribeAddresses(currentAddresses: Array<string>, addressesToRemove: Array<string>): void

But this implementation only accepts a single parameter. This will fail TypeScript compilation.

🔎 Proposed fix
- subscribeAddresses(addresses: Array<string>): void {
-   this.addresses = addresses
+ subscribeAddresses(_currentAddresses: Array<string>, addressesToAdd: Array<string>): void {
+   this.addresses = [...new Set([...this.addresses, ...addressesToAdd])]
    const subscribeAddresses = this.getAddressesSubscription()

    try {
      this.socket?.send(JSON.stringify(subscribeAddresses))
    } catch (err) {
      this.logger.debug(err, `failed to subscribe addresses: ${JSON.stringify(subscribeAddresses)}`)
    }
  }

- unsubscribeAddresses(addresses: Array<string>): void {
-   this.subscribeAddresses(addresses)
+ unsubscribeAddresses(_currentAddresses: Array<string>, addressesToRemove: Array<string>): void {
+   this.addresses = this.addresses.filter(addr => !addressesToRemove.includes(addr))
+   const subscribeAddresses = this.getAddressesSubscription()
+
+   try {
+     this.socket?.send(JSON.stringify(subscribeAddresses))
+   } catch (err) {
+     this.logger.debug(err, `failed to unsubscribe addresses: ${JSON.stringify(subscribeAddresses)}`)
+   }
  }
♻️ Duplicate comments (1)
node/proxy/api/src/app.ts (1)

64-74: Add graceful shutdown handlers for WebSocket connections.

The server and WebSocket connections lack graceful shutdown handlers (SIGTERM/SIGINT). This can lead to abrupt disconnections and incomplete cleanup. Based on learnings, the CoinCap URL format using wss://wss.coincap.io is correct for v3.

🔎 Proposed fix
 wsServer.on('connection', (connection) => {
   MarketDataConnectionHandler.start(connection, coincap, prometheus, logger)
 })
+
+const shutdown = (signal: string) => {
+  logger.info(`${signal} received, closing connections`)
+  
+  wsServer.close(() => {
+    coincap.disconnect()
+    server.close(() => {
+      logger.info('Server closed')
+      process.exit(0)
+    })
+  })
+  
+  // Force exit after timeout
+  setTimeout(() => {
+    logger.error('Forced shutdown after timeout')
+    process.exit(1)
+  }, 10000)
+}
+
+process.on('SIGTERM', () => shutdown('SIGTERM'))
+process.on('SIGINT', () => shutdown('SIGINT'))
🧹 Nitpick comments (2)
node/proxy/api/src/app.ts (1)

30-30: Consider including WebSocket connection count in health check.

The coinstacks include connections: wsServer.clients.size in their health responses (e.g., optimism line 24), which aids monitoring. Consider adding this for consistency.

🔎 Proposed enhancement
-app.get('/health', async (_, res) => res.json({ status: 'ok' }))
+app.get('/health', async (_, res) => res.json({ status: 'ok', connections: wsServer?.clients.size ?? 0 }))

Note: This requires moving the health endpoint registration after wsServer is created, or using a function that accesses wsServer at runtime.

node/packages/websocket/src/connectionHandler.ts (1)

110-112: Consider guarding publish against closed connections.

If publish is called after the websocket is closed or in a closing state, this.websocket.send() may throw. Consider checking readyState or wrapping in try-catch.

🔎 Proposed fix
  publish(subscriptionId: string, payload: { data: unknown } & Record<string, unknown>): void {
+   if (this.websocket.readyState !== WebSocket.OPEN) return
    this.websocket.send(JSON.stringify({ ...payload, subscriptionId }))
  }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9cbf0e1 and 4a6409c.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (40)
  • node/coinstacks/arbitrum-nova/api/package.json
  • node/coinstacks/arbitrum/api/package.json
  • node/coinstacks/arbitrum/api/src/app.ts
  • node/coinstacks/avalanche/api/package.json
  • node/coinstacks/avalanche/api/src/app.ts
  • node/coinstacks/base/api/package.json
  • node/coinstacks/base/api/src/app.ts
  • node/coinstacks/bitcoin/api/package.json
  • node/coinstacks/bitcoincash/api/package.json
  • node/coinstacks/bitcoincash/api/src/app.ts
  • node/coinstacks/bnbsmartchain/api/package.json
  • node/coinstacks/bnbsmartchain/api/src/app.ts
  • node/coinstacks/common/api/package.json
  • node/coinstacks/common/api/src/evm/moralisService.ts
  • node/coinstacks/common/api/src/middleware.ts
  • node/coinstacks/common/api/src/registry.ts
  • node/coinstacks/common/api/src/websocket.ts
  • node/coinstacks/dogecoin/api/package.json
  • node/coinstacks/ethereum/api/package.json
  • node/coinstacks/ethereum/api/src/app.ts
  • node/coinstacks/gnosis/api/package.json
  • node/coinstacks/gnosis/api/src/app.ts
  • node/coinstacks/litecoin/api/package.json
  • node/coinstacks/litecoin/api/src/app.ts
  • node/coinstacks/optimism/api/package.json
  • node/coinstacks/optimism/api/src/app.ts
  • node/coinstacks/polygon/api/package.json
  • node/coinstacks/polygon/api/src/app.ts
  • node/coinstacks/solana/api/package.json
  • node/coinstacks/solana/api/src/websocket.ts
  • node/coinstacks/zcash/api/package.json
  • node/coinstacks/zcash/api/src/app.ts
  • node/packages/blockbook/src/websocket.ts
  • node/packages/websocket/package.json
  • node/packages/websocket/src/connectionHandler.ts
  • node/packages/websocket/src/websocket.ts
  • node/proxy/api/package.json
  • node/proxy/api/src/app.ts
  • node/proxy/sample.env
  • package.json
💤 Files with no reviewable changes (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (19)
  • node/coinstacks/bitcoincash/api/package.json
  • node/packages/websocket/package.json
  • node/coinstacks/bitcoin/api/package.json
  • node/coinstacks/litecoin/api/src/app.ts
  • node/coinstacks/arbitrum/api/package.json
  • node/coinstacks/gnosis/api/package.json
  • node/coinstacks/avalanche/api/package.json
  • node/coinstacks/litecoin/api/package.json
  • node/coinstacks/common/api/src/middleware.ts
  • node/coinstacks/base/api/src/app.ts
  • node/coinstacks/bnbsmartchain/api/package.json
  • node/coinstacks/ethereum/api/package.json
  • node/coinstacks/gnosis/api/src/app.ts
  • node/coinstacks/solana/api/package.json
  • node/coinstacks/bitcoincash/api/src/app.ts
  • node/coinstacks/dogecoin/api/package.json
  • node/proxy/sample.env
  • node/coinstacks/common/api/src/registry.ts
  • node/coinstacks/arbitrum/api/src/app.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: premiumjibles
Repo: shapeshift/unchained PR: 1153
File: node/proxy/api/src/marketData.ts:33-33
Timestamp: 2025-08-20T06:35:01.854Z
Learning: premiumjibles confirmed that CoinCap v3 uses wss.coincap.io as the host for WebSocket connections, not ws.coincap.io as in previous versions.
Learnt from: premiumjibles
Repo: shapeshift/unchained PR: 1153
File: node/proxy/api/src/marketData.ts:104-107
Timestamp: 2025-08-20T06:35:03.599Z
Learning: For WebSocket integrations with specific financial data APIs like CoinCap, the message payloads are typically JSON strings, so simple .toString() handling is usually sufficient rather than comprehensive type checking for all possible ws message payload types.
📚 Learning: 2025-08-20T06:35:01.854Z
Learnt from: premiumjibles
Repo: shapeshift/unchained PR: 1153
File: node/proxy/api/src/marketData.ts:33-33
Timestamp: 2025-08-20T06:35:01.854Z
Learning: premiumjibles confirmed that CoinCap v3 uses wss.coincap.io as the host for WebSocket connections, not ws.coincap.io as in previous versions.

Applied to files:

  • node/proxy/api/src/app.ts
📚 Learning: 2025-08-20T06:35:03.599Z
Learnt from: premiumjibles
Repo: shapeshift/unchained PR: 1153
File: node/proxy/api/src/marketData.ts:104-107
Timestamp: 2025-08-20T06:35:03.599Z
Learning: For WebSocket integrations with specific financial data APIs like CoinCap, the message payloads are typically JSON strings, so simple .toString() handling is usually sufficient rather than comprehensive type checking for all possible ws message payload types.

Applied to files:

  • node/coinstacks/common/api/src/websocket.ts
🧬 Code graph analysis (5)
node/coinstacks/common/api/src/evm/moralisService.ts (1)
node/packages/websocket/src/websocket.ts (1)
  • AddressSubscriptionClient (115-118)
node/proxy/api/src/app.ts (3)
node/packages/prometheus/src/prometheus.ts (1)
  • Prometheus (7-35)
node/proxy/api/src/coincap.ts (1)
  • CoincapWebsocketClient (6-101)
node/proxy/api/src/marketData.ts (1)
  • MarketDataConnectionHandler (32-73)
node/packages/blockbook/src/websocket.ts (3)
node/coinstacks/solana/api/src/websocket.ts (2)
  • WebsocketClient (20-119)
  • subscribeAddresses (79-103)
node/packages/websocket/src/websocket.ts (1)
  • Subscription (9-14)
node/coinstacks/common/api/src/evm/moralisService.ts (1)
  • subscribeAddresses (824-829)
node/coinstacks/common/api/src/websocket.ts (1)
node/packages/websocket/src/websocket.ts (1)
  • AddressSubscriptionClient (115-118)
node/coinstacks/solana/api/src/websocket.ts (1)
node/packages/blockbook/src/websocket.ts (1)
  • WebsocketClient (17-107)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: check
🔇 Additional comments (27)
node/proxy/api/package.json (1)

16-16: LGTM! Clean dependency addition.

The addition of the dedicated Prometheus package is consistent with the broader refactor across the codebase and maintains version alignment.

node/coinstacks/zcash/api/package.json (1)

17-17: LGTM! Dependency addition aligns with import refactor.

The Prometheus dependency addition is consistent with the package-wide migration pattern and supports the import changes in the corresponding app.ts file.

node/coinstacks/zcash/api/src/app.ts (1)

6-6: LGTM! Clean import migration to dedicated Prometheus package.

The import refactor properly separates Prometheus concerns into a dedicated package while maintaining all existing functionality. This improves modularity without introducing breaking changes.

Also applies to: 9-9

node/coinstacks/arbitrum-nova/api/package.json (1)

16-17: LGTM! Consistent dependency addition.

The Prometheus package addition follows the same pattern across all coinstacks and maintains version consistency.

node/coinstacks/common/api/package.json (1)

18-18: LGTM! Proper dependency management.

Adding Prometheus as a dependency in the common-api package is correct. Packages that directly import from @shapeshiftoss/prometheus should still list it as a direct dependency (as they do), following proper dependency management practices.

node/coinstacks/polygon/api/package.json (1)

16-17: LGTM - Clean dependency addition.

The addition of the @shapeshiftoss/prometheus package is consistent with the refactor to extract Prometheus into a dedicated package. Version alignment with common-api is maintained.

node/coinstacks/ethereum/api/src/app.ts (1)

5-7: LGTM - Consistent refactor pattern.

The Prometheus import refactor follows the same clean pattern as the other coinstacks. The API usage remains unchanged.

node/coinstacks/base/api/package.json (1)

16-17: LGTM - Dependency addition matches refactor pattern.

Consistent with the Prometheus package extraction across coinstacks.

node/coinstacks/bnbsmartchain/api/src/app.ts (1)

5-7: LGTM - Refactor consistently applied.

The Prometheus import has been successfully migrated to the dedicated package, maintaining the same usage pattern across all coinstacks.

node/coinstacks/polygon/api/src/app.ts (1)

5-7: LGTM - Import refactor improves modularity.

The Prometheus import has been cleanly moved from @shapeshiftoss/common-api to the dedicated @shapeshiftoss/prometheus package. The class is properly exported with the expected API surface, and all 15 coinstacks consistently use the identical instantiation pattern, confirming the usage remains unchanged.

node/coinstacks/optimism/api/package.json (1)

16-17: LGTM!

The addition of the @shapeshiftoss/prometheus dependency aligns with the broader refactoring to centralize Prometheus integration across coinstacks.

node/coinstacks/optimism/api/src/app.ts (1)

5-7: LGTM!

The Prometheus import has been cleanly moved to the dedicated @shapeshiftoss/prometheus package. All usage remains consistent.

node/coinstacks/avalanche/api/src/app.ts (1)

5-7: LGTM!

Consistent refactoring pattern moving Prometheus to the dedicated package. This aligns with the changes across other coinstacks.

node/proxy/api/src/app.ts (2)

6-7: LGTM!

Prometheus and WebSocket server imports are appropriate for the new market data integration.


13-14: LGTM!

The market data components are properly imported.

node/coinstacks/solana/api/src/websocket.ts (3)

2-2: LGTM!

The refactor to extend AddressSubscriptionWebsocketClient aligns with the new WebSocket architecture. This pattern is consistent with the blockbook implementation.

Also applies to: 20-20


32-32: LGTM!

The change from initialize() to super.connect() correctly adopts the new base class connection lifecycle.


98-98: LGTM!

Adding optional chaining for socket operations is good defensive coding and consistent with the blockbook implementation.

Also applies to: 110-110

node/packages/blockbook/src/websocket.ts (1)

17-30: LGTM on base class migration.

The transition to extend AddressSubscriptionWebsocketClient and use super.connect() instead of initialize() correctly aligns with the new websocket package structure. The optional chaining on socket?.send calls is a good defensive pattern.

node/packages/websocket/src/connectionHandler.ts (2)

37-64: LGTM on connection lifecycle management.

The constructor properly initializes the heartbeat mechanism by calling this.heartbeat() immediately after the initial ping (line 46), addressing the earlier feedback. Event handlers are correctly wired, and the cleanup logic properly decrements the Prometheus counter on close.


81-101: LGTM on message handling.

The onMessage method now correctly uses side-effect calls followed by bare returns instead of returning expressions, addressing the earlier Biome lint feedback.

node/packages/websocket/src/websocket.ts (2)

52-75: LGTM on connection management refactor.

The connect() method properly guards against duplicate connections (line 53), uses optional chaining for socket operations, and correctly wires up all event handlers. The heartbeat and reset logic are properly initialized on open.


77-89: LGTM on close/reconnection logic.

The close handler now properly:

  • Clears all timeouts (pingTimeout, resetTimeout) in addition to intervals
  • Skips reconnection on normal closure (code 1000)
  • Uses exponential backoff with jitter for reconnection attempts
node/coinstacks/common/api/src/websocket.ts (4)

21-24: Type guard strengthened but could be more robust.

The type guard now correctly checks data.topic === 'txs' and Array.isArray(data.addresses), addressing the core concern from previous feedback. For additional safety, you could verify array elements are strings, but this is optional since the downstream handlers validate address content.


60-90: LGTM on route handling with proper guards.

The onSubscribe and onUnsubscribe methods now properly guard against missing routes by checking !route || typeof route.subscribe !== 'function' before invoking, addressing the previous feedback about potential runtime exceptions on unknown topics.


92-101: LGTM on cleanup lifecycle.

The onClose hook properly iterates through all subscription IDs, unsubscribes from the registry, and notifies the client of unsubscribed addresses. This ensures proper cleanup when the connection closes.


103-138: Implementation correctly uses new two-parameter signatures.

Unlike the blockbook and Solana websocket clients, this implementation correctly calls subscribeAddresses(currentAddresses, subscribedAddresses) and unsubscribeAddresses(currentAddresses, unsubscribedAddresses) with both parameters, matching the AddressSubscriptionClient interface.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Fix all issues with AI Agents 🤖
In @node/proxy/api/src/app.ts:
- Around line 69-79: The WebSocket server (wsServer) isn't closed on process
shutdown; add a shutdown handler that logs shutdown, disconnects the
CoincapWebsocketClient (coincap.disconnect()), closes the wsServer (ensuring
existing connections are closed), then closes the HTTP server (server.close())
and exits; register this handler on SIGTERM and SIGINT so
MarketDataConnectionHandler connections are torn down cleanly and resources are
released.
♻️ Duplicate comments (2)
node/proxy/api/src/marketData.ts (1)

18-20: Strengthen the type guard validation.

The current guard only checks that data is an object with an assets property, but doesn't validate that assets is an array of non-empty strings. This could allow invalid payloads through.

🔎 Proposed enhancement
 function isSubscribePayload(data: unknown): data is SubscribePayload {
-  return data !== null && typeof data === 'object' && 'assets' in data
+  if (!data || typeof data !== 'object') return false
+  const assets = (data as any).assets
+  return (
+    Array.isArray(assets) &&
+    assets.length > 0 &&
+    assets.every((a) => typeof a === 'string' && a.length > 0)
+  )
 }
node/proxy/api/src/app.ts (1)

17-17: Validate COINCAP_API_KEY is provided.

If COINCAP_API_KEY is undefined, the WebSocket URL at line 71 will interpolate the string "undefined", causing connection failures. Add validation to fail fast with a clear error message.

🔎 Proposed fix
-const COINCAP_API_KEY = process.env.COINCAP_API_KEY
+const COINCAP_API_KEY = process.env.COINCAP_API_KEY
+if (!COINCAP_API_KEY) {
+  throw new Error('COINCAP_API_KEY environment variable is required')
+}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4a6409c and 166f117.

📒 Files selected for processing (3)
  • node/proxy/api/src/app.ts
  • node/proxy/api/src/coincap.ts
  • node/proxy/api/src/marketData.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • node/proxy/api/src/coincap.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: premiumjibles
Repo: shapeshift/unchained PR: 1153
File: node/proxy/api/src/marketData.ts:33-33
Timestamp: 2025-08-20T06:35:01.854Z
Learning: premiumjibles confirmed that CoinCap v3 uses wss.coincap.io as the host for WebSocket connections, not ws.coincap.io as in previous versions.
Learnt from: premiumjibles
Repo: shapeshift/unchained PR: 1153
File: node/proxy/api/src/marketData.ts:104-107
Timestamp: 2025-08-20T06:35:03.599Z
Learning: For WebSocket integrations with specific financial data APIs like CoinCap, the message payloads are typically JSON strings, so simple .toString() handling is usually sufficient rather than comprehensive type checking for all possible ws message payload types.
📚 Learning: 2025-08-20T06:35:01.854Z
Learnt from: premiumjibles
Repo: shapeshift/unchained PR: 1153
File: node/proxy/api/src/marketData.ts:33-33
Timestamp: 2025-08-20T06:35:01.854Z
Learning: premiumjibles confirmed that CoinCap v3 uses wss.coincap.io as the host for WebSocket connections, not ws.coincap.io as in previous versions.

Applied to files:

  • node/proxy/api/src/app.ts
  • node/proxy/api/src/marketData.ts
📚 Learning: 2025-08-20T06:35:03.599Z
Learnt from: premiumjibles
Repo: shapeshift/unchained PR: 1153
File: node/proxy/api/src/marketData.ts:104-107
Timestamp: 2025-08-20T06:35:03.599Z
Learning: For WebSocket integrations with specific financial data APIs like CoinCap, the message payloads are typically JSON strings, so simple .toString() handling is usually sufficient rather than comprehensive type checking for all possible ws message payload types.

Applied to files:

  • node/proxy/api/src/app.ts
  • node/proxy/api/src/marketData.ts
🧬 Code graph analysis (2)
node/proxy/api/src/app.ts (4)
go/pkg/api/middleware.go (1)
  • Logger (30-63)
node/packages/prometheus/src/prometheus.ts (1)
  • Prometheus (7-35)
node/proxy/api/src/coincap.ts (1)
  • CoincapWebsocketClient (6-94)
node/proxy/api/src/marketData.ts (1)
  • MarketDataConnectionHandler (32-78)
node/proxy/api/src/marketData.ts (1)
node/packages/websocket/src/connectionHandler.ts (1)
  • MessageResponse (18-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: check
🔇 Additional comments (7)
node/proxy/api/src/app.ts (3)

7-7: Use WebSocketServer import for ws v8+ compatibility.

The codebase uses ws v8.15.0, where the server class is exported as WebSocketServer rather than Server. Using the older Server import may cause type mismatches.

🔎 Proposed fix
-import { Server } from 'ws'
+import { WebSocketServer } from 'ws'

Then update line 75:

-const wsServer = new Server({ server })
+const wsServer = new WebSocketServer({ server })

Based on learnings from the monorepo showing ws v8.15.0 usage across packages.


24-24: Prometheus integration looks good.

The Prometheus instance is correctly instantiated and passed to the common middleware, which will enable HTTP request metrics collection as expected.

Also applies to: 28-28


32-35: Metrics endpoint correctly implemented.

The /metrics endpoint properly sets the content type and exposes Prometheus metrics, following standard conventions for metrics scraping.

node/proxy/api/src/marketData.ts (4)

1-16: Interfaces are well-defined.

The MarketDataMessage and SubscribePayload interfaces are clear and appropriate for market data subscriptions. The imports are correctly scoped for the functionality.


22-30: MarketDataClient interface is well-designed.

The interface provides a clean contract for client implementations, with appropriate parameters for subscribe/unsubscribe operations and support for both targeted and bulk unsubscribe via the optional subscriptionId.


32-43: Constructor pattern is appropriate.

The private constructor with static start() factory method is a good pattern for this connection handler, ensuring proper initialization and preventing misuse of the constructor.


45-77: Subscription lifecycle methods are correctly implemented.

The onSubscribe, onUnsubscribe, and onClose methods properly validate inputs, manage the subscription state, and delegate to the client interface. The defensive checks prevent invalid subscriptions and ensure clean cleanup on disconnect.

@kaladinlight kaladinlight merged commit f187217 into develop Jan 5, 2026
3 checks passed
@kaladinlight kaladinlight deleted the 10283_coincap_poc branch January 5, 2026 21:31
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.

3 participants