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:
Automated Action 2025-05-28 23:08:21 +00:00
parent 70da7ba6b3
commit 85b1f2a581
5 changed files with 341 additions and 42 deletions

View File

@ -1,4 +1,4 @@
from typing import List from typing import Dict, List
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -8,6 +8,7 @@ from app.models.models import ArbitrageEvent
from app.schemas.schemas import (ArbitrageEventResponse, ScanRequest, from app.schemas.schemas import (ArbitrageEventResponse, ScanRequest,
ScanStatusResponse) ScanStatusResponse)
from app.services.scanner import BlockScanner from app.services.scanner import BlockScanner
from app.services.solana_client import SolanaClient
router = APIRouter() router = APIRouter()
@ -23,6 +24,37 @@ async def scan_for_arbitrage(
""" """
scanner = BlockScanner(db) 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 # Add scanning task to background tasks
background_tasks.add_task( background_tasks.add_task(
scanner.scan_blocks, scanner.scan_blocks,
@ -32,6 +64,7 @@ async def scan_for_arbitrage(
# Return current scan status # Return current scan status
status = scanner.get_scan_status() status = scanner.get_scan_status()
status["message"] = "Scan started successfully"
return ScanStatusResponse(**status) return ScanStatusResponse(**status)
@ -44,6 +77,15 @@ async def get_scan_status(
""" """
scanner = BlockScanner(db) scanner = BlockScanner(db)
status = scanner.get_scan_status() 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) return ScanStatusResponse(**status)
@ -121,4 +163,108 @@ async def get_arbitrage_event(
confidence_score=event.confidence_score, confidence_score=event.confidence_score,
detected_at=event.detected_at, detected_at=event.detected_at,
block_slot=block_slot, 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

View File

@ -155,6 +155,7 @@ class ScanStatusResponse(BaseModel):
last_scan_time: datetime last_scan_time: datetime
arbitrage_events_count: int arbitrage_events_count: int
scan_in_progress: bool scan_in_progress: bool
message: Optional[str] = None
class ScanRequest(BaseModel): class ScanRequest(BaseModel):

View File

@ -210,27 +210,63 @@ class BlockScanner:
all_events = [] all_events = []
try: 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 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 # Get latest block if start_slot not provided
if start_slot is None: if start_slot is None:
latest_block = self.solana_client.get_latest_block() logger.info("Start slot not provided, fetching latest block")
# Get block at that slot try:
block_data = self.solana_client.get_block(latest_block["lastValidBlockHeight"]) latest_block = self.solana_client.get_latest_block()
start_slot = block_data["parentSlot"] 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 # Get list of blocks to scan
end_slot = start_slot - num_blocks end_slot = start_slot - num_blocks
if end_slot < 0: if end_slot < 0:
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 # Scan each block
for slot in blocks_to_scan: for slot in blocks_to_scan:
try: try:
logger.info(f"Processing block at slot {slot}")
block_data = self.solana_client.get_block(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) _, events = self.process_block(block_data)
logger.info(f"Found {len(events)} arbitrage events in block {slot}")
all_events.extend(events) all_events.extend(events)
except Exception as e: except Exception as e:
logger.error(f"Error processing block {slot}: {str(e)}") logger.error(f"Error processing block {slot}: {str(e)}")

View File

@ -26,50 +26,131 @@ class SolanaClient:
""" """
Get latest finalized block Get latest finalized block
""" """
response = self.client.get_latest_blockhash() try:
if "error" in response: logger.info(f"Requesting latest blockhash from {self.rpc_url}")
logger.error(f"Error getting latest block: {response['error']}") response = self.client.get_latest_blockhash()
raise Exception(f"Error getting latest block: {response['error']}")
logger.info(f"Got response: {response}")
return response["result"]
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: def get_block(self, slot: int) -> Dict:
""" """
Get block data by slot number Get block data by slot number
""" """
response = self.client.get_block( try:
slot, logger.info(f"Requesting block data for slot {slot} from {self.rpc_url}")
encoding="json",
max_supported_transaction_version=0, response = self.client.get_block(
transaction_details="full", slot,
) encoding="json",
max_supported_transaction_version=0,
if "error" in response: transaction_details="full",
logger.error(f"Error getting block {slot}: {response['error']}") )
raise Exception(f"Error getting block {slot}: {response['error']}")
logger.debug(f"Raw block response: {response}")
return response["result"]
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]: def get_blocks(self, start_slot: int, end_slot: int = None) -> List[int]:
""" """
Get a list of confirmed blocks Get a list of confirmed blocks
""" """
if end_slot is None: try:
# Get latest slot if end_slot not provided logger.info(f"Getting blocks from {start_slot} to {end_slot or 'latest'}")
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']}")
end_slot = response["result"] if end_slot is None:
# Get latest slot if end_slot not provided
response = self.client.get_blocks(start_slot, end_slot) logger.info("No end_slot provided, fetching latest slot")
response = self.client.get_slot()
if "error" in response:
logger.error(f"Error getting blocks from {start_slot} to {end_slot}: {response['error']}") logger.debug(f"get_slot response: {response}")
raise Exception(f"Error getting blocks from {start_slot} to {end_slot}: {response['error']}")
if "error" in response:
return response["result"] 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: def get_transaction(self, signature: str) -> Dict:
""" """

39
main.py
View File

@ -1,4 +1,5 @@
import logging import logging
from datetime import datetime
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
@ -14,6 +15,12 @@ logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 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__) logger = logging.getLogger(__name__)
app = FastAPI( app = FastAPI(
@ -40,8 +47,36 @@ app.include_router(api_router, prefix=settings.API_V1_STR)
# Health check endpoint # Health check endpoint
@app.get("/health", tags=["health"]) @app.get("/health", tags=["health"])
async def health_check(): async def health_check():
"""Health check endpoint""" """Health check endpoint with Solana RPC connectivity status"""
return {"status": "ok"} 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") @app.on_event("startup")