solana-proxy/ws_proxy.py
afd afa26d0e29 This is a bunch of untested AI slop, first pass.
Implement Solana RPC proxy with automatic failover and caching

- Add multi-provider support for 5 free Solana RPC endpoints (Alchemy, PublicNode, Helius, QuickNode, Solana Public)
- Implement automatic failover with 30-minute backoff for failed providers
- Add disk-based response caching with 100GB LRU eviction
- Create SQLite error logging with UUID tracking
- Support both HTTP JSON-RPC and WebSocket connections
- Include provider-specific authentication handling
- Add response normalization for consistent output
- Write end-to-end tests for core functionality

The proxy provides a unified endpoint that automatically routes requests to available providers, caches responses to reduce load, and logs all errors with retrievable UUIDs for debugging.
2025-07-15 22:10:06 -04:00

126 lines
5.1 KiB
Python

import json
import asyncio
import logging
from typing import Dict, Any, Optional
from aiohttp import web, WSMsgType, ClientSession
from router import Router
class WebSocketProxy:
def __init__(self, router: Router):
self.router = router
self.logger = logging.getLogger(__name__)
self.subscription_mappings: Dict[str, str] = {}
async def handle_ws_connection(self, request: web.Request) -> web.WebSocketResponse:
ws = web.WebSocketResponse()
await ws.prepare(request)
self.logger.info("New WebSocket connection established")
provider = self.router.get_next_available_provider()
if not provider:
await ws.close(code=1011, message=b'No available providers')
return ws
try:
provider_ws = await self._connect_to_provider(provider)
if not provider_ws:
await ws.close(code=1011, message=b'Failed to connect to provider')
return ws
await asyncio.gather(
self._proxy_client_to_provider(ws, provider_ws, provider),
self._proxy_provider_to_client(provider_ws, ws, provider),
return_exceptions=True
)
except Exception as e:
self.logger.error(f"WebSocket proxy error: {e}")
finally:
if not ws.closed:
await ws.close()
return ws
async def _connect_to_provider(self, provider) -> Optional[object]:
try:
session = ClientSession()
ws = await session.ws_connect(provider.ws_url)
self.logger.info(f"Connected to provider {provider.name} WebSocket")
return ws
except Exception as e:
self.logger.error(f"Failed to connect to provider {provider.name}: {e}")
return None
async def _proxy_client_to_provider(self, client_ws: web.WebSocketResponse, provider_ws, provider) -> None:
async for msg in client_ws:
if msg.type == WSMsgType.TEXT:
try:
data = json.loads(msg.data)
transformed_request = provider.transform_request(data)
await provider_ws.send_str(json.dumps(transformed_request))
self.logger.debug(f"Forwarded message to {provider.name}: {data.get('method', 'unknown')}")
except json.JSONDecodeError:
self.logger.warning("Received invalid JSON from client")
except Exception as e:
self.logger.error(f"Error forwarding to provider: {e}")
break
elif msg.type == WSMsgType.ERROR:
self.logger.error(f'WebSocket error: {client_ws.exception()}')
break
elif msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING):
break
async def _proxy_provider_to_client(self, provider_ws, client_ws: web.WebSocketResponse, provider) -> None:
async for msg in provider_ws:
if msg.type == WSMsgType.TEXT:
try:
data = json.loads(msg.data)
transformed_response = provider.transform_response(data)
if "result" in transformed_response and "subscription" in str(transformed_response.get("result", {})):
subscription_id = transformed_response.get("result")
if subscription_id:
self.subscription_mappings[str(subscription_id)] = provider.name
transformed_response["_cached"] = False
transformed_response["_provider"] = provider.name
method = transformed_response.get("method", "")
params = transformed_response.get("params", {})
if method and params:
self.router.cache.set(method, params, transformed_response)
await client_ws.send_str(json.dumps(transformed_response))
self.logger.debug(f"Forwarded response from {provider.name}")
except json.JSONDecodeError:
self.logger.warning(f"Received invalid JSON from provider {provider.name}")
except Exception as e:
self.logger.error(f"Error forwarding from provider: {e}")
break
elif msg.type == WSMsgType.ERROR:
self.logger.error(f'Provider WebSocket error: {provider_ws.exception()}')
break
elif msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING):
break
async def handle_ws_connection(request: web.Request) -> web.WebSocketResponse:
router: Router = request.app['router']
ws_proxy = WebSocketProxy(router)
return await ws_proxy.handle_ws_connection(request)
def setup_ws_routes(app: web.Application) -> None:
app.router.add_get('/ws', handle_ws_connection)