wip(core): platform aware trading

This commit is contained in:
smypmsa
2025-08-02 11:30:30 +00:00
parent bb35101a8f
commit 9ec9eeb793
19 changed files with 726 additions and 2038 deletions
+24 -3
View File
@@ -1,15 +1,23 @@
"""
Base class for WebSocket token listeners.
Base class for WebSocket token listeners - now platform-agnostic.
"""
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable
from trading.base import TokenInfo
from interfaces.core import Platform, TokenInfo
class BaseTokenListener(ABC):
"""Base abstract class for token listeners."""
"""Base abstract class for token listeners - now platform-agnostic."""
def __init__(self, platform: Platform | None = None):
"""Initialize the listener with optional platform specification.
Args:
platform: Platform to monitor (if None, monitor all platforms)
"""
self.platform = platform
@abstractmethod
async def listen_for_tokens(
@@ -27,3 +35,16 @@ class BaseTokenListener(ABC):
creator_address: Optional creator address to filter by
"""
pass
def should_process_token(self, token_info: TokenInfo) -> bool:
"""Check if a token should be processed based on platform filter.
Args:
token_info: Token information
Returns:
True if token should be processed
"""
if self.platform is None:
return True # Process all platforms
return token_info.platform == self.platform
-190
View File
@@ -1,190 +0,0 @@
"""
Event processing for pump.fun tokens.
"""
import base64
import json
import struct
from typing import Any
import base58
from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction
from core.pubkeys import PumpAddresses
from trading.base import TokenInfo
from utils.logger import get_logger
logger = get_logger(__name__)
class PumpEventProcessor:
"""Processes events from pump.fun program."""
# Discriminator for create instruction
CREATE_DISCRIMINATOR = 8576854823835016728
def __init__(self, pump_program: Pubkey):
"""Initialize event processor.
Args:
pump_program: Pump.fun program address
"""
self.pump_program = pump_program
self._idl = self._load_idl()
def _load_idl(self) -> dict[str, Any]:
"""Load IDL from file.
Returns:
IDL as dictionary
"""
try:
with open("idl/pump_fun_idl.json") as f:
return json.load(f)
except Exception as e:
logger.error(f"Failed to load IDL: {e!s}")
# Create a minimal IDL with just what we need
return {
"instructions": [
{
"name": "create",
"args": [
{"name": "name", "type": "string"},
{"name": "symbol", "type": "string"},
{"name": "uri", "type": "string"},
],
}
]
}
def process_transaction(self, tx_data: str) -> TokenInfo | None:
"""Process a transaction and extract token info.
Args:
tx_data: Base64 encoded transaction data
Returns:
TokenInfo if a token creation is found, None otherwise
"""
try:
tx_data_decoded = base64.b64decode(tx_data)
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
for ix in transaction.message.instructions:
# Check if instruction is from pump.fun program
program_id_index = ix.program_id_index
if program_id_index >= len(transaction.message.account_keys):
continue
program_id = transaction.message.account_keys[program_id_index]
if str(program_id) != str(self.pump_program):
continue
ix_data = bytes(ix.data)
# Check if it's a create instruction
if len(ix_data) < 8:
continue
discriminator = struct.unpack("<Q", ix_data[:8])[0]
if discriminator != self.CREATE_DISCRIMINATOR:
continue
# Found a create instruction, decode it
create_ix = next(
(
instr
for instr in self._idl["instructions"]
if instr["name"] == "create"
),
None,
)
if not create_ix:
continue
# Get account keys for this instruction
account_keys = [
transaction.message.account_keys[index] for index in ix.accounts
]
# Decode instruction arguments
decoded_args = self._decode_create_instruction(
ix_data, create_ix, account_keys
)
creator = Pubkey.from_string(decoded_args["creator"])
creator_vault = self._find_creator_vault(creator)
return TokenInfo(
name=decoded_args["name"],
symbol=decoded_args["symbol"],
uri=decoded_args["uri"],
mint=Pubkey.from_string(decoded_args["mint"]),
bonding_curve=Pubkey.from_string(decoded_args["bondingCurve"]),
associated_bonding_curve=Pubkey.from_string(
decoded_args["associatedBondingCurve"]
),
user=Pubkey.from_string(decoded_args["user"]),
creator=creator,
creator_vault=creator_vault,
)
except Exception as e:
logger.error(f"Error processing transaction: {e!s}")
return None
def _decode_create_instruction(
self, ix_data: bytes, ix_def: dict[str, Any], accounts: list[Pubkey]
) -> dict[str, Any]:
"""Decode create instruction data.
Args:
ix_data: Instruction data bytes
ix_def: Instruction definition from IDL
accounts: List of account pubkeys
Returns:
Decoded instruction arguments
"""
args = {}
offset = 8 # Skip 8-byte discriminator
for arg in ix_def["args"]:
if arg["type"] == "string":
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
elif arg["type"] == "pubkey":
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
else:
logger.warning(f"Unsupported type: {arg['type']}")
value = None
args[arg["name"]] = value
args["mint"] = str(accounts[0])
args["bondingCurve"] = str(accounts[2])
args["associatedBondingCurve"] = str(accounts[3])
args["user"] = str(accounts[7])
return args
def _find_creator_vault(self, creator: Pubkey) -> Pubkey:
"""
Find the creator vault for a creator.
Args:
creator: Creator address
Returns:
Creator vault address
"""
derived_address, _ = Pubkey.find_program_address(
[b"creator-vault", bytes(creator)],
PumpAddresses.PROGRAM,
)
return derived_address
-125
View File
@@ -1,125 +0,0 @@
"""
Event processing for pump.fun tokens using Geyser data.
"""
import struct
from typing import Final
import base58
from solders.pubkey import Pubkey
from core.pubkeys import PumpAddresses
from trading.base import TokenInfo
from utils.logger import get_logger
logger = get_logger(__name__)
class GeyserEventProcessor:
"""Processes token creation events from Geyser stream."""
CREATE_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 8576854823835016728)
def __init__(self, pump_program: Pubkey):
"""Initialize event processor.
Args:
pump_program: Pump.fun program address
"""
self.pump_program = pump_program
def process_transaction_data(
self, instruction_data: bytes, accounts: list, keys: list
) -> TokenInfo | None:
"""Process transaction data and extract token creation info.
Args:
instruction_data: Raw instruction data
accounts: List of account indices
keys: List of account public keys
Returns:
TokenInfo if token creation found, None otherwise
"""
if not instruction_data.startswith(self.CREATE_DISCRIMINATOR):
return None
try:
# Skip past the 8-byte discriminator
offset = 8
# Helper to read strings (prefixed with length)
def read_string():
nonlocal offset
# Get string length (4-byte uint)
length = struct.unpack_from("<I", instruction_data, offset)[0]
offset += 4
# Extract and decode the string
value = instruction_data[offset : offset + length].decode("utf-8")
offset += length
return value
def read_pubkey():
nonlocal offset
value = base58.b58encode(instruction_data[offset : offset + 32]).decode(
"utf-8"
)
offset += 32
return Pubkey.from_string(value)
# Helper to get account key
def get_account_key(index):
if index >= len(accounts):
return None
account_index = accounts[index]
if account_index >= len(keys):
return None
return Pubkey.from_bytes(keys[account_index])
name = read_string()
symbol = read_string()
uri = read_string()
creator = read_pubkey()
mint = get_account_key(0)
bonding_curve = get_account_key(2)
associated_bonding_curve = get_account_key(3)
user = get_account_key(7)
creator_vault = self._find_creator_vault(creator)
if not all([mint, bonding_curve, associated_bonding_curve, user]):
logger.warning("Missing required account keys in token creation")
return None
return TokenInfo(
name=name,
symbol=symbol,
uri=uri,
mint=mint,
bonding_curve=bonding_curve,
associated_bonding_curve=associated_bonding_curve,
user=user,
creator=creator,
creator_vault=creator_vault,
)
except Exception as e:
logger.error(f"Failed to process transaction data: {e}")
return None
def _find_creator_vault(self, creator: Pubkey) -> Pubkey:
"""
Find the creator vault for a creator.
Args:
creator: Creator address
Returns:
Creator vault address
"""
derived_address, _ = Pubkey.find_program_address(
[b"creator-vault", bytes(creator)],
PumpAddresses.PROGRAM,
)
return derived_address
+137
View File
@@ -0,0 +1,137 @@
"""
Factory for creating platform-aware token listeners.
"""
from interfaces.core import Platform
from monitoring.base_listener import BaseTokenListener
from utils.logger import get_logger
logger = get_logger(__name__)
class ListenerFactory:
"""Factory for creating appropriate token listeners based on configuration."""
@staticmethod
def create_listener(
listener_type: str,
wss_endpoint: str | None = None,
geyser_endpoint: str | None = None,
geyser_api_token: str | None = None,
geyser_auth_type: str = "x-token",
pumpportal_url: str = "wss://pumpportal.fun/api/data",
platforms: list[Platform] | None = None,
**kwargs
) -> BaseTokenListener:
"""Create a token listener based on the specified type.
Args:
listener_type: Type of listener ('logs', 'blocks', 'geyser', or 'pumpportal')
wss_endpoint: WebSocket endpoint URL (for logs/blocks listeners)
geyser_endpoint: Geyser gRPC endpoint URL (for geyser listener)
geyser_api_token: Geyser API token (for geyser listener)
geyser_auth_type: Geyser authentication type
pumpportal_url: PumpPortal WebSocket URL (for pumpportal listener)
platforms: List of platforms to monitor (if None, monitor all)
**kwargs: Additional arguments
Returns:
Configured token listener
Raises:
ValueError: If listener type is invalid or required parameters are missing
"""
listener_type = listener_type.lower()
if listener_type == "geyser":
if not geyser_endpoint or not geyser_api_token:
raise ValueError(
"Geyser endpoint and API token are required for geyser listener"
)
from monitoring.universal_geyser_listener import UniversalGeyserListener
listener = UniversalGeyserListener(
geyser_endpoint=geyser_endpoint,
geyser_api_token=geyser_api_token,
geyser_auth_type=geyser_auth_type,
platforms=platforms,
)
logger.info("Created Universal Geyser listener for token monitoring")
return listener
elif listener_type == "logs":
if not wss_endpoint:
raise ValueError("WebSocket endpoint is required for logs listener")
from monitoring.universal_logs_listener import UniversalLogsListener
listener = UniversalLogsListener(
wss_endpoint=wss_endpoint,
platforms=platforms,
)
logger.info("Created Universal Logs listener for token monitoring")
return listener
elif listener_type == "blocks":
if not wss_endpoint:
raise ValueError("WebSocket endpoint is required for blocks listener")
from monitoring.universal_block_listener import UniversalBlockListener
listener = UniversalBlockListener(
wss_endpoint=wss_endpoint,
platforms=platforms,
)
logger.info("Created Universal Block listener for token monitoring")
return listener
elif listener_type == "pumpportal":
# PumpPortal is pump.fun specific, so filter platforms
pumpfun_platforms = [Platform.PUMP_FUN]
if platforms:
pumpfun_platforms = [p for p in platforms if p == Platform.PUMP_FUN]
if not pumpfun_platforms:
raise ValueError("PumpPortal listener only supports pump.fun platform")
from monitoring.pumpportal_listener import PumpPortalListener
listener = PumpPortalListener(
pump_program=None, # Will be determined from platform
pumpportal_url=pumpportal_url,
)
logger.info("Created PumpPortal listener for token monitoring")
return listener
else:
raise ValueError(
f"Invalid listener type '{listener_type}'. "
f"Must be one of: 'logs', 'blocks', 'geyser', 'pumpportal'"
)
@staticmethod
def get_supported_listener_types() -> list[str]:
"""Get list of supported listener types.
Returns:
List of supported listener type strings
"""
return ["logs", "blocks", "geyser", "pumpportal"]
@staticmethod
def get_platform_compatible_listeners(platform: Platform) -> list[str]:
"""Get list of listener types compatible with a specific platform.
Args:
platform: Platform to check compatibility for
Returns:
List of compatible listener types
"""
if platform == Platform.PUMP_FUN:
return ["logs", "blocks", "geyser", "pumpportal"]
elif platform == Platform.LETS_BONK:
return ["logs", "blocks", "geyser"] # PumpPortal is pump.fun only
else:
return ["logs", "blocks", "geyser"] # Default universal listeners
-174
View File
@@ -1,174 +0,0 @@
"""
Event processing for pump.fun tokens using logsSubscribe data.
"""
import base64
import struct
from typing import Final
import base58
from solders.pubkey import Pubkey
from core.pubkeys import PumpAddresses, SystemAddresses
from trading.base import TokenInfo
from utils.logger import get_logger
logger = get_logger(__name__)
class LogsEventProcessor:
"""Processes events from pump.fun program logs."""
# Discriminator for create instruction to avoid non-create transactions
CREATE_DISCRIMINATOR: Final[int] = 8530921459188068891
def __init__(self, pump_program: Pubkey):
"""Initialize event processor.
Args:
pump_program: Pump.fun program address
"""
self.pump_program = pump_program
def process_program_logs(self, logs: list[str], signature: str) -> TokenInfo | None:
"""Process program logs and extract token info.
Args:
logs: List of log strings from the notification
signature: Transaction signature
Returns:
TokenInfo if a token creation is found, None otherwise
"""
# Check if this is a token creation
if not any("Program log: Instruction: Create" in log for log in logs):
return None
# Skip swaps as the first condition may pass them
if any("Program log: Instruction: CreateTokenAccount" in log for log in logs):
return None
# Find and process program data
for log in logs:
if "Program data:" in log:
try:
encoded_data = log.split(": ")[1]
decoded_data = base64.b64decode(encoded_data)
parsed_data = self._parse_create_instruction(decoded_data)
if parsed_data and "name" in parsed_data:
mint = Pubkey.from_string(parsed_data["mint"])
bonding_curve = Pubkey.from_string(parsed_data["bondingCurve"])
associated_curve = self._find_associated_bonding_curve(
mint, bonding_curve
)
creator = Pubkey.from_string(parsed_data["creator"])
creator_vault = self._find_creator_vault(creator)
return TokenInfo(
name=parsed_data["name"],
symbol=parsed_data["symbol"],
uri=parsed_data["uri"],
mint=mint,
bonding_curve=bonding_curve,
associated_bonding_curve=associated_curve,
user=Pubkey.from_string(parsed_data["user"]),
creator=creator,
creator_vault=creator_vault,
)
except Exception as e:
logger.error(f"Failed to process log data: {e}")
return None
def _parse_create_instruction(self, data: bytes) -> dict | None:
"""Parse the create instruction data.
Args:
data: Raw instruction data
Returns:
Dictionary of parsed data or None if parsing fails
"""
if len(data) < 8:
return None
# Check for the correct instruction discriminator
discriminator = struct.unpack("<Q", data[:8])[0]
if discriminator != self.CREATE_DISCRIMINATOR:
logger.info(
f"Skipping non-Create instruction with discriminator: {discriminator}"
)
return None
offset = 8
parsed_data = {}
# Parse fields based on CreateEvent structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
return parsed_data
except Exception as e:
logger.error(f"Failed to parse create instruction: {e}")
return None
def _find_associated_bonding_curve(
self, mint: Pubkey, bonding_curve: Pubkey
) -> Pubkey:
"""
Find the associated bonding curve for a given mint and bonding curve.
This uses the standard ATA derivation.
Args:
mint: Token mint address
bonding_curve: Bonding curve address
Returns:
Associated bonding curve address
"""
derived_address, _ = Pubkey.find_program_address(
[
bytes(bonding_curve),
bytes(SystemAddresses.TOKEN_PROGRAM),
bytes(mint),
],
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
)
return derived_address
def _find_creator_vault(self, creator: Pubkey) -> Pubkey:
"""
Find the creator vault for a creator.
Args:
creator: Creator address
Returns:
Creator vault address
"""
derived_address, _ = Pubkey.find_program_address(
[b"creator-vault", bytes(creator)],
PumpAddresses.PROGRAM,
)
return derived_address
@@ -1,5 +1,5 @@
"""
WebSocket monitoring for pump.fun tokens.
Universal block listener that works with any platform through the interface system.
"""
import asyncio
@@ -7,30 +7,61 @@ import json
from collections.abc import Awaitable, Callable
import websockets
from solders.pubkey import Pubkey
from interfaces.core import Platform, TokenInfo
from monitoring.base_listener import BaseTokenListener
from monitoring.block_event_processor import PumpEventProcessor
from trading.base import TokenInfo
from platforms import get_platform_implementations
from utils.logger import get_logger
logger = get_logger(__name__)
class BlockListener(BaseTokenListener):
"""WebSocket listener for pump.fun token creation events using blockSubscribe."""
class UniversalBlockListener(BaseTokenListener):
"""Universal block listener that works with any platform."""
def __init__(self, wss_endpoint: str, pump_program: Pubkey):
"""Initialize token listener.
def __init__(
self,
wss_endpoint: str,
platforms: list[Platform] | None = None,
):
"""Initialize universal block listener.
Args:
wss_endpoint: WebSocket endpoint URL
pump_program: Pump.fun program address
platforms: List of platforms to monitor (if None, monitor all supported platforms)
"""
super().__init__()
self.wss_endpoint = wss_endpoint
self.pump_program = pump_program
self.event_processor = PumpEventProcessor(pump_program)
self.ping_interval = 20 # seconds
# Import platform factory and get supported platforms
from platforms import platform_factory
if platforms is None:
# Monitor all supported platforms
self.platforms = platform_factory.get_supported_platforms()
else:
self.platforms = platforms
# Get event parsers for all platforms
self.platform_parsers = {}
self.platform_program_ids = []
for platform in self.platforms:
try:
# We'll need a dummy client for getting the parser
from core.client import SolanaClient
dummy_client = SolanaClient("http://localhost") # Won't be used for parsing
implementations = get_platform_implementations(platform, dummy_client)
parser = implementations.event_parser
self.platform_parsers[platform] = parser
self.platform_program_ids.append(str(parser.get_program_id()))
logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}")
except Exception as e:
logger.warning(f"Could not register platform {platform.value}: {e}")
async def listen_for_tokens(
self,
@@ -38,17 +69,21 @@ class BlockListener(BaseTokenListener):
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
"""Listen for new token creations.
"""Listen for new token creations using blockSubscribe.
Args:
token_callback: Callback function for new tokens
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
if not self.platform_parsers:
logger.error("No platform parsers available. Cannot listen for tokens.")
return
while True:
try:
async with websockets.connect(self.wss_endpoint) as websocket:
await self._subscribe_to_program(websocket)
await self._subscribe_to_programs(websocket)
ping_task = asyncio.create_task(self._ping_loop(websocket))
try:
@@ -58,9 +93,10 @@ class BlockListener(BaseTokenListener):
continue
logger.info(
f"New token detected: {token_info.name} ({token_info.symbol})"
f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}"
)
# Apply filters
if match_string and not (
match_string.lower() in token_info.name.lower()
or match_string.lower() in token_info.symbol.lower()
@@ -70,10 +106,7 @@ class BlockListener(BaseTokenListener):
)
continue
if (
creator_address
and str(token_info.user) != creator_address
):
if creator_address and str(token_info.user) != creator_address:
logger.info(
f"Token not created by {creator_address}. Skipping..."
)
@@ -90,32 +123,35 @@ class BlockListener(BaseTokenListener):
logger.info("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
async def _subscribe_to_program(self, websocket) -> None:
"""Subscribe to blocks mentioning the pump.fun program.
async def _subscribe_to_programs(self, websocket) -> None:
"""Subscribe to blocks mentioning any of the monitored program IDs.
Args:
websocket: Active WebSocket connection
"""
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": str(self.pump_program)},
{
"commitment": "confirmed",
"encoding": "base64", # base64 is faster than other encoding options
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0,
},
],
}
)
# For block subscriptions, we can use mentionsAccountOrProgram to monitor multiple programs
# We'll create separate subscriptions for each program to be more specific
for i, program_id in enumerate(self.platform_program_ids):
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": i + 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": program_id},
{
"commitment": "confirmed",
"encoding": "base64",
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0,
},
],
}
)
await websocket.send(subscription_message)
logger.info(f"Subscribed to blocks mentioning program: {self.pump_program}")
await websocket.send(subscription_message)
logger.info(f"Subscribed to blocks mentioning program: {program_id}")
async def _ping_loop(self, websocket) -> None:
"""Keep connection alive with pings.
@@ -140,7 +176,7 @@ class BlockListener(BaseTokenListener):
logger.error(f"Ping error: {e!s}")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
"""Wait for token creation event.
"""Wait for token creation event from any platform.
Args:
websocket: Active WebSocket connection
@@ -166,15 +202,21 @@ class BlockListener(BaseTokenListener):
if "transactions" not in block:
return None
# Try each platform's event parser on each transaction
for tx in block["transactions"]:
if not isinstance(tx, dict) or "transaction" not in tx:
continue
token_info = self.event_processor.process_transaction(
tx["transaction"][0]
)
if token_info:
return token_info
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:
logger.debug("No data received for 30 seconds")
@@ -184,4 +226,4 @@ class BlockListener(BaseTokenListener):
except Exception as e:
logger.error(f"Error processing WebSocket message: {e!s}")
return None
return None
@@ -1,5 +1,5 @@
"""
Geyser monitoring for pump.fun tokens.
Universal Geyser listener that works with any platform through the interface system.
"""
import asyncio
@@ -9,34 +9,36 @@ import grpc
from solders.pubkey import Pubkey
from geyser.generated import geyser_pb2, geyser_pb2_grpc
from interfaces.core import Platform, TokenInfo
from monitoring.base_listener import BaseTokenListener
from monitoring.geyser_event_processor import GeyserEventProcessor
from trading.base import TokenInfo
from platforms import get_platform_implementations
from utils.logger import get_logger
logger = get_logger(__name__)
class GeyserListener(BaseTokenListener):
"""Geyser listener for pump.fun token creation events."""
class UniversalGeyserListener(BaseTokenListener):
"""Universal Geyser listener that works with any platform."""
def __init__(
self,
geyser_endpoint: str,
geyser_api_token: str,
geyser_auth_type: str,
pump_program: Pubkey,
platforms: list[Platform] | None = None,
):
"""Initialize token listener.
"""Initialize universal Geyser listener.
Args:
geyser_endpoint: Geyser gRPC endpoint URL
geyser_api_token: API token for authentication
geyser_auth_type: authentication type ('x-token' or 'basic')
pump_program: Pump.fun program address
platforms: List of platforms to monitor (if None, monitor all supported platforms)
"""
super().__init__()
self.geyser_endpoint = geyser_endpoint
self.geyser_api_token = geyser_api_token
valid_auth_types = {"x-token", "basic"}
self.auth_type: str = (geyser_auth_type or "x-token").lower()
if self.auth_type not in valid_auth_types:
@@ -44,8 +46,35 @@ class GeyserListener(BaseTokenListener):
f"Unsupported auth_type={self.auth_type!r}. "
f"Expected one of {valid_auth_types}"
)
self.pump_program = pump_program
self.event_processor = GeyserEventProcessor(pump_program)
# Import platform factory and get supported platforms
from platforms import platform_factory
if platforms is None:
# Monitor all supported platforms
self.platforms = platform_factory.get_supported_platforms()
else:
self.platforms = platforms
# Get event parsers for all platforms
self.platform_parsers = {}
self.platform_program_ids = set()
for platform in self.platforms:
try:
# We'll need a dummy client for getting the parser - this is a design issue we should fix
from core.client import SolanaClient
dummy_client = SolanaClient("http://localhost") # Won't be used for parsing
implementations = get_platform_implementations(platform, dummy_client)
parser = implementations.event_parser
self.platform_parsers[platform] = parser
self.platform_program_ids.add(parser.get_program_id())
logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}")
except Exception as e:
logger.warning(f"Could not register platform {platform.value}: {e}")
async def _create_geyser_connection(self):
"""Establish a secure connection to the Geyser endpoint."""
@@ -66,12 +95,15 @@ class GeyserListener(BaseTokenListener):
return geyser_pb2_grpc.GeyserStub(channel), channel
def _create_subscription_request(self):
"""Create a subscription request for Pump.fun transactions."""
"""Create a subscription request for all monitored platforms."""
request = geyser_pb2.SubscribeRequest()
request.transactions["pump_filter"].account_include.append(
str(self.pump_program)
)
request.transactions["pump_filter"].failed = False
# Add all platform program IDs to the filter
for program_id in self.platform_program_ids:
filter_name = f"platform_filter_{program_id}"
request.transactions[filter_name].account_include.append(str(program_id))
request.transactions[filter_name].failed = False
request.commitment = geyser_pb2.CommitmentLevel.PROCESSED
return request
@@ -88,15 +120,18 @@ class GeyserListener(BaseTokenListener):
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
if not self.platform_parsers:
logger.error("No platform parsers available. Cannot listen for tokens.")
return
while True:
try:
stub, channel = await self._create_geyser_connection()
request = self._create_subscription_request()
logger.info(f"Connected to Geyser endpoint: {self.geyser_endpoint}")
logger.info(
f"Monitoring for transactions involving program: {self.pump_program}"
)
logger.info(f"Monitoring platforms: {[p.value for p in self.platforms]}")
logger.info(f"Monitoring program IDs: {[str(pid) for pid in self.platform_program_ids]}")
try:
async for update in stub.Subscribe(iter([request])):
@@ -105,9 +140,10 @@ class GeyserListener(BaseTokenListener):
continue
logger.info(
f"New token detected: {token_info.name} ({token_info.symbol})"
f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}"
)
# Apply filters
if match_string and not (
match_string.lower() in token_info.name.lower()
or match_string.lower() in token_info.symbol.lower()
@@ -156,24 +192,25 @@ class GeyserListener(BaseTokenListener):
return None
for ix in msg.instructions:
# Skip non-Pump.fun program instructions
# Check which platform this instruction belongs to
program_idx = ix.program_id_index
if program_idx >= len(msg.account_keys):
continue
program_id = msg.account_keys[program_idx]
if bytes(program_id) != bytes(self.pump_program):
continue
# Process instruction data
token_info = self.event_processor.process_transaction_data(
ix.data, ix.accounts, msg.account_keys
)
if token_info:
return token_info
program_id = Pubkey.from_bytes(msg.account_keys[program_idx])
# Find the matching platform parser
for platform, parser in self.platform_parsers.items():
if program_id == parser.get_program_id():
# Use the platform's event parser
token_info = parser.parse_token_creation_from_instruction(
ix.data, ix.accounts, msg.account_keys
)
if token_info:
return token_info
return None
except Exception as e:
logger.error(f"Error processing Geyser update: {e}")
return None
return None
@@ -1,5 +1,5 @@
"""
WebSocket monitoring for pump.fun tokens using logsSubscribe.
Universal logs listener that works with any platform through the interface system.
"""
import asyncio
@@ -7,30 +7,61 @@ import json
from collections.abc import Awaitable, Callable
import websockets
from solders.pubkey import Pubkey
from interfaces.core import Platform, TokenInfo
from monitoring.base_listener import BaseTokenListener
from monitoring.logs_event_processor import LogsEventProcessor
from trading.base import TokenInfo
from platforms import get_platform_implementations
from utils.logger import get_logger
logger = get_logger(__name__)
class LogsListener(BaseTokenListener):
"""WebSocket listener for pump.fun token creation events using logsSubscribe."""
class UniversalLogsListener(BaseTokenListener):
"""Universal logs listener that works with any platform."""
def __init__(self, wss_endpoint: str, pump_program: Pubkey):
"""Initialize token listener.
def __init__(
self,
wss_endpoint: str,
platforms: list[Platform] | None = None,
):
"""Initialize universal logs listener.
Args:
wss_endpoint: WebSocket endpoint URL
pump_program: Pump.fun program address
platforms: List of platforms to monitor (if None, monitor all supported platforms)
"""
super().__init__()
self.wss_endpoint = wss_endpoint
self.pump_program = pump_program
self.event_processor = LogsEventProcessor(pump_program)
self.ping_interval = 20 # seconds
# Import platform factory and get supported platforms
from platforms import platform_factory
if platforms is None:
# Monitor all supported platforms
self.platforms = platform_factory.get_supported_platforms()
else:
self.platforms = platforms
# Get event parsers for all platforms
self.platform_parsers = {}
self.platform_program_ids = []
for platform in self.platforms:
try:
# We'll need a dummy client for getting the parser
from core.client import SolanaClient
dummy_client = SolanaClient("http://localhost") # Won't be used for parsing
implementations = get_platform_implementations(platform, dummy_client)
parser = implementations.event_parser
self.platform_parsers[platform] = parser
self.platform_program_ids.append(str(parser.get_program_id()))
logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}")
except Exception as e:
logger.warning(f"Could not register platform {platform.value}: {e}")
async def listen_for_tokens(
self,
@@ -45,6 +76,10 @@ class LogsListener(BaseTokenListener):
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
if not self.platform_parsers:
logger.error("No platform parsers available. Cannot listen for tokens.")
return
while True:
try:
async with websockets.connect(self.wss_endpoint) as websocket:
@@ -58,9 +93,10 @@ class LogsListener(BaseTokenListener):
continue
logger.info(
f"New token detected: {token_info.name} ({token_info.symbol})"
f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}"
)
# Apply filters
if match_string and not (
match_string.lower() in token_info.name.lower()
or match_string.lower() in token_info.symbol.lower()
@@ -70,10 +106,7 @@ class LogsListener(BaseTokenListener):
)
continue
if (
creator_address
and str(token_info.user) != creator_address
):
if creator_address and str(token_info.user) != creator_address:
logger.info(
f"Token not created by {creator_address}. Skipping..."
)
@@ -86,38 +119,40 @@ class LogsListener(BaseTokenListener):
ping_task.cancel()
except Exception as e:
logger.error(f"WebSocket connection error: {str(e)}")
logger.error(f"WebSocket connection error: {e!s}")
logger.info("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
async def _subscribe_to_logs(self, websocket) -> None:
"""Subscribe to logs mentioning the pump.fun program.
"""Subscribe to logs mentioning any of the monitored program IDs.
Args:
websocket: Active WebSocket connection
"""
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "logsSubscribe",
"params": [
{"mentions": [str(self.pump_program)]},
{"commitment": "processed"},
],
}
)
# Subscribe to logs for all monitored platforms
for program_id in self.platform_program_ids:
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": len(self.platform_program_ids), # Use different IDs
"method": "logsSubscribe",
"params": [
{"mentions": [program_id]},
{"commitment": "processed"},
],
}
)
await websocket.send(subscription_message)
logger.info(f"Subscribed to logs mentioning program: {self.pump_program}")
await websocket.send(subscription_message)
logger.info(f"Subscribed to logs mentioning program: {program_id}")
# Wait for subscription confirmation
response = await websocket.recv()
response_data = json.loads(response)
if "result" in response_data:
logger.info(f"Subscription confirmed with ID: {response_data['result']}")
else:
logger.warning(f"Unexpected subscription response: {response}")
# Wait for subscription confirmation
response = await websocket.recv()
response_data = json.loads(response)
if "result" in response_data:
logger.info(f"Subscription confirmed with ID: {response_data['result']}")
else:
logger.warning(f"Unexpected subscription response: {response}")
async def _ping_loop(self, websocket) -> None:
"""Keep connection alive with pings.
@@ -131,7 +166,7 @@ class LogsListener(BaseTokenListener):
try:
pong_waiter = await websocket.ping()
await asyncio.wait_for(pong_waiter, timeout=10)
except asyncio.TimeoutError:
except TimeoutError:
logger.warning("Ping timeout - server not responding")
# Force reconnection
await websocket.close()
@@ -139,9 +174,17 @@ class LogsListener(BaseTokenListener):
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"Ping error: {str(e)}")
logger.error(f"Ping error: {e!s}")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
"""Wait for token creation events from any platform.
Args:
websocket: Active WebSocket connection
Returns:
TokenInfo if a token creation is found, None otherwise
"""
try:
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
@@ -153,15 +196,20 @@ class LogsListener(BaseTokenListener):
logs = log_data.get("logs", [])
signature = log_data.get("signature", "unknown")
# Use the processor to extract token info
return self.event_processor.process_program_logs(logs, signature)
# Try each platform's event parser
for platform, parser in self.platform_parsers.items():
token_info = parser.parse_token_creation_from_logs(logs, signature)
if token_info:
return token_info
except asyncio.TimeoutError:
return None
except TimeoutError:
logger.debug("No data received for 30 seconds")
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed")
raise
except Exception as e:
logger.error(f"Error processing WebSocket message: {str(e)}")
logger.error(f"Error processing WebSocket message: {e!s}")
return None
return None