mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-06 12:07:45 +00:00
updated code structure
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Event processing for pump.fun tokens.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import struct
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
from solders.transaction import VersionedTransaction
|
||||
|
||||
from src.trading.base import TokenInfo
|
||||
from src.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", "r") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load IDL: {str(e)}")
|
||||
# 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) -> Optional[TokenInfo]:
|
||||
"""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
|
||||
)
|
||||
|
||||
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"]),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing transaction: {str(e)}")
|
||||
|
||||
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"] == "publicKey":
|
||||
value = base64.b64encode(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
|
||||
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
WebSocket monitoring for pump.fun tokens.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional
|
||||
|
||||
import websockets
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from src.monitoring.events import PumpEventProcessor
|
||||
from src.trading.base import TokenInfo
|
||||
from src.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PumpTokenListener:
|
||||
"""WebSocket listener for pump.fun token creation events."""
|
||||
|
||||
def __init__(self, wss_endpoint: str, pump_program: Pubkey):
|
||||
"""Initialize token listener.
|
||||
|
||||
Args:
|
||||
wss_endpoint: WebSocket endpoint URL
|
||||
pump_program: Pump.fun program address
|
||||
"""
|
||||
self.wss_endpoint = wss_endpoint
|
||||
self.pump_program = pump_program
|
||||
self.event_processor = PumpEventProcessor(pump_program)
|
||||
self.ping_interval = 20 # seconds
|
||||
|
||||
async def listen_for_tokens(
|
||||
self,
|
||||
token_callback: Callable[[TokenInfo], Awaitable[None]],
|
||||
match_string: str | None = None,
|
||||
creator_address: str | None = None,
|
||||
) -> None:
|
||||
"""Listen for new token creations.
|
||||
|
||||
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
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
async with websockets.connect(self.wss_endpoint) as websocket:
|
||||
await self._subscribe_to_program(websocket)
|
||||
ping_task = asyncio.create_task(self._ping_loop(websocket))
|
||||
|
||||
try:
|
||||
while True:
|
||||
token_info = await self._wait_for_token_creation(websocket)
|
||||
if not token_info:
|
||||
continue
|
||||
|
||||
logger.info(
|
||||
f"New token detected: {token_info.name} ({token_info.symbol})"
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if match_string and not (
|
||||
match_string.lower() in token_info.name.lower()
|
||||
or match_string.lower() in token_info.symbol.lower()
|
||||
):
|
||||
logger.info(
|
||||
f"Token does not match filter '{match_string}'. Skipping..."
|
||||
)
|
||||
continue
|
||||
|
||||
if (
|
||||
creator_address
|
||||
and str(token_info.user) != creator_address
|
||||
):
|
||||
logger.info(
|
||||
f"Token not created by {creator_address}. Skipping..."
|
||||
)
|
||||
continue
|
||||
|
||||
await token_callback(token_info)
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.warning("WebSocket connection closed. Reconnecting...")
|
||||
ping_task.cancel()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket connection error: {str(e)}")
|
||||
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.
|
||||
|
||||
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",
|
||||
"showRewards": False,
|
||||
"transactionDetails": "full",
|
||||
"maxSupportedTransactionVersion": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
await websocket.send(subscription_message)
|
||||
logger.info(f"Subscribed to blocks mentioning program: {self.pump_program}")
|
||||
|
||||
async def _ping_loop(self, websocket) -> None:
|
||||
"""Keep connection alive with pings.
|
||||
|
||||
Args:
|
||||
websocket: Active WebSocket connection
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(self.ping_interval)
|
||||
await websocket.ping()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Ping error: {str(e)}")
|
||||
|
||||
async def _wait_for_token_creation(self, websocket) -> Optional[TokenInfo]:
|
||||
"""Wait for token creation event.
|
||||
|
||||
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)
|
||||
|
||||
if "method" not in data or data["method"] != "blockNotification":
|
||||
return None
|
||||
|
||||
if "params" not in data or "result" not in data["params"]:
|
||||
return None
|
||||
|
||||
block_data = data["params"]["result"]
|
||||
if "value" not in block_data or "block" not in block_data["value"]:
|
||||
return None
|
||||
|
||||
block = block_data["value"]["block"]
|
||||
if "transactions" not in block:
|
||||
return None
|
||||
|
||||
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
|
||||
|
||||
except asyncio.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)}")
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user