280 lines
7.1 KiB
Markdown
280 lines
7.1 KiB
Markdown
# Solana RPC Proxy Implementation Plan
|
|
|
|
## Project Overview
|
|
A Python-based reverse proxy for Solana RPC endpoints that provides unified access to multiple free providers with automatic failover, response caching, and detailed error tracking.
|
|
|
|
## Architecture Components
|
|
|
|
### 1. Provider Module (`providers.py`)
|
|
**Purpose**: Abstract provider differences and handle authentication
|
|
|
|
```
|
|
Provider class:
|
|
- name: str
|
|
- http_url: str
|
|
- ws_url: str
|
|
- transform_request(request) -> request
|
|
- transform_response(response) -> response
|
|
- is_available() -> bool
|
|
- mark_failed() -> None
|
|
- backoff_until: datetime
|
|
```
|
|
|
|
**Provider Implementations**:
|
|
- `AlchemyProvider` - API key in URL path
|
|
- `PublicNodeProvider` - No auth
|
|
- `HeliusProvider` - API key in query param
|
|
- `QuickNodeProvider` - Token in URL path
|
|
- `SolanaPublicProvider` - No auth
|
|
|
|
### 2. Cache Module (`cache.py`)
|
|
**Purpose**: Disk-based caching for HTTP and WebSocket responses
|
|
|
|
```
|
|
Cache class:
|
|
- get(method: str, params: dict) -> Optional[response]
|
|
- set(method: str, params: dict, response: dict) -> None
|
|
- size_check() -> None # Enforce 100GB limit
|
|
- clear_oldest() -> None # LRU eviction
|
|
```
|
|
|
|
**Implementation Notes**:
|
|
- Use `diskcache` library for simplicity
|
|
- Key format: `f"{method}:{json.dumps(params, sort_keys=True)}"`
|
|
- Store both HTTP responses and WebSocket messages
|
|
- Implement 100GB limit with LRU eviction
|
|
|
|
### 3. Error Logger Module (`errors.py`)
|
|
**Purpose**: SQLite-based error logging with UUID tracking
|
|
|
|
```
|
|
ErrorLogger class:
|
|
- log_error(provider: str, request: dict, error: Exception) -> str (UUID)
|
|
- get_error(error_id: str) -> Optional[dict]
|
|
- setup_db() -> None
|
|
```
|
|
|
|
**Database Schema**:
|
|
```sql
|
|
CREATE TABLE errors (
|
|
id TEXT PRIMARY KEY, -- UUID
|
|
timestamp DATETIME,
|
|
provider TEXT,
|
|
request_method TEXT,
|
|
request_params TEXT, -- JSON
|
|
error_type TEXT,
|
|
error_message TEXT,
|
|
error_traceback TEXT
|
|
);
|
|
```
|
|
|
|
### 4. Response Normalizer Module (`normalizer.py`)
|
|
**Purpose**: Handle minor provider response differences
|
|
|
|
```
|
|
normalize_response(provider: str, response: dict) -> dict
|
|
normalize_error(error: Exception, error_id: str) -> dict
|
|
```
|
|
|
|
**Normalization Tasks**:
|
|
- Ensure consistent field names
|
|
- Handle null vs missing fields
|
|
- Standardize error formats
|
|
- Add proxy metadata (cached, provider used)
|
|
|
|
### 5. Request Router Module (`router.py`)
|
|
**Purpose**: Core failover and routing logic
|
|
|
|
```
|
|
Router class:
|
|
- providers: List[Provider]
|
|
- cache: Cache
|
|
- error_logger: ErrorLogger
|
|
-
|
|
- route_request(method: str, params: dict) -> response
|
|
- get_available_provider() -> Optional[Provider]
|
|
- mark_provider_failed(provider: Provider) -> None
|
|
```
|
|
|
|
**Failover Algorithm**:
|
|
1. Check cache first
|
|
2. Get next available provider (round-robin)
|
|
3. Try request
|
|
4. On success: cache and return
|
|
5. On failure: log error, mark provider failed, try next
|
|
6. All failed: return error with ID
|
|
|
|
### 6. HTTP Proxy Module (`http_proxy.py`)
|
|
**Purpose**: aiohttp server for HTTP JSON-RPC
|
|
|
|
```
|
|
- setup_routes(app: aiohttp.Application)
|
|
- handle_rpc_request(request: aiohttp.Request) -> aiohttp.Response
|
|
```
|
|
|
|
### 7. WebSocket Proxy Module (`ws_proxy.py`)
|
|
**Purpose**: WebSocket subscription handling
|
|
|
|
```
|
|
- handle_ws_connection(request: aiohttp.Request) -> aiohttp.WebSocketResponse
|
|
- proxy_ws_messages(client_ws, provider_ws, cache, provider_name)
|
|
```
|
|
|
|
**WebSocket Complexity**:
|
|
- Maintain subscription ID mapping
|
|
- Cache subscription responses
|
|
- Handle reconnection on provider failure
|
|
|
|
### 8. Main Application (`main.py`)
|
|
**Purpose**: Wire everything together
|
|
|
|
```
|
|
- load_config() -> dict # From .env
|
|
- setup_providers(config) -> List[Provider]
|
|
- create_app() -> aiohttp.Application
|
|
- main() -> None
|
|
```
|
|
|
|
## Configuration (.env)
|
|
|
|
```env
|
|
# Provider endpoints and auth
|
|
ALCHEMY_API_KEY=your_key_here
|
|
HELIUS_API_KEY=your_key_here
|
|
QUICKNODE_ENDPOINT=your_endpoint.quiknode.pro
|
|
QUICKNODE_TOKEN=your_token_here
|
|
|
|
# Proxy settings
|
|
PROXY_PORT=8545
|
|
CACHE_SIZE_GB=100
|
|
BACKOFF_MINUTES=30
|
|
|
|
# Logging
|
|
LOG_LEVEL=INFO
|
|
ERROR_DB_PATH=./errors.db
|
|
```
|
|
|
|
## Rate Limits Documentation
|
|
|
|
```python
|
|
# providers.py comments
|
|
|
|
# Alchemy (Source: https://docs.alchemy.com/reference/throughput)
|
|
# Free tier: 330 CUPs (Compute Units per Second)
|
|
# WebSocket: 10 concurrent requests per connection
|
|
|
|
# PublicNode (Source: https://publicnode.com)
|
|
# No published rate limits - "completely free"
|
|
|
|
# Helius (Source: https://docs.helius.dev/pricing)
|
|
# Free tier: 10 requests/second
|
|
# 1M credits per month
|
|
|
|
# QuickNode (Source: https://www.quicknode.com/pricing)
|
|
# Free tier: 10M credits/month
|
|
# WebSocket: 50 credits per response
|
|
|
|
# Solana Public (Source: https://solana.com/docs/cluster/rpc-endpoints)
|
|
# Rate limits subject to change without notice
|
|
# Not intended for production use
|
|
```
|
|
|
|
## Testing Strategy (`test_e2e.py`)
|
|
|
|
Happy-path end-to-end tests only:
|
|
|
|
1. **Test HTTP Proxy**:
|
|
- Start proxy
|
|
- Make getBalance request
|
|
- Verify response format
|
|
- Verify cache hit on second request
|
|
|
|
2. **Test WebSocket Proxy**:
|
|
- Connect WebSocket
|
|
- Subscribe to account
|
|
- Verify subscription response
|
|
- Verify cached messages
|
|
|
|
3. **Test Failover**:
|
|
- Mock provider failure
|
|
- Verify failover to next provider
|
|
- Verify error logged with ID
|
|
|
|
4. **Test All Providers**:
|
|
- Iterate through each provider
|
|
- Verify basic request works
|
|
- Verify auth handled correctly
|
|
|
|
## Implementation Notes
|
|
|
|
### Functional Programming Style
|
|
- Use pure functions where possible
|
|
- Avoid class state mutations
|
|
- Use immutable data structures
|
|
- Compose small functions
|
|
|
|
### KISS Principles
|
|
- No complex health checking (just try request)
|
|
- No credit tracking (let providers handle)
|
|
- Simple round-robin selection
|
|
- Basic LRU cache eviction
|
|
|
|
### DRY Principles
|
|
- Single Provider base class
|
|
- Reuse request/response transformation
|
|
- Common error handling flow
|
|
- Shared cache logic for HTTP/WS
|
|
|
|
## Deployment Considerations
|
|
|
|
1. **Cache Storage**: Need ~100GB disk space
|
|
2. **Memory Usage**: Keep minimal, use disk cache
|
|
3. **Concurrent Clients**: Basic round-robin if multiple connect
|
|
4. **Monitoring**: Log all errors, provide error IDs
|
|
5. **Security**: Keep API keys in .env, never log them
|
|
|
|
## Future Enhancements (Out of Scope)
|
|
|
|
- Credit/quota tracking
|
|
- Advanced health checking
|
|
- Response time optimization
|
|
- Geographic routing
|
|
- Analytics dashboard
|
|
- Webhook error notifications
|
|
|
|
## File Structure
|
|
|
|
```
|
|
solana-proxy/
|
|
├── .env # Configuration
|
|
├── .env.example # Template
|
|
├── requirements.txt # Dependencies
|
|
├── main.py # Entry point
|
|
├── providers.py # Provider abstractions
|
|
├── cache.py # Caching logic
|
|
├── errors.py # Error logging
|
|
├── normalizer.py # Response normalization
|
|
├── router.py # Request routing
|
|
├── http_proxy.py # HTTP handler
|
|
├── ws_proxy.py # WebSocket handler
|
|
└── test_e2e.py # End-to-end tests
|
|
```
|
|
|
|
## Dependencies
|
|
|
|
```
|
|
aiohttp==3.9.0
|
|
python-dotenv==1.0.0
|
|
diskcache==5.6.0
|
|
aiohttp-cors==0.7.0
|
|
```
|
|
|
|
## Success Criteria
|
|
|
|
1. Single endpoint proxies to 5 providers
|
|
2. Automatic failover works
|
|
3. Responses are cached (up to 100GB)
|
|
4. Errors logged with retrievable IDs
|
|
5. Both HTTP and WebSocket work
|
|
6. Response format is unified
|
|
7. Happy-path tests pass |