-
Notifications
You must be signed in to change notification settings - Fork 49
feat: add new market data socket with coincap provider for live price updates #1153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
📝 WalkthroughWalkthroughAdds a websocket-based market-data subsystem (connection handler, CoinCap client), a websocket base package with heartbeat/routing, a new Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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.
📒 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.
|
Looks good overall. Some suggestions:
I will push up a draft for reference |
|
@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? |
75c16ef to
efe8cbd
Compare
There was a problem hiding this 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/urlincludes query params, exploding series cardinality. Preferreq.pathand 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: Renameheliusto provider-agnosticwsClientfor 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/'. Usereq.pathand 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
finishmisses aborted responses. If you care about those, handlecloseonce 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: Preferinstanceoffor custom error detection.If
ApiErroris a class,instanceof ApiErroris 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→closeonerror 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 neededYou’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 codeUse 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.
📒 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 usageChange 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 flowUnsubscribing all and resubscribing aggregate addresses on close is consistent with the registry model.
aec767a to
9cbf0e1
Compare
There was a problem hiding this 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.
📒 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.tsnode/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.tsnode/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.
There was a problem hiding this 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.
📒 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.
There was a problem hiding this 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 matchAddressSubscriptionWebsocketClientabstract methods.The base class
AddressSubscriptionWebsocketClient(innode/packages/websocket/src/websocket.tslines 111-112) declares:abstract subscribeAddresses(currentAddresses: Array<string>, addressesToAdd: Array<string>): void abstract unsubscribeAddresses(currentAddresses: Array<string>, addressesToRemove: Array<string>): voidBut 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.iois 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.sizein 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
wsServeris created, or using a function that accesses wsServer at runtime.node/packages/websocket/src/connectionHandler.ts (1)
110-112: Consider guardingpublishagainst closed connections.If
publishis 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.
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (40)
node/coinstacks/arbitrum-nova/api/package.jsonnode/coinstacks/arbitrum/api/package.jsonnode/coinstacks/arbitrum/api/src/app.tsnode/coinstacks/avalanche/api/package.jsonnode/coinstacks/avalanche/api/src/app.tsnode/coinstacks/base/api/package.jsonnode/coinstacks/base/api/src/app.tsnode/coinstacks/bitcoin/api/package.jsonnode/coinstacks/bitcoincash/api/package.jsonnode/coinstacks/bitcoincash/api/src/app.tsnode/coinstacks/bnbsmartchain/api/package.jsonnode/coinstacks/bnbsmartchain/api/src/app.tsnode/coinstacks/common/api/package.jsonnode/coinstacks/common/api/src/evm/moralisService.tsnode/coinstacks/common/api/src/middleware.tsnode/coinstacks/common/api/src/registry.tsnode/coinstacks/common/api/src/websocket.tsnode/coinstacks/dogecoin/api/package.jsonnode/coinstacks/ethereum/api/package.jsonnode/coinstacks/ethereum/api/src/app.tsnode/coinstacks/gnosis/api/package.jsonnode/coinstacks/gnosis/api/src/app.tsnode/coinstacks/litecoin/api/package.jsonnode/coinstacks/litecoin/api/src/app.tsnode/coinstacks/optimism/api/package.jsonnode/coinstacks/optimism/api/src/app.tsnode/coinstacks/polygon/api/package.jsonnode/coinstacks/polygon/api/src/app.tsnode/coinstacks/solana/api/package.jsonnode/coinstacks/solana/api/src/websocket.tsnode/coinstacks/zcash/api/package.jsonnode/coinstacks/zcash/api/src/app.tsnode/packages/blockbook/src/websocket.tsnode/packages/websocket/package.jsonnode/packages/websocket/src/connectionHandler.tsnode/packages/websocket/src/websocket.tsnode/proxy/api/package.jsonnode/proxy/api/src/app.tsnode/proxy/sample.envpackage.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/prometheuspackage 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-apito the dedicated@shapeshiftoss/prometheuspackage. 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/prometheusdependency 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/prometheuspackage. 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
AddressSubscriptionWebsocketClientaligns 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()tosuper.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
AddressSubscriptionWebsocketClientand usesuper.connect()instead ofinitialize()correctly aligns with the new websocket package structure. The optional chaining onsocket?.sendcalls 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
onMessagemethod 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'andArray.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
onSubscribeandonUnsubscribemethods 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
onClosehook 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)andunsubscribeAddresses(currentAddresses, unsubscribedAddresses)with both parameters, matching theAddressSubscriptionClientinterface.
There was a problem hiding this 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
datais an object with anassetsproperty, but doesn't validate thatassetsis 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_KEYis 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.
📒 Files selected for processing (3)
node/proxy/api/src/app.tsnode/proxy/api/src/coincap.tsnode/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.tsnode/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.tsnode/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: UseWebSocketServerimport for ws v8+ compatibility.The codebase uses ws v8.15.0, where the server class is exported as
WebSocketServerrather thanServer. Using the olderServerimport 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
/metricsendpoint 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
MarketDataMessageandSubscribePayloadinterfaces 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, andonClosemethods 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.
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
docker compose up -din the unchained directorydocker compose upinnode/proxy/api{"subscriptionId":"test-123","method":"subscribe","data":{"assets":["bitcoin","ethereum"]}}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
Chores
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.