Enhance Solana RPC client with better error handling and debugging
- Add detailed logging throughout the Solana client and scanner - Improve error handling in RPC client methods - Add debug endpoints to validate Solana connection - Add message field to scan status responses - Enhance health endpoint with RPC connectivity status - Handle invalid block ranges and API rate limits
This commit is contained in:
parent
70da7ba6b3
commit
85b1f2a581
@ -1,4 +1,4 @@
|
||||
from typing import List
|
||||
from typing import Dict, List
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
@ -8,6 +8,7 @@ from app.models.models import ArbitrageEvent
|
||||
from app.schemas.schemas import (ArbitrageEventResponse, ScanRequest,
|
||||
ScanStatusResponse)
|
||||
from app.services.scanner import BlockScanner
|
||||
from app.services.solana_client import SolanaClient
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -23,6 +24,37 @@ async def scan_for_arbitrage(
|
||||
"""
|
||||
scanner = BlockScanner(db)
|
||||
|
||||
# Check if scan is already in progress
|
||||
if scanner.scan_in_progress:
|
||||
return ScanStatusResponse(
|
||||
last_scanned_block=scanner.get_last_scanned_block() or 0,
|
||||
last_scan_time=scanner.get_scan_status()["last_scan_time"],
|
||||
arbitrage_events_count=scanner.get_scan_status()["arbitrage_events_count"],
|
||||
scan_in_progress=True,
|
||||
message="Scan already in progress, please wait for it to complete"
|
||||
)
|
||||
|
||||
try:
|
||||
# Verify Solana connection before starting scan
|
||||
client = SolanaClient()
|
||||
latest_block = client.get_latest_block()
|
||||
if not latest_block:
|
||||
return ScanStatusResponse(
|
||||
last_scanned_block=scanner.get_last_scanned_block() or 0,
|
||||
last_scan_time=scanner.get_scan_status()["last_scan_time"],
|
||||
arbitrage_events_count=scanner.get_scan_status()["arbitrage_events_count"],
|
||||
scan_in_progress=False,
|
||||
message="Failed to connect to Solana RPC. Please check your connection and try again."
|
||||
)
|
||||
except Exception as e:
|
||||
return ScanStatusResponse(
|
||||
last_scanned_block=scanner.get_last_scanned_block() or 0,
|
||||
last_scan_time=scanner.get_scan_status()["last_scan_time"],
|
||||
arbitrage_events_count=scanner.get_scan_status()["arbitrage_events_count"],
|
||||
scan_in_progress=False,
|
||||
message=f"Error connecting to Solana RPC: {str(e)}"
|
||||
)
|
||||
|
||||
# Add scanning task to background tasks
|
||||
background_tasks.add_task(
|
||||
scanner.scan_blocks,
|
||||
@ -32,6 +64,7 @@ async def scan_for_arbitrage(
|
||||
|
||||
# Return current scan status
|
||||
status = scanner.get_scan_status()
|
||||
status["message"] = "Scan started successfully"
|
||||
return ScanStatusResponse(**status)
|
||||
|
||||
|
||||
@ -44,6 +77,15 @@ async def get_scan_status(
|
||||
"""
|
||||
scanner = BlockScanner(db)
|
||||
status = scanner.get_scan_status()
|
||||
|
||||
# Add helpful message based on status
|
||||
if status["scan_in_progress"]:
|
||||
status["message"] = "Scan is currently in progress"
|
||||
elif status["last_scanned_block"] == 0:
|
||||
status["message"] = "No blocks have been scanned yet. Use the /scan endpoint to start scanning."
|
||||
else:
|
||||
status["message"] = f"Last scan completed at {status['last_scan_time']}. Found {status['arbitrage_events_count']} arbitrage events."
|
||||
|
||||
return ScanStatusResponse(**status)
|
||||
|
||||
|
||||
@ -121,4 +163,108 @@ async def get_arbitrage_event(
|
||||
confidence_score=event.confidence_score,
|
||||
detected_at=event.detected_at,
|
||||
block_slot=block_slot,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/debug/solana-connection", response_model=Dict)
|
||||
async def debug_solana_connection():
|
||||
"""
|
||||
Debug endpoint to test Solana RPC connection
|
||||
"""
|
||||
client = SolanaClient()
|
||||
results = {}
|
||||
|
||||
try:
|
||||
# Test getting latest blockhash
|
||||
latest_block = client.get_latest_block()
|
||||
results["latest_blockhash"] = latest_block
|
||||
|
||||
# Try to get a slot
|
||||
slot_response = client.client.get_slot()
|
||||
if "result" in slot_response:
|
||||
results["current_slot"] = slot_response["result"]
|
||||
|
||||
# Try to get a block
|
||||
try:
|
||||
block_data = client.get_block(slot_response["result"])
|
||||
results["block_data"] = {"has_data": block_data is not None}
|
||||
if block_data:
|
||||
results["block_data"]["parent_slot"] = block_data.get("parentSlot")
|
||||
results["block_data"]["transactions_count"] = len(block_data.get("transactions", []))
|
||||
except Exception as e:
|
||||
results["block_error"] = str(e)
|
||||
|
||||
except Exception as e:
|
||||
results["error"] = str(e)
|
||||
|
||||
# Add RPC URL info
|
||||
results["rpc_url"] = client.rpc_url
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/validate-connection", response_model=Dict)
|
||||
async def validate_solana_connection():
|
||||
"""
|
||||
Validate the Solana RPC connection and return detailed status
|
||||
"""
|
||||
client = SolanaClient()
|
||||
result = {
|
||||
"status": "success",
|
||||
"rpc_url": client.rpc_url,
|
||||
"connection_valid": False,
|
||||
"message": "",
|
||||
}
|
||||
|
||||
try:
|
||||
# Test 1: Get latest blockhash
|
||||
latest_blockhash = client.get_latest_block()
|
||||
result["blockhash_test"] = {
|
||||
"success": latest_blockhash is not None,
|
||||
"data": latest_blockhash if latest_blockhash else "Failed to retrieve"
|
||||
}
|
||||
|
||||
# Test 2: Get current slot
|
||||
try:
|
||||
slot_response = client.client.get_slot()
|
||||
current_slot = slot_response.get("result") if "result" in slot_response else None
|
||||
result["slot_test"] = {
|
||||
"success": current_slot is not None,
|
||||
"data": current_slot if current_slot else "Failed to retrieve"
|
||||
}
|
||||
|
||||
# If we have a slot, try to get a block
|
||||
if current_slot:
|
||||
try:
|
||||
block = client.get_block(current_slot)
|
||||
result["block_test"] = {
|
||||
"success": block is not None,
|
||||
"data": {
|
||||
"has_transactions": block is not None and "transactions" in block,
|
||||
"transaction_count": len(block.get("transactions", [])) if block else 0
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
result["block_test"] = {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
result["slot_test"] = {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
# Determine overall status
|
||||
if result.get("blockhash_test", {}).get("success", False) and result.get("slot_test", {}).get("success", False):
|
||||
result["connection_valid"] = True
|
||||
result["message"] = "Solana RPC connection is valid and functioning properly."
|
||||
else:
|
||||
result["status"] = "error"
|
||||
result["message"] = "Solana RPC connection is not fully functional."
|
||||
|
||||
except Exception as e:
|
||||
result["status"] = "error"
|
||||
result["message"] = f"Error validating Solana RPC connection: {str(e)}"
|
||||
|
||||
return result
|
@ -155,6 +155,7 @@ class ScanStatusResponse(BaseModel):
|
||||
last_scan_time: datetime
|
||||
arbitrage_events_count: int
|
||||
scan_in_progress: bool
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class ScanRequest(BaseModel):
|
||||
|
@ -210,27 +210,63 @@ class BlockScanner:
|
||||
all_events = []
|
||||
|
||||
try:
|
||||
logger.info(f"Starting block scan. Num blocks: {num_blocks}, Start slot: {start_slot}")
|
||||
|
||||
num_blocks = num_blocks or settings.SOLANA_BLOCKS_TO_SCAN
|
||||
logger.info(f"Using RPC URL: {self.solana_client.rpc_url}")
|
||||
|
||||
# Get latest block if start_slot not provided
|
||||
if start_slot is None:
|
||||
latest_block = self.solana_client.get_latest_block()
|
||||
# Get block at that slot
|
||||
block_data = self.solana_client.get_block(latest_block["lastValidBlockHeight"])
|
||||
start_slot = block_data["parentSlot"]
|
||||
logger.info("Start slot not provided, fetching latest block")
|
||||
try:
|
||||
latest_block = self.solana_client.get_latest_block()
|
||||
logger.info(f"Latest block data: {latest_block}")
|
||||
|
||||
# Get block at that slot
|
||||
last_valid_height = latest_block.get("lastValidBlockHeight")
|
||||
if not last_valid_height:
|
||||
logger.error(f"No lastValidBlockHeight in response: {latest_block}")
|
||||
raise ValueError("Invalid response format: missing lastValidBlockHeight")
|
||||
|
||||
logger.info(f"Fetching block at height: {last_valid_height}")
|
||||
block_data = self.solana_client.get_block(last_valid_height)
|
||||
|
||||
if not block_data or "parentSlot" not in block_data:
|
||||
logger.error(f"Invalid block data response: {block_data}")
|
||||
raise ValueError("Invalid block data: missing parentSlot")
|
||||
|
||||
start_slot = block_data["parentSlot"]
|
||||
logger.info(f"Using start_slot: {start_slot}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting latest block: {str(e)}")
|
||||
raise
|
||||
|
||||
# Get list of blocks to scan
|
||||
end_slot = start_slot - num_blocks
|
||||
if end_slot < 0:
|
||||
end_slot = 0
|
||||
|
||||
blocks_to_scan = self.solana_client.get_blocks(end_slot, start_slot)
|
||||
logger.info(f"Getting blocks from {end_slot} to {start_slot}")
|
||||
try:
|
||||
blocks_to_scan = self.solana_client.get_blocks(end_slot, start_slot)
|
||||
logger.info(f"Found {len(blocks_to_scan)} blocks to scan")
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting blocks to scan: {str(e)}")
|
||||
raise
|
||||
|
||||
# Scan each block
|
||||
for slot in blocks_to_scan:
|
||||
try:
|
||||
logger.info(f"Processing block at slot {slot}")
|
||||
block_data = self.solana_client.get_block(slot)
|
||||
|
||||
if not block_data:
|
||||
logger.warning(f"Empty block data for slot {slot}")
|
||||
continue
|
||||
|
||||
logger.info(f"Block {slot} has {len(block_data.get('transactions', []))} transactions")
|
||||
_, events = self.process_block(block_data)
|
||||
logger.info(f"Found {len(events)} arbitrage events in block {slot}")
|
||||
all_events.extend(events)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing block {slot}: {str(e)}")
|
||||
|
@ -26,50 +26,131 @@ class SolanaClient:
|
||||
"""
|
||||
Get latest finalized block
|
||||
"""
|
||||
response = self.client.get_latest_blockhash()
|
||||
if "error" in response:
|
||||
logger.error(f"Error getting latest block: {response['error']}")
|
||||
raise Exception(f"Error getting latest block: {response['error']}")
|
||||
|
||||
return response["result"]
|
||||
try:
|
||||
logger.info(f"Requesting latest blockhash from {self.rpc_url}")
|
||||
response = self.client.get_latest_blockhash()
|
||||
|
||||
logger.info(f"Got response: {response}")
|
||||
|
||||
if "error" in response:
|
||||
error_msg = response.get("error", {})
|
||||
logger.error(f"RPC error getting latest block: {error_msg}")
|
||||
raise Exception(f"RPC error getting latest block: {error_msg}")
|
||||
|
||||
if "result" not in response:
|
||||
logger.error(f"Invalid response format, missing 'result' key: {response}")
|
||||
raise ValueError("Invalid response format, missing 'result' key")
|
||||
|
||||
return response["result"]
|
||||
except Exception as e:
|
||||
logger.error(f"Exception in get_latest_block: {str(e)}")
|
||||
raise
|
||||
|
||||
def get_block(self, slot: int) -> Dict:
|
||||
"""
|
||||
Get block data by slot number
|
||||
"""
|
||||
response = self.client.get_block(
|
||||
slot,
|
||||
encoding="json",
|
||||
max_supported_transaction_version=0,
|
||||
transaction_details="full",
|
||||
)
|
||||
|
||||
if "error" in response:
|
||||
logger.error(f"Error getting block {slot}: {response['error']}")
|
||||
raise Exception(f"Error getting block {slot}: {response['error']}")
|
||||
|
||||
return response["result"]
|
||||
try:
|
||||
logger.info(f"Requesting block data for slot {slot} from {self.rpc_url}")
|
||||
|
||||
response = self.client.get_block(
|
||||
slot,
|
||||
encoding="json",
|
||||
max_supported_transaction_version=0,
|
||||
transaction_details="full",
|
||||
)
|
||||
|
||||
logger.debug(f"Raw block response: {response}")
|
||||
|
||||
if "error" in response:
|
||||
error_msg = response.get("error", {})
|
||||
error_code = error_msg.get("code", "unknown")
|
||||
error_message = error_msg.get("message", "unknown")
|
||||
|
||||
logger.error(f"RPC error getting block {slot}: code={error_code}, message={error_message}")
|
||||
|
||||
# Handle specific error cases
|
||||
if error_code == -32009: # slot not found or not available
|
||||
logger.warning(f"Block at slot {slot} not found or not available")
|
||||
return None
|
||||
|
||||
raise Exception(f"RPC error getting block {slot}: {error_msg}")
|
||||
|
||||
if "result" not in response:
|
||||
logger.error(f"Invalid response format for block {slot}, missing 'result' key: {response}")
|
||||
raise ValueError(f"Invalid response format for block {slot}, missing 'result' key")
|
||||
|
||||
return response["result"]
|
||||
except Exception as e:
|
||||
logger.error(f"Exception in get_block for slot {slot}: {str(e)}")
|
||||
raise
|
||||
|
||||
def get_blocks(self, start_slot: int, end_slot: int = None) -> List[int]:
|
||||
"""
|
||||
Get a list of confirmed blocks
|
||||
"""
|
||||
if end_slot is None:
|
||||
# Get latest slot if end_slot not provided
|
||||
response = self.client.get_slot()
|
||||
if "error" in response:
|
||||
logger.error(f"Error getting latest slot: {response['error']}")
|
||||
raise Exception(f"Error getting latest slot: {response['error']}")
|
||||
try:
|
||||
logger.info(f"Getting blocks from {start_slot} to {end_slot or 'latest'}")
|
||||
|
||||
end_slot = response["result"]
|
||||
|
||||
response = self.client.get_blocks(start_slot, end_slot)
|
||||
|
||||
if "error" in response:
|
||||
logger.error(f"Error getting blocks from {start_slot} to {end_slot}: {response['error']}")
|
||||
raise Exception(f"Error getting blocks from {start_slot} to {end_slot}: {response['error']}")
|
||||
|
||||
return response["result"]
|
||||
if end_slot is None:
|
||||
# Get latest slot if end_slot not provided
|
||||
logger.info("No end_slot provided, fetching latest slot")
|
||||
response = self.client.get_slot()
|
||||
|
||||
logger.debug(f"get_slot response: {response}")
|
||||
|
||||
if "error" in response:
|
||||
error_msg = response.get("error", {})
|
||||
logger.error(f"RPC error getting latest slot: {error_msg}")
|
||||
raise Exception(f"RPC error getting latest slot: {error_msg}")
|
||||
|
||||
if "result" not in response:
|
||||
logger.error(f"Invalid response format, missing 'result' key: {response}")
|
||||
raise ValueError("Invalid response format, missing 'result' key")
|
||||
|
||||
end_slot = response["result"]
|
||||
logger.info(f"Using end_slot: {end_slot}")
|
||||
|
||||
logger.info(f"Requesting blocks from {start_slot} to {end_slot}")
|
||||
|
||||
# Check if the range is valid
|
||||
if start_slot > end_slot:
|
||||
logger.warning(f"Invalid slot range: start={start_slot}, end={end_slot}. Swapping values.")
|
||||
start_slot, end_slot = end_slot, start_slot
|
||||
|
||||
# Limit the range to prevent potential issues with large requests
|
||||
if end_slot - start_slot > 500:
|
||||
logger.warning(f"Large slot range requested: {start_slot} to {end_slot}, limiting to 500 blocks")
|
||||
start_slot = end_slot - 500
|
||||
|
||||
response = self.client.get_blocks(start_slot, end_slot)
|
||||
|
||||
logger.debug(f"get_blocks response: {response}")
|
||||
|
||||
if "error" in response:
|
||||
error_msg = response.get("error", {})
|
||||
logger.error(f"RPC error getting blocks from {start_slot} to {end_slot}: {error_msg}")
|
||||
|
||||
# Handle specific error cases
|
||||
if isinstance(error_msg, dict) and error_msg.get("code") == -32602:
|
||||
logger.warning("Invalid slot range, trying alternative approach")
|
||||
# Use a fallback approach if needed
|
||||
# For now, return an empty list
|
||||
return []
|
||||
|
||||
raise Exception(f"RPC error getting blocks from {start_slot} to {end_slot}: {error_msg}")
|
||||
|
||||
if "result" not in response:
|
||||
logger.error(f"Invalid response format, missing 'result' key: {response}")
|
||||
raise ValueError("Invalid response format, missing 'result' key")
|
||||
|
||||
blocks = response["result"]
|
||||
logger.info(f"Retrieved {len(blocks)} blocks from {start_slot} to {end_slot}")
|
||||
return blocks
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception in get_blocks: {str(e)}")
|
||||
raise
|
||||
|
||||
def get_transaction(self, signature: str) -> Dict:
|
||||
"""
|
||||
|
39
main.py
39
main.py
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
@ -14,6 +15,12 @@ logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
# Enable debug logging for the scanner and Solana client modules
|
||||
logging.getLogger("app.services.scanner").setLevel(logging.DEBUG)
|
||||
logging.getLogger("app.services.solana_client").setLevel(logging.DEBUG)
|
||||
# Also enable debug for solana.rpc
|
||||
logging.getLogger("solana.rpc").setLevel(logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(
|
||||
@ -40,8 +47,36 @@ app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||
# Health check endpoint
|
||||
@app.get("/health", tags=["health"])
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {"status": "ok"}
|
||||
"""Health check endpoint with Solana RPC connectivity status"""
|
||||
from app.services.solana_client import SolanaClient
|
||||
|
||||
health_status = {
|
||||
"status": "ok",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"solana_rpc": {
|
||||
"url": settings.SOLANA_RPC_URL,
|
||||
"connection": "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Solana RPC connection
|
||||
try:
|
||||
client = SolanaClient()
|
||||
response = client.client.get_latest_blockhash()
|
||||
if "result" in response:
|
||||
health_status["solana_rpc"]["connection"] = "ok"
|
||||
health_status["solana_rpc"]["latest_blockhash"] = response["result"].get("blockhash", "unknown")
|
||||
else:
|
||||
health_status["solana_rpc"]["connection"] = "error"
|
||||
health_status["solana_rpc"]["error"] = "No result in response"
|
||||
except Exception as e:
|
||||
health_status["solana_rpc"]["connection"] = "error"
|
||||
health_status["solana_rpc"]["error"] = str(e)
|
||||
|
||||
if health_status["solana_rpc"]["connection"] != "ok":
|
||||
health_status["status"] = "degraded"
|
||||
|
||||
return health_status
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
|
Loading…
x
Reference in New Issue
Block a user