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.
180 lines
5.6 KiB
Python
180 lines
5.6 KiB
Python
import asyncio
|
|
import json
|
|
import aiohttp
|
|
import pytest
|
|
from main import create_app, load_config
|
|
|
|
|
|
@pytest.fixture
|
|
async def app():
|
|
config = load_config()
|
|
config["proxy_port"] = 8546 # Use different port for testing
|
|
app = create_app(config)
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
async def client(app, aiohttp_client):
|
|
return await aiohttp_client(app)
|
|
|
|
|
|
async def test_http_proxy_getBalance(client):
|
|
"""Test basic HTTP RPC request"""
|
|
request_data = {
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "getBalance",
|
|
"params": ["11111111111111111111111111111112"] # System program ID
|
|
}
|
|
|
|
async with client.post('/', json=request_data) as response:
|
|
assert response.status == 200
|
|
data = await response.json()
|
|
|
|
assert data["jsonrpc"] == "2.0"
|
|
assert data["id"] == 1
|
|
assert "result" in data or "error" in data
|
|
assert "_provider" in data
|
|
assert data["_provider"] != "proxy_error"
|
|
|
|
|
|
async def test_cache_functionality(client):
|
|
"""Test that responses are cached"""
|
|
request_data = {
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "getBalance",
|
|
"params": ["11111111111111111111111111111112"]
|
|
}
|
|
|
|
# First request
|
|
async with client.post('/', json=request_data) as response:
|
|
data1 = await response.json()
|
|
provider1 = data1["_provider"]
|
|
cached1 = data1["_cached"]
|
|
|
|
# Second request (should be cached)
|
|
async with client.post('/', json=request_data) as response:
|
|
data2 = await response.json()
|
|
provider2 = data2["_provider"]
|
|
cached2 = data2["_cached"]
|
|
|
|
# First request shouldn't be cached, second should be
|
|
assert not cached1
|
|
assert cached2
|
|
assert provider2 == "cache"
|
|
|
|
|
|
|
|
async def test_invalid_json_request(client):
|
|
"""Test invalid JSON handling"""
|
|
async with client.post('/', data="invalid json") as response:
|
|
assert response.status == 400
|
|
data = await response.json()
|
|
|
|
assert "error" in data
|
|
assert data["error"]["code"] == -32700
|
|
|
|
|
|
async def test_missing_method(client):
|
|
"""Test missing method handling"""
|
|
request_data = {
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"params": []
|
|
}
|
|
|
|
async with client.post('/', json=request_data) as response:
|
|
assert response.status == 400
|
|
data = await response.json()
|
|
|
|
assert "error" in data
|
|
assert data["error"]["code"] == -32600
|
|
|
|
|
|
async def test_websocket_connection(client):
|
|
"""Test WebSocket connection establishment"""
|
|
try:
|
|
async with client.ws_connect('/ws') as ws:
|
|
# Send a simple subscription request
|
|
subscribe_request = {
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "accountSubscribe",
|
|
"params": [
|
|
"11111111111111111111111111111112",
|
|
{"encoding": "jsonParsed"}
|
|
]
|
|
}
|
|
|
|
await ws.send_str(json.dumps(subscribe_request))
|
|
|
|
# Wait for response (with timeout)
|
|
try:
|
|
msg = await asyncio.wait_for(ws.receive(), timeout=10.0)
|
|
if msg.type == aiohttp.WSMsgType.TEXT:
|
|
response = json.loads(msg.data)
|
|
assert "result" in response or "error" in response
|
|
if "_provider" in response:
|
|
assert response["_provider"] != "proxy_error"
|
|
|
|
except asyncio.TimeoutError:
|
|
# WebSocket might timeout if providers are unavailable
|
|
# This is acceptable for the test
|
|
pass
|
|
|
|
except Exception as e:
|
|
# WebSocket connection might fail if no providers are available
|
|
# This is acceptable for testing environment
|
|
pytest.skip(f"WebSocket test skipped due to provider unavailability: {e}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run tests manually if executed directly
|
|
import sys
|
|
|
|
async def run_tests():
|
|
config = load_config()
|
|
config["proxy_port"] = 8546
|
|
app = create_app(config)
|
|
|
|
# Start the application
|
|
runner = aiohttp.web.AppRunner(app)
|
|
await runner.setup()
|
|
site = aiohttp.web.TCPSite(runner, 'localhost', config["proxy_port"])
|
|
await site.start()
|
|
|
|
print(f"Test server started on port {config['proxy_port']}")
|
|
|
|
try:
|
|
# Simple manual test
|
|
async with aiohttp.ClientSession() as session:
|
|
request_data = {
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "getBalance",
|
|
"params": ["11111111111111111111111111111112"]
|
|
}
|
|
|
|
async with session.post(
|
|
f"http://localhost:{config['proxy_port']}/",
|
|
json=request_data
|
|
) as response:
|
|
data = await response.json()
|
|
print("Response:", json.dumps(data, indent=2))
|
|
|
|
# Test second request (should be cached)
|
|
async with session.post(
|
|
f"http://localhost:{config['proxy_port']}/",
|
|
json=request_data
|
|
) as response:
|
|
cached_data = await response.json()
|
|
print("Cached response:", json.dumps(cached_data, indent=2))
|
|
|
|
except Exception as e:
|
|
print(f"Test error: {e}")
|
|
|
|
finally:
|
|
await runner.cleanup()
|
|
|
|
asyncio.run(run_tests()) |