mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 23:37:45 +00:00
feat(examples): listen and parse wallet txs
This commit is contained in:
@@ -0,0 +1,318 @@
|
|||||||
|
"""
|
||||||
|
Listens for pump.fun bonding curve buy/sell transactions involving a specific wallet.
|
||||||
|
Filters transactions to show only buy/sell operations on pump.fun bonding curves,
|
||||||
|
excluding pump AMM (migrated) transactions..
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import base58
|
||||||
|
import websockets
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
||||||
|
WALLET_TO_TRACK = "..." # Change this to your target wallet
|
||||||
|
|
||||||
|
# Pump.fun program constants
|
||||||
|
PUMP_BONDING_CURVE_PROGRAM_ID = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
|
||||||
|
PUMP_AMM_PROGRAM_ID = "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"
|
||||||
|
|
||||||
|
# TradeEvent discriminator and parsing constants
|
||||||
|
TRADE_EVENT_DISCRIMINATOR = bytes([189, 219, 127, 211, 78, 230, 97, 238])
|
||||||
|
EVENT_DISCRIMINATOR_SIZE = 8
|
||||||
|
|
||||||
|
# Display settings
|
||||||
|
MAX_LOGS_TO_SHOW = 5
|
||||||
|
RECONNECT_DELAY = 5
|
||||||
|
MIN_LOG_PARTS_FOR_PROGRAM = 2
|
||||||
|
PING_INTERVAL = 20
|
||||||
|
|
||||||
|
if not WSS_ENDPOINT:
|
||||||
|
print("Error: SOLANA_NODE_WSS_ENDPOINT environment variable not set")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
async def subscribe_to_wallet_logs(websocket):
|
||||||
|
"""Subscribe to logs mentioning our target wallet."""
|
||||||
|
subscription_message = json.dumps(
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "logsSubscribe",
|
||||||
|
"params": [{"mentions": [WALLET_TO_TRACK]}, {"commitment": "processed"}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await websocket.send(subscription_message)
|
||||||
|
print(f"Subscribed to logs mentioning wallet: {WALLET_TO_TRACK}")
|
||||||
|
|
||||||
|
# Wait for subscription confirmation
|
||||||
|
response = await websocket.recv()
|
||||||
|
response_data = json.loads(response)
|
||||||
|
if "result" in response_data:
|
||||||
|
print(f"Subscription confirmed with ID: {response_data['result']}")
|
||||||
|
else:
|
||||||
|
print(f"Unexpected subscription response: {response}")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
|
||||||
|
async def keep_connection_alive(websocket):
|
||||||
|
"""Send ping messages to keep the WebSocket connection alive."""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(PING_INTERVAL)
|
||||||
|
try:
|
||||||
|
pong_waiter = await websocket.ping()
|
||||||
|
await asyncio.wait_for(pong_waiter, timeout=10)
|
||||||
|
except TimeoutError:
|
||||||
|
print("Ping timeout - server not responding")
|
||||||
|
await websocket.close()
|
||||||
|
return
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except (websockets.exceptions.WebSocketException, ConnectionError) as e:
|
||||||
|
print(f"Ping error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def is_pump_bonding_curve_buysell(logs):
|
||||||
|
"""Check if transaction is a pump.fun bonding curve buy/sell transaction."""
|
||||||
|
# Must mention pump.fun bonding curve program
|
||||||
|
if not any(PUMP_BONDING_CURVE_PROGRAM_ID in log for log in logs):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Must NOT mention pump AMM program (exclude migrated tokens)
|
||||||
|
if any(PUMP_AMM_PROGRAM_ID in log for log in logs):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Must have TradeEvent data (more reliable than instruction logs)
|
||||||
|
if parse_trade_event(logs) is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def parse_trade_event(logs):
|
||||||
|
"""Parse TradeEvent data from transaction logs."""
|
||||||
|
for log in logs:
|
||||||
|
if "Program data:" in log:
|
||||||
|
try:
|
||||||
|
encoded_data = log.split("Program data: ")[1].strip()
|
||||||
|
decoded_data = base64.b64decode(encoded_data)
|
||||||
|
|
||||||
|
if len(decoded_data) >= EVENT_DISCRIMINATOR_SIZE:
|
||||||
|
discriminator = decoded_data[:EVENT_DISCRIMINATOR_SIZE]
|
||||||
|
if discriminator == TRADE_EVENT_DISCRIMINATOR:
|
||||||
|
return decode_trade_event(
|
||||||
|
decoded_data[EVENT_DISCRIMINATOR_SIZE:]
|
||||||
|
)
|
||||||
|
except (ValueError, binascii.Error):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def decode_trade_event(data):
|
||||||
|
"""Decode TradeEvent structure from raw bytes."""
|
||||||
|
if len(data) < 32 + 8 + 8 + 1 + 32: # minimum size check
|
||||||
|
return None
|
||||||
|
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
# Parse fields according to TradeEvent structure
|
||||||
|
mint = data[offset : offset + 32]
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
sol_amount = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
|
||||||
|
token_amount = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
|
||||||
|
is_buy = bool(data[offset])
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
user = data[offset : offset + 32]
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
timestamp = struct.unpack("<q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
|
||||||
|
virtual_sol_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
|
||||||
|
virtual_token_reserves = struct.unpack("<Q", data[offset : offset + 8])[0]
|
||||||
|
offset += 8
|
||||||
|
|
||||||
|
return {
|
||||||
|
"mint": base58.b58encode(mint).decode(),
|
||||||
|
"sol_amount": sol_amount,
|
||||||
|
"token_amount": token_amount,
|
||||||
|
"is_buy": is_buy,
|
||||||
|
"user": base58.b58encode(user).decode(),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"virtual_sol_reserves": virtual_sol_reserves,
|
||||||
|
"virtual_token_reserves": virtual_token_reserves,
|
||||||
|
"price_per_token": (sol_amount * 1_000_000) / token_amount
|
||||||
|
if token_amount > 0
|
||||||
|
else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def display_transaction_info(signature, logs):
|
||||||
|
"""Display formatted transaction information."""
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
print(f"[{timestamp}] Pump.fun Bonding Curve Transaction:")
|
||||||
|
print(f" Signature: {signature}")
|
||||||
|
print(f" Wallet: {WALLET_TO_TRACK}")
|
||||||
|
|
||||||
|
# Parse trade event data
|
||||||
|
trade_data = parse_trade_event(logs)
|
||||||
|
if trade_data:
|
||||||
|
print(f" Type: {'BUY' if trade_data['is_buy'] else 'SELL'}")
|
||||||
|
print(f" Token: {trade_data['mint']}")
|
||||||
|
print(f" SOL Amount: {trade_data['sol_amount'] / 1_000_000_000:.6f} SOL")
|
||||||
|
print(f" Token Amount: {trade_data['token_amount']:,}")
|
||||||
|
print(
|
||||||
|
f" Price per Token: {trade_data['price_per_token'] / 1_000_000_000:.9f} SOL"
|
||||||
|
)
|
||||||
|
print(f" Trader: {trade_data['user']}")
|
||||||
|
|
||||||
|
# Extract and display program info
|
||||||
|
display_program_info(logs)
|
||||||
|
|
||||||
|
# Show pump.fun related logs
|
||||||
|
display_pump_logs(logs)
|
||||||
|
|
||||||
|
print(" " + "-" * 76)
|
||||||
|
|
||||||
|
|
||||||
|
def display_program_info(logs):
|
||||||
|
"""Extract and display program information from logs."""
|
||||||
|
programs_involved = set()
|
||||||
|
instructions = []
|
||||||
|
|
||||||
|
for log in logs:
|
||||||
|
if "Program " in log and " invoke" in log:
|
||||||
|
parts = log.split()
|
||||||
|
if len(parts) >= MIN_LOG_PARTS_FOR_PROGRAM:
|
||||||
|
program_id = parts[1]
|
||||||
|
programs_involved.add(program_id)
|
||||||
|
elif "Instruction:" in log:
|
||||||
|
instruction = log.split("Instruction: ")[-1]
|
||||||
|
instructions.append(instruction)
|
||||||
|
|
||||||
|
if programs_involved:
|
||||||
|
print(f" Programs involved: {', '.join(programs_involved)}")
|
||||||
|
if instructions:
|
||||||
|
print(f" Instructions: {', '.join(instructions)}")
|
||||||
|
|
||||||
|
|
||||||
|
def display_pump_logs(logs):
|
||||||
|
"""Display relevant pump.fun logs."""
|
||||||
|
pump_logs = [
|
||||||
|
log
|
||||||
|
for log in logs
|
||||||
|
if any(
|
||||||
|
keyword in log
|
||||||
|
for keyword in [
|
||||||
|
"Program log: Instruction:",
|
||||||
|
"Program data:",
|
||||||
|
PUMP_BONDING_CURVE_PROGRAM_ID,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
if pump_logs:
|
||||||
|
print(" Pump.fun logs:")
|
||||||
|
for log in pump_logs[:MAX_LOGS_TO_SHOW]:
|
||||||
|
print(f" {log}")
|
||||||
|
if len(pump_logs) > MAX_LOGS_TO_SHOW:
|
||||||
|
print(f" ... and {len(pump_logs) - MAX_LOGS_TO_SHOW} more logs")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_transaction(log_data):
|
||||||
|
"""Handle a transaction log notification."""
|
||||||
|
signature = log_data.get("signature", "unknown")
|
||||||
|
logs = log_data.get("logs", [])
|
||||||
|
|
||||||
|
# Check if this is a pump.fun bonding curve buy/sell transaction
|
||||||
|
if not is_pump_bonding_curve_buysell(logs):
|
||||||
|
return
|
||||||
|
|
||||||
|
display_transaction_info(signature, logs)
|
||||||
|
|
||||||
|
|
||||||
|
async def process_websocket_message(websocket):
|
||||||
|
"""Process incoming WebSocket messages."""
|
||||||
|
try:
|
||||||
|
response = await asyncio.wait_for(websocket.recv(), timeout=30)
|
||||||
|
data = json.loads(response)
|
||||||
|
|
||||||
|
if "method" not in data or data["method"] != "logsNotification":
|
||||||
|
return
|
||||||
|
|
||||||
|
log_data = data["params"]["result"]["value"]
|
||||||
|
await handle_transaction(log_data)
|
||||||
|
|
||||||
|
except TimeoutError:
|
||||||
|
print("No data received for 30 seconds")
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
print("WebSocket connection closed")
|
||||||
|
raise
|
||||||
|
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
||||||
|
print(f"Error processing message: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def listen_for_transactions():
|
||||||
|
"""Main function to listen for wallet transactions."""
|
||||||
|
print(f"Starting to monitor wallet: {WALLET_TO_TRACK}")
|
||||||
|
print(f"Connecting to: {WSS_ENDPOINT}")
|
||||||
|
print("Looking for pump.fun bonding curve buy/sell transactions only...")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with websockets.connect(WSS_ENDPOINT) as websocket:
|
||||||
|
await subscribe_to_wallet_logs(websocket)
|
||||||
|
ping_task = asyncio.create_task(keep_connection_alive(websocket))
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await process_websocket_message(websocket)
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
print("WebSocket connection closed. Reconnecting...")
|
||||||
|
ping_task.cancel()
|
||||||
|
|
||||||
|
except (
|
||||||
|
websockets.exceptions.WebSocketException,
|
||||||
|
ConnectionError,
|
||||||
|
OSError,
|
||||||
|
) as e:
|
||||||
|
print(f"Connection error: {e}")
|
||||||
|
print(f"Reconnecting in {RECONNECT_DELAY} seconds...")
|
||||||
|
await asyncio.sleep(RECONNECT_DELAY)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to run the wallet transaction listener."""
|
||||||
|
try:
|
||||||
|
asyncio.run(listen_for_transactions())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nStopping wallet transaction listener...")
|
||||||
|
except (ValueError, RuntimeError) as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user