fix(core): handle block listener errors

This commit is contained in:
smypmsa
2025-08-14 09:49:01 +00:00
parent f061a75498
commit f8e0414ee7
+143 -28
View File
@@ -3,14 +3,17 @@ Universal block listener that works with any platform through the interface syst
""" """
import asyncio import asyncio
import base64
import json import json
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import websockets import websockets
from solders.transaction import VersionedTransaction
from core.client import SolanaClient
from interfaces.core import Platform, TokenInfo from interfaces.core import Platform, TokenInfo
from monitoring.base_listener import BaseTokenListener from monitoring.base_listener import BaseTokenListener
from platforms import get_platform_implementations from platforms import get_platform_implementations, platform_factory
from utils.logger import get_logger from utils.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -23,7 +26,7 @@ class UniversalBlockListener(BaseTokenListener):
self, self,
wss_endpoint: str, wss_endpoint: str,
platforms: list[Platform] | None = None, platforms: list[Platform] | None = None,
): ) -> None:
"""Initialize universal block listener. """Initialize universal block listener.
Args: Args:
@@ -34,9 +37,7 @@ class UniversalBlockListener(BaseTokenListener):
self.wss_endpoint = wss_endpoint self.wss_endpoint = wss_endpoint
self.ping_interval = 20 # seconds self.ping_interval = 20 # seconds
# Import platform factory and get supported platforms # Get supported platforms
from platforms import platform_factory
if platforms is None: if platforms is None:
# Monitor all supported platforms # Monitor all supported platforms
self.platforms = platform_factory.get_supported_platforms() self.platforms = platform_factory.get_supported_platforms()
@@ -46,15 +47,14 @@ class UniversalBlockListener(BaseTokenListener):
# Get event parsers for all platforms # Get event parsers for all platforms
self.platform_parsers = {} self.platform_parsers = {}
self.platform_program_ids = [] self.platform_program_ids = []
# Map program IDs to their parsers for faster lookup
self.program_id_to_parser = {}
for platform in self.platforms: for platform in self.platforms:
try: try:
# Create a simple dummy client that doesn't start blockhash updater
from core.client import SolanaClient
# Create a mock client class to avoid network operations # Create a mock client class to avoid network operations
class DummyClient(SolanaClient): class DummyClient(SolanaClient):
def __init__(self): def __init__(self) -> None:
# Skip SolanaClient.__init__ to avoid starting blockhash updater # Skip SolanaClient.__init__ to avoid starting blockhash updater
self.rpc_endpoint = "http://dummy" self.rpc_endpoint = "http://dummy"
self._client = None self._client = None
@@ -67,7 +67,9 @@ class UniversalBlockListener(BaseTokenListener):
implementations = get_platform_implementations(platform, dummy_client) implementations = get_platform_implementations(platform, dummy_client)
parser = implementations.event_parser parser = implementations.event_parser
self.platform_parsers[platform] = parser self.platform_parsers[platform] = parser
self.platform_program_ids.append(str(parser.get_program_id())) program_id_str = str(parser.get_program_id())
self.platform_program_ids.append(program_id_str)
self.program_id_to_parser[program_id_str] = (platform, parser)
logger.info( logger.info(
f"Registered platform {platform.value} with program ID {parser.get_program_id()}" f"Registered platform {platform.value} with program ID {parser.get_program_id()}"
@@ -139,7 +141,9 @@ class UniversalBlockListener(BaseTokenListener):
logger.info("Reconnecting in 5 seconds...") logger.info("Reconnecting in 5 seconds...")
await asyncio.sleep(5) await asyncio.sleep(5)
async def _subscribe_to_programs(self, websocket) -> None: async def _subscribe_to_programs(
self, websocket: websockets.WebSocketServerProtocol
) -> None:
"""Subscribe to blocks mentioning any of the monitored program IDs. """Subscribe to blocks mentioning any of the monitored program IDs.
Args: Args:
@@ -169,7 +173,7 @@ class UniversalBlockListener(BaseTokenListener):
await websocket.send(subscription_message) await websocket.send(subscription_message)
logger.info(f"Subscribed to blocks mentioning program: {program_id}") logger.info(f"Subscribed to blocks mentioning program: {program_id}")
async def _ping_loop(self, websocket) -> None: async def _ping_loop(self, websocket: websockets.WebSocketServerProtocol) -> None:
"""Keep connection alive with pings. """Keep connection alive with pings.
Args: Args:
@@ -191,7 +195,9 @@ class UniversalBlockListener(BaseTokenListener):
except Exception: except Exception:
logger.exception("Ping error") logger.exception("Ping error")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None: async def _wait_for_token_creation(
self, websocket: websockets.WebSocketServerProtocol
) -> TokenInfo | None:
"""Wait for token creation event from any platform. """Wait for token creation event from any platform.
Args: Args:
@@ -204,6 +210,14 @@ class UniversalBlockListener(BaseTokenListener):
response = await asyncio.wait_for(websocket.recv(), timeout=30) response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response) data = json.loads(response)
# Handle subscription errors
if "error" in data:
logger.error(f"Block subscription error: {data['error']}")
return None
elif "result" in data:
# Subscription confirmation - continue waiting for notifications
return None
if "method" not in data or data["method"] != "blockNotification": if "method" not in data or data["method"] != "blockNotification":
return None return None
@@ -218,21 +232,8 @@ class UniversalBlockListener(BaseTokenListener):
if "transactions" not in block: if "transactions" not in block:
return None return None
# Try each platform's event parser on each transaction # Process all transactions in the block for token creations
for tx in block["transactions"]: return self._process_block_transactions(block["transactions"])
if not isinstance(tx, dict) or "transaction" not in tx:
continue
for platform, parser in self.platform_parsers.items():
# Check if the parser has a block parsing method
if hasattr(parser, "parse_token_creation_from_block"):
token_info = parser.parse_token_creation_from_block(
{"transactions": [tx]}
)
if token_info:
return token_info
return None
except TimeoutError: except TimeoutError:
logger.debug("No data received for 30 seconds") logger.debug("No data received for 30 seconds")
@@ -243,3 +244,117 @@ class UniversalBlockListener(BaseTokenListener):
logger.exception("Error processing WebSocket message") logger.exception("Error processing WebSocket message")
return None return None
def _process_block_transactions(self, transactions: list) -> TokenInfo | None:
"""Process all transactions in a block looking for token creations.
Args:
transactions: List of transaction data from block
Returns:
TokenInfo if a token creation is found, None otherwise
"""
for tx in transactions:
if not isinstance(tx, dict) or "transaction" not in tx:
continue
tx_data = tx["transaction"]
# Handle base64 encoded transaction data
if isinstance(tx_data, list) and len(tx_data) > 0:
token_info = self._parse_encoded_transaction(tx, tx_data[0])
if token_info:
return token_info
# Handle already decoded transaction data (shouldn't happen in blockSubscribe)
elif isinstance(tx_data, dict) and "message" in tx_data:
token_info = self._parse_decoded_transaction(tx, tx_data)
if token_info:
return token_info
return None
def _parse_encoded_transaction(
self, tx: dict, encoded_data: str
) -> TokenInfo | None:
"""Parse base64 encoded transaction data.
Args:
tx: Transaction wrapper from block
encoded_data: Base64 encoded transaction data
Returns:
TokenInfo if token creation found, None otherwise
"""
try:
tx_bytes = base64.b64decode(encoded_data)
transaction = VersionedTransaction.from_bytes(tx_bytes)
# Check if any of the instructions use our monitored programs
for instruction in transaction.message.instructions:
program_id = str(
transaction.message.account_keys[instruction.program_id_index]
)
# Check if this program ID is one we're monitoring
if program_id in self.program_id_to_parser:
platform, parser = self.program_id_to_parser[program_id]
# Try to parse with the appropriate parser
try:
if hasattr(parser, "parse_token_creation_from_block"):
token_info = parser.parse_token_creation_from_block(
{"transactions": [tx]}
)
if token_info:
return token_info
except Exception:
# Expected for non-creation transactions
continue
except Exception:
# Failed to decode transaction - skip it
pass
return None
def _parse_decoded_transaction(self, tx: dict, tx_data: dict) -> TokenInfo | None:
"""Parse already decoded transaction data.
Args:
tx: Transaction wrapper from block
tx_data: Decoded transaction data
Returns:
TokenInfo if token creation found, None otherwise
"""
message = tx_data["message"]
if "instructions" not in message or "accountKeys" not in message:
return None
for ix in message["instructions"]:
if "programIdIndex" not in ix:
continue
program_idx = ix["programIdIndex"]
if program_idx >= len(message["accountKeys"]):
continue
program_id = message["accountKeys"][program_idx]
# Check if this program ID is one we're monitoring
if program_id in self.program_id_to_parser:
platform, parser = self.program_id_to_parser[program_id]
try:
if hasattr(parser, "parse_token_creation_from_block"):
token_info = parser.parse_token_creation_from_block(
{"transactions": [tx]}
)
if token_info:
return token_info
except Exception:
# Expected for non-creation transactions
continue
return None