mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 15:27:44 +00:00
feat: add geyser auth type, compare listeners for new tokens
This commit is contained in:
+6
-3
@@ -1,5 +1,8 @@
|
|||||||
SOLANA_NODE_RPC_ENDPOINT=...
|
SOLANA_NODE_RPC_ENDPOINT=...
|
||||||
SOLANA_NODE_WSS_ENDPOINT=...
|
SOLANA_NODE_WSS_ENDPOINT=...
|
||||||
GEYSER_ENDPOINT=...
|
|
||||||
GEYSER_API_TOKEN=...
|
GEYSER_ENDPOINT=
|
||||||
SOLANA_PRIVATE_KEY=...
|
GEYSER_API_TOKEN=
|
||||||
|
GEYSER_AUTH_TYPE=x-token or basic
|
||||||
|
|
||||||
|
SOLANA_PRIVATE_KEY=
|
||||||
@@ -15,6 +15,7 @@ separate_process: true
|
|||||||
geyser:
|
geyser:
|
||||||
endpoint: "${GEYSER_ENDPOINT}"
|
endpoint: "${GEYSER_ENDPOINT}"
|
||||||
api_token: "${GEYSER_API_TOKEN}"
|
api_token: "${GEYSER_API_TOKEN}"
|
||||||
|
auth_type: "x-token" # or "basic"
|
||||||
|
|
||||||
# Trading parameters
|
# Trading parameters
|
||||||
# Control trade execution: amount of SOL per trade and acceptable price deviation
|
# Control trade execution: amount of SOL per trade and acceptable price deviation
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ separate_process: true
|
|||||||
geyser:
|
geyser:
|
||||||
endpoint: "${GEYSER_ENDPOINT}"
|
endpoint: "${GEYSER_ENDPOINT}"
|
||||||
api_token: "${GEYSER_API_TOKEN}"
|
api_token: "${GEYSER_API_TOKEN}"
|
||||||
|
auth_type: "basic" # or "x-token"
|
||||||
|
|
||||||
# Trading parameters
|
# Trading parameters
|
||||||
# Control trade execution: amount of SOL per trade and acceptable price deviation
|
# Control trade execution: amount of SOL per trade and acceptable price deviation
|
||||||
|
|||||||
@@ -0,0 +1,637 @@
|
|||||||
|
"""
|
||||||
|
This script compares four methods of detecting new Pump.fun tokens:
|
||||||
|
1. Block subscription listener - listens for blocks containing Pump.fun program
|
||||||
|
2. Geyser gRPC listener - uses Geyser gRPC API to get transactions containing Pump.fun program
|
||||||
|
3. Logs subscription listener - listens for logs containing Pump.fun program
|
||||||
|
4. PumpPortal WebSocket listener - connects to PumpPortal WebSocket and listens for token events
|
||||||
|
|
||||||
|
The script tracks which method detects new tokens first and provides detailed performance statistics.
|
||||||
|
|
||||||
|
Note: multiple endpoints available. Scroll down to change providers which you want to test.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
|
||||||
|
import base58
|
||||||
|
import grpc
|
||||||
|
import websockets
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from solders.pubkey import Pubkey
|
||||||
|
from solders.transaction import VersionedTransaction
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||||
|
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
|
||||||
|
PUMPPORTAL_WS_URL = "wss://pumpportal.fun/api/data"
|
||||||
|
TEST_DURATION = 30 # seconds
|
||||||
|
|
||||||
|
GEYSER_AUTH_TYPE = "x-token" # or "basic"
|
||||||
|
|
||||||
|
|
||||||
|
class DetectionTracker:
|
||||||
|
"""Tracks and analyzes detection times for both methods across providers"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.tokens = {} # {mint: {provider: timestamp}}
|
||||||
|
self.messages = {} # {provider: count}
|
||||||
|
self.start_time = time.time()
|
||||||
|
|
||||||
|
def add_token(self, mint, name, symbol, provider, timestamp):
|
||||||
|
"""Record a token detection event"""
|
||||||
|
if mint not in self.tokens:
|
||||||
|
self.tokens[mint] = {'name': name, 'symbol': symbol, 'detections': {}}
|
||||||
|
self.tokens[mint]['detections'][provider] = timestamp
|
||||||
|
print(f"[TOKEN] mint={mint} name={name} symbol={symbol} provider={provider} time={timestamp:.3f}")
|
||||||
|
|
||||||
|
def increment_messages(self, provider):
|
||||||
|
"""Count WebSocket/gRPC messages received by listener"""
|
||||||
|
if provider not in self.messages:
|
||||||
|
self.messages[provider] = 0
|
||||||
|
self.messages[provider] += 1
|
||||||
|
|
||||||
|
def print_summary(self):
|
||||||
|
"""Print detailed summary statistics of the comparison test"""
|
||||||
|
test_duration = time.time() - self.start_time
|
||||||
|
|
||||||
|
# Count total messages
|
||||||
|
total_messages = sum(self.messages.values())
|
||||||
|
|
||||||
|
print("\n=== Test Summary ===")
|
||||||
|
print(f"Test duration: {test_duration:.2f} seconds")
|
||||||
|
print(f"Messages received: {total_messages}")
|
||||||
|
|
||||||
|
# Count unique tokens detected by each provider
|
||||||
|
provider_tokens = {}
|
||||||
|
all_providers = set()
|
||||||
|
for mint, token_data in self.tokens.items():
|
||||||
|
providers = token_data['detections'].keys()
|
||||||
|
all_providers.update(providers)
|
||||||
|
for provider in providers:
|
||||||
|
if provider not in provider_tokens:
|
||||||
|
provider_tokens[provider] = 0
|
||||||
|
provider_tokens[provider] += 1
|
||||||
|
|
||||||
|
print(f"Tokens detected: {len(self.tokens)}")
|
||||||
|
for provider, count in sorted(provider_tokens.items()):
|
||||||
|
print(f" - {provider}: {count}")
|
||||||
|
|
||||||
|
print("\n=== Provider Message Counts ===")
|
||||||
|
print("Provider | Messages")
|
||||||
|
print("-" * 40)
|
||||||
|
for provider in sorted(self.messages.keys()):
|
||||||
|
message_count = self.messages.get(provider, 0)
|
||||||
|
print(f"{provider:<22} | {message_count:<8}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=== Token Detection Provider Performance ===")
|
||||||
|
self._print_provider_performance()
|
||||||
|
|
||||||
|
# Print token details
|
||||||
|
print("\n=== Detected Tokens ===")
|
||||||
|
print("Mint | Name | Symbol | First Provider | Detected By")
|
||||||
|
print("-" * 100)
|
||||||
|
|
||||||
|
for mint, token_data in sorted(self.tokens.items(), key=lambda x: min(x[1]['detections'].values())):
|
||||||
|
name = token_data['name'][:15] # Truncate long names
|
||||||
|
symbol = token_data['symbol'][:6] # Truncate long symbols
|
||||||
|
|
||||||
|
# Find first provider
|
||||||
|
first_provider = min(token_data['detections'].items(), key=lambda x: x[1])[0]
|
||||||
|
|
||||||
|
# Get list of providers that detected this token
|
||||||
|
providers = ", ".join(sorted(token_data['detections'].keys()))
|
||||||
|
|
||||||
|
print(f"{mint} | {name:<16} | {symbol:<6} | {first_provider:<14} | {providers}")
|
||||||
|
|
||||||
|
def _print_provider_performance(self):
|
||||||
|
"""Print performance metrics for providers"""
|
||||||
|
# Count how many times each provider was first
|
||||||
|
first_count = {}
|
||||||
|
total_tokens = len(self.tokens)
|
||||||
|
|
||||||
|
for mint, token_data in self.tokens.items():
|
||||||
|
detections = token_data['detections']
|
||||||
|
if not detections:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find the fastest provider for this token
|
||||||
|
fastest_provider = min(detections.items(), key=lambda x: x[1])[0]
|
||||||
|
if fastest_provider not in first_count:
|
||||||
|
first_count[fastest_provider] = 0
|
||||||
|
first_count[fastest_provider] += 1
|
||||||
|
|
||||||
|
if not first_count:
|
||||||
|
print("No tokens detected")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Print rankings
|
||||||
|
print("Provider | First Detections | Percentage")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
for provider, count in sorted(first_count.items(), key=lambda x: x[1], reverse=True):
|
||||||
|
percentage = (count / total_tokens) * 100 if total_tokens > 0 else 0
|
||||||
|
print(f"{provider:<22} | {count:<16} | {percentage:.1f}%")
|
||||||
|
|
||||||
|
# Calculate average latency between providers
|
||||||
|
self._print_provider_latency_matrix()
|
||||||
|
|
||||||
|
def _print_provider_latency_matrix(self):
|
||||||
|
"""Print a matrix of average latency between providers"""
|
||||||
|
# Get unique providers
|
||||||
|
all_providers = set()
|
||||||
|
for token_data in self.tokens.values():
|
||||||
|
all_providers.update(token_data['detections'].keys())
|
||||||
|
|
||||||
|
if len(all_providers) <= 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
providers_list = sorted(all_providers)
|
||||||
|
|
||||||
|
print("\nAverage Latency Matrix (ms):")
|
||||||
|
# Print header
|
||||||
|
header = " |"
|
||||||
|
for provider in providers_list:
|
||||||
|
header += f" {provider[:8]:>8} |"
|
||||||
|
print(header)
|
||||||
|
print("-" * len(header))
|
||||||
|
|
||||||
|
# Calculate and print latency matrix
|
||||||
|
for provider1 in providers_list:
|
||||||
|
row = f"{provider1[:8]:>8} |"
|
||||||
|
for provider2 in providers_list:
|
||||||
|
if provider1 == provider2:
|
||||||
|
row += " — |"
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate average latency
|
||||||
|
latencies = []
|
||||||
|
for token_data in self.tokens.values():
|
||||||
|
detections = token_data['detections']
|
||||||
|
if provider1 in detections and provider2 in detections:
|
||||||
|
latency_ms = (detections[provider2] - detections[provider1]) * 1000
|
||||||
|
latencies.append(latency_ms)
|
||||||
|
|
||||||
|
if latencies:
|
||||||
|
avg_latency = sum(latencies) / len(latencies)
|
||||||
|
row += f" {avg_latency:>+7.1f} |"
|
||||||
|
else:
|
||||||
|
row += " ? |"
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
|
||||||
|
# ============ TOKEN DETECTION METHODS ============
|
||||||
|
|
||||||
|
async def fetch_existing_token_mints():
|
||||||
|
"""
|
||||||
|
Fetch existing token mints to avoid duplicate detections
|
||||||
|
"""
|
||||||
|
# You could implement this by querying a known database or API
|
||||||
|
# For simplicity, we'll return an empty set
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_create_instruction(data):
|
||||||
|
"""
|
||||||
|
Parse binary create instruction data into a structured format
|
||||||
|
"""
|
||||||
|
if len(data) < 8:
|
||||||
|
return None
|
||||||
|
|
||||||
|
offset = 8 # Skip discriminator
|
||||||
|
parsed_data = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse name (string)
|
||||||
|
length = struct.unpack("<I", data[offset:offset + 4])[0]
|
||||||
|
offset += 4
|
||||||
|
parsed_data["name"] = data[offset:offset + length].decode("utf-8")
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
# Parse symbol (string)
|
||||||
|
length = struct.unpack("<I", data[offset:offset + 4])[0]
|
||||||
|
offset += 4
|
||||||
|
parsed_data["symbol"] = data[offset:offset + length].decode("utf-8")
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
# Parse uri (string)
|
||||||
|
length = struct.unpack("<I", data[offset:offset + 4])[0]
|
||||||
|
offset += 4
|
||||||
|
parsed_data["uri"] = data[offset:offset + length].decode("utf-8")
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
# Parse mint (pubkey)
|
||||||
|
parsed_data["mint"] = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
return parsed_data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to parse create instruction: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_transaction_successful(logs):
|
||||||
|
"""Check if a transaction was successful based on log messages"""
|
||||||
|
for log in logs:
|
||||||
|
if "AnchorError thrown" in log or "Error" in log:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ============ WEBSOCKET LISTENERS ============
|
||||||
|
|
||||||
|
async def listen_block_subscription(wss_url, provider_name, tracker, known_tokens=None):
|
||||||
|
"""
|
||||||
|
Listen for new tokens via block subscription
|
||||||
|
"""
|
||||||
|
if known_tokens is None:
|
||||||
|
known_tokens = set()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(f"[INFO] Connecting block listener to {provider_name}...")
|
||||||
|
async with websockets.connect(wss_url) as websocket:
|
||||||
|
subscription_message = json.dumps({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "blockSubscribe",
|
||||||
|
"params": [
|
||||||
|
{"mentionsAccountOrProgram": str(PUMP_PROGRAM_ID)},
|
||||||
|
{
|
||||||
|
"commitment": "confirmed",
|
||||||
|
"encoding": "base64",
|
||||||
|
"showRewards": False,
|
||||||
|
"transactionDetails": "full",
|
||||||
|
"maxSupportedTransactionVersion": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
await websocket.send(subscription_message)
|
||||||
|
await websocket.recv()
|
||||||
|
print(f"[INFO] Block listener active for {provider_name}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
response = await websocket.recv()
|
||||||
|
data = json.loads(response)
|
||||||
|
tracker.increment_messages(provider_name)
|
||||||
|
|
||||||
|
if data.get("method") != "blockNotification":
|
||||||
|
continue
|
||||||
|
|
||||||
|
block_data = data["params"]["result"]
|
||||||
|
if "value" not in block_data or "block" not in block_data["value"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
block = block_data["value"]["block"]
|
||||||
|
if "transactions" not in block:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for tx in block["transactions"]:
|
||||||
|
if not isinstance(tx, dict) or "transaction" not in tx:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tx_data_b64 = tx["transaction"][0]
|
||||||
|
tx_data = base64.b64decode(tx_data_b64)
|
||||||
|
|
||||||
|
try:
|
||||||
|
transaction = VersionedTransaction.from_bytes(tx_data)
|
||||||
|
for ix in transaction.message.instructions:
|
||||||
|
if transaction.message.account_keys[ix.program_id_index] == PUMP_PROGRAM_ID:
|
||||||
|
data_bytes = bytes(ix.data)
|
||||||
|
|
||||||
|
|
||||||
|
if not data_bytes.startswith(PUMP_CREATE_PREFIX):
|
||||||
|
continue
|
||||||
|
|
||||||
|
parsed = parse_create_instruction(data_bytes)
|
||||||
|
if not parsed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(ix.accounts) > 0:
|
||||||
|
try:
|
||||||
|
mint = str(transaction.message.account_keys[ix.accounts[0]]) # First account is usually the mint
|
||||||
|
|
||||||
|
if mint in known_tokens:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ts = time.time()
|
||||||
|
tracker.add_token(mint, parsed["name"], parsed["symbol"],
|
||||||
|
f"{provider_name}_block", ts)
|
||||||
|
known_tokens.add(mint)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to process block instruction: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to process transaction: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Block listener for {provider_name}: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Connection error in block listener for {provider_name}: {e}")
|
||||||
|
print("[INFO] Reconnecting in 5 seconds...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
|
async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens=None):
|
||||||
|
"""
|
||||||
|
Listen for new tokens via logs subscription
|
||||||
|
"""
|
||||||
|
if known_tokens is None:
|
||||||
|
known_tokens = set()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(f"[INFO] Connecting logs listener to {provider_name}...")
|
||||||
|
async with websockets.connect(wss_url) as websocket:
|
||||||
|
subscription_message = json.dumps({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "logsSubscribe",
|
||||||
|
"params": [
|
||||||
|
{"mentions": [str(PUMP_PROGRAM_ID)]},
|
||||||
|
{"commitment": "processed"},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
await websocket.send(subscription_message)
|
||||||
|
await websocket.recv()
|
||||||
|
print(f"[INFO] Logs listener active for {provider_name}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
response = await websocket.recv()
|
||||||
|
data = json.loads(response)
|
||||||
|
tracker.increment_messages(provider_name)
|
||||||
|
|
||||||
|
if data.get("method") != "logsNotification":
|
||||||
|
continue
|
||||||
|
|
||||||
|
log_data = data["params"]["result"]["value"]
|
||||||
|
logs = log_data.get("logs", [])
|
||||||
|
|
||||||
|
if not any("Program log: Instruction: Create" in log for log in logs):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for log in logs:
|
||||||
|
if "Program data:" in log:
|
||||||
|
try:
|
||||||
|
encoded_data = log.split(": ")[1]
|
||||||
|
data_bytes = base64.b64decode(encoded_data)
|
||||||
|
|
||||||
|
parsed = parse_create_instruction(data_bytes)
|
||||||
|
if not parsed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
mint = parsed.get("mint")
|
||||||
|
if not mint:
|
||||||
|
continue
|
||||||
|
if mint in known_tokens:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ts = time.time()
|
||||||
|
tracker.add_token(
|
||||||
|
mint,
|
||||||
|
parsed.get("name", "Unknown"),
|
||||||
|
parsed.get("symbol", "UNK"),
|
||||||
|
f"{provider_name}_logs",
|
||||||
|
ts
|
||||||
|
)
|
||||||
|
known_tokens.add(mint)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to decode Program data: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Logs listener for {provider_name}: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Connection error in logs listener for {provider_name}: {e}")
|
||||||
|
print("[INFO] Reconnecting in 5 seconds...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
|
async def listen_geyser_grpc(endpoint, api_token, provider_name, tracker, known_tokens=None):
|
||||||
|
"""
|
||||||
|
Listen for new tokens via Geyser gRPC API
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Import the generated protobuf modules
|
||||||
|
from generated import geyser_pb2, geyser_pb2_grpc
|
||||||
|
except ImportError:
|
||||||
|
print("[ERROR] Could not import geyser_pb2 or geyser_pb2_grpc. Make sure to generate from .proto files")
|
||||||
|
return
|
||||||
|
|
||||||
|
if known_tokens is None:
|
||||||
|
known_tokens = set()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(f"[INFO] Connecting Geyser gRPC listener to {provider_name}...")
|
||||||
|
|
||||||
|
if GEYSER_AUTH_TYPE == "x-token":
|
||||||
|
auth = grpc.metadata_call_credentials(
|
||||||
|
lambda context, callback: callback((("x-token", api_token),), None)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
auth = grpc.metadata_call_credentials(
|
||||||
|
lambda context, callback: callback((("authorization", f"Basic {api_token}"),), None)
|
||||||
|
)
|
||||||
|
|
||||||
|
creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth)
|
||||||
|
channel = grpc.aio.secure_channel(endpoint, creds)
|
||||||
|
stub = geyser_pb2_grpc.GeyserStub(channel)
|
||||||
|
|
||||||
|
request = geyser_pb2.SubscribeRequest()
|
||||||
|
request.transactions["pump_filter"].account_include.append(str(PUMP_PROGRAM_ID))
|
||||||
|
request.transactions["pump_filter"].failed = False
|
||||||
|
request.commitment = geyser_pb2.CommitmentLevel.PROCESSED
|
||||||
|
|
||||||
|
print(f"[INFO] Geyser gRPC listener active for {provider_name}")
|
||||||
|
|
||||||
|
async for update in stub.Subscribe(iter([request])):
|
||||||
|
tracker.increment_messages(provider_name)
|
||||||
|
|
||||||
|
# Skip non-transaction updates
|
||||||
|
if not update.HasField("transaction"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
tx = update.transaction.transaction.transaction
|
||||||
|
msg = getattr(tx, "message", None)
|
||||||
|
if msg is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for ix in msg.instructions:
|
||||||
|
if not ix.data.startswith(PUMP_CREATE_PREFIX):
|
||||||
|
continue
|
||||||
|
|
||||||
|
parsed = parse_create_instruction(ix.data)
|
||||||
|
if not parsed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(ix.accounts) == 0 or ix.accounts[0] >= len(msg.account_keys):
|
||||||
|
continue
|
||||||
|
|
||||||
|
mint = base58.b58encode(bytes(msg.account_keys[ix.accounts[0]])).decode()
|
||||||
|
|
||||||
|
if mint in known_tokens:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ts = time.time()
|
||||||
|
tracker.add_token(mint, parsed["name"], parsed["symbol"],
|
||||||
|
f"{provider_name}_geyser", ts)
|
||||||
|
known_tokens.add(mint)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Connection error in Geyser gRPC listener for {provider_name}: {e}")
|
||||||
|
print("[INFO] Reconnecting in 5 seconds...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
|
async def listen_pumpportal(provider_name, tracker, known_tokens=None):
|
||||||
|
"""
|
||||||
|
Listen for new tokens via PumpPortal WebSocket
|
||||||
|
"""
|
||||||
|
if known_tokens is None:
|
||||||
|
known_tokens = set()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print("[INFO] Connecting to PumpPortal WebSocket...")
|
||||||
|
async with websockets.connect(PUMPPORTAL_WS_URL) as websocket:
|
||||||
|
# Subscribe to new token events
|
||||||
|
await websocket.send(json.dumps({"method": "subscribeNewToken", "params": []}))
|
||||||
|
print(f"[INFO] PumpPortal listener active for {provider_name}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Receive WebSocket message
|
||||||
|
message = await websocket.recv()
|
||||||
|
data = json.loads(message)
|
||||||
|
tracker.increment_messages(provider_name)
|
||||||
|
|
||||||
|
# Extract token information
|
||||||
|
token_info = None
|
||||||
|
if "method" in data and data["method"] == "newToken":
|
||||||
|
token_info = data.get("params", [{}])[0]
|
||||||
|
elif "signature" in data and "mint" in data:
|
||||||
|
token_info = data
|
||||||
|
|
||||||
|
if not token_info:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get token details
|
||||||
|
mint = token_info.get("mint")
|
||||||
|
name = token_info.get("name", "Unknown")
|
||||||
|
symbol = token_info.get("symbol", "UNK")
|
||||||
|
|
||||||
|
if not mint:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip known tokens
|
||||||
|
if mint in known_tokens:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Record the token detection
|
||||||
|
ts = time.time()
|
||||||
|
tracker.add_token(mint, name, symbol,
|
||||||
|
f"{provider_name}_pumpportal", ts)
|
||||||
|
known_tokens.add(mint)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] PumpPortal listener for {provider_name}: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Connection error in PumpPortal listener for {provider_name}: {e}")
|
||||||
|
print("[INFO] Reconnecting in 5 seconds...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
|
# ============ MAIN TEST RUNNER ============
|
||||||
|
|
||||||
|
async def run_comparison_test(providers, test_duration=600):
|
||||||
|
"""
|
||||||
|
Run the comparison test with multiple WebSocket endpoints
|
||||||
|
|
||||||
|
Args:
|
||||||
|
providers: Dict of {provider_name: {'wss': wss_url, 'geyser': (endpoint, api_token)}}
|
||||||
|
test_duration: How long to run the test in seconds (default: 10 minutes)
|
||||||
|
"""
|
||||||
|
# Initialize our tracker and fetch existing tokens to avoid duplicates
|
||||||
|
tracker = DetectionTracker()
|
||||||
|
known_tokens = await fetch_existing_token_mints()
|
||||||
|
print(f"[INFO] Loaded {len(known_tokens)} existing tokens")
|
||||||
|
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
# Start all listeners for each provider
|
||||||
|
for provider_name, urls in providers.items():
|
||||||
|
if urls.get('wss'):
|
||||||
|
print(f"[INFO] Starting block listener for {provider_name}")
|
||||||
|
task = asyncio.create_task(
|
||||||
|
listen_block_subscription(urls['wss'], provider_name, tracker, known_tokens.copy())
|
||||||
|
)
|
||||||
|
tasks.append(task)
|
||||||
|
|
||||||
|
if urls.get('wss'):
|
||||||
|
print(f"[INFO] Starting logs listener for {provider_name}")
|
||||||
|
task = asyncio.create_task(
|
||||||
|
listen_logs_subscription(urls['wss'], provider_name, tracker, known_tokens.copy())
|
||||||
|
)
|
||||||
|
tasks.append(task)
|
||||||
|
|
||||||
|
if urls.get('geyser'):
|
||||||
|
endpoint, api_token = urls['geyser']
|
||||||
|
if endpoint and api_token:
|
||||||
|
print(f"[INFO] Starting Geyser gRPC listener for {provider_name}")
|
||||||
|
task = asyncio.create_task(
|
||||||
|
listen_geyser_grpc(endpoint, api_token, provider_name, tracker, known_tokens.copy())
|
||||||
|
)
|
||||||
|
tasks.append(task)
|
||||||
|
|
||||||
|
# Start PumpPortal listener (only once, not per provider)
|
||||||
|
print("[INFO] Starting PumpPortal listener")
|
||||||
|
task = asyncio.create_task(
|
||||||
|
listen_pumpportal("pumpportal", tracker, known_tokens.copy())
|
||||||
|
)
|
||||||
|
tasks.append(task)
|
||||||
|
|
||||||
|
print(f"[INFO] Test running for {test_duration} seconds...")
|
||||||
|
await asyncio.sleep(test_duration)
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
tracker.print_summary()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Read providers from environment variables
|
||||||
|
providers = {
|
||||||
|
"provider_1": {
|
||||||
|
'wss': os.environ.get("SOLANA_NODE_WSS_ENDPOINT"),
|
||||||
|
'geyser': (
|
||||||
|
os.environ.get("GEYSER_ENDPOINT"),
|
||||||
|
os.environ.get("GEYSER_API_TOKEN")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
# Add more providers to .env as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
# Filter out any providers with missing endpoints
|
||||||
|
providers = {name: urls for name, urls in providers.items()
|
||||||
|
if (urls.get('wss')) or
|
||||||
|
('geyser' in urls and urls['geyser'][0] and urls['geyser'][1])}
|
||||||
|
|
||||||
|
print(f"[INFO] Starting Pump.fun token detector comparison test for {TEST_DURATION} seconds")
|
||||||
|
print(f"[INFO] Providers: {', '.join(providers.keys())}")
|
||||||
|
|
||||||
|
asyncio.run(run_comparison_test(providers, test_duration=TEST_DURATION))
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
Monitors Solana for new Pump.fun token creations using Geyser gRPC.
|
Monitors Solana for new Pump.fun token creations using Geyser gRPC.
|
||||||
Decodes 'create' instructions to extract and display token details (name, symbol, mint, bonding curve).
|
Decodes 'create' instructions to extract and display token details (name, symbol, mint, bonding curve).
|
||||||
Requires a Geyser API token for access.
|
Requires a Geyser API token for access.
|
||||||
|
Supports both Basic and X-Token authentication methods.
|
||||||
|
|
||||||
It is proven to be the fastest listener.
|
It is proven to be the fastest listener.
|
||||||
"""
|
"""
|
||||||
@@ -21,16 +22,24 @@ load_dotenv()
|
|||||||
|
|
||||||
GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT")
|
GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT")
|
||||||
GEYSER_API_TOKEN = os.getenv("GEYSER_API_TOKEN")
|
GEYSER_API_TOKEN = os.getenv("GEYSER_API_TOKEN")
|
||||||
|
# Default to x-token auth, can be set to "basic"
|
||||||
|
AUTH_TYPE = "x-token"
|
||||||
|
|
||||||
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||||
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
|
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
|
||||||
|
|
||||||
|
|
||||||
async def create_geyser_connection():
|
async def create_geyser_connection():
|
||||||
"""Establish a secure connection to the Geyser endpoint."""
|
"""Establish a secure connection to the Geyser endpoint using the configured auth type."""
|
||||||
auth = grpc.metadata_call_credentials(
|
if AUTH_TYPE == "x-token":
|
||||||
lambda _, callback: callback((("authorization", f"Basic {GEYSER_API_TOKEN}"),), None)
|
auth = grpc.metadata_call_credentials(
|
||||||
)
|
lambda _, callback: callback((("x-token", GEYSER_API_TOKEN),), None)
|
||||||
|
)
|
||||||
|
else: # Default to basic auth
|
||||||
|
auth = grpc.metadata_call_credentials(
|
||||||
|
lambda _, callback: callback((("authorization", f"Basic {GEYSER_API_TOKEN}"),), None)
|
||||||
|
)
|
||||||
|
|
||||||
creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth)
|
creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth)
|
||||||
channel = grpc.aio.secure_channel(GEYSER_ENDPOINT, creds)
|
channel = grpc.aio.secure_channel(GEYSER_ENDPOINT, creds)
|
||||||
return geyser_pb2_grpc.GeyserStub(channel)
|
return geyser_pb2_grpc.GeyserStub(channel)
|
||||||
@@ -101,6 +110,7 @@ def print_token_info(info, signature):
|
|||||||
|
|
||||||
async def monitor_pump():
|
async def monitor_pump():
|
||||||
"""Monitor Solana blockchain for new Pump.fun token creations."""
|
"""Monitor Solana blockchain for new Pump.fun token creations."""
|
||||||
|
print(f"Starting Pump.fun token monitor using {AUTH_TYPE.upper()} authentication")
|
||||||
stub = await create_geyser_connection()
|
stub = await create_geyser_connection()
|
||||||
request = create_subscription_request()
|
request = create_subscription_request()
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ async def start_bot(config_path: str):
|
|||||||
# Geyser configuration (if applicable)
|
# Geyser configuration (if applicable)
|
||||||
geyser_endpoint=cfg.get("geyser", {}).get("endpoint"),
|
geyser_endpoint=cfg.get("geyser", {}).get("endpoint"),
|
||||||
geyser_api_token=cfg.get("geyser", {}).get("api_token"),
|
geyser_api_token=cfg.get("geyser", {}).get("api_token"),
|
||||||
|
geyser_auth_type=cfg.get("geyser", {}).get("auth_type"),
|
||||||
|
|
||||||
# Priority fee configuration
|
# Priority fee configuration
|
||||||
enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get("enable_dynamic", False),
|
enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get("enable_dynamic", False),
|
||||||
|
|||||||
@@ -20,26 +20,31 @@ logger = get_logger(__name__)
|
|||||||
class GeyserListener(BaseTokenListener):
|
class GeyserListener(BaseTokenListener):
|
||||||
"""Geyser listener for pump.fun token creation events."""
|
"""Geyser listener for pump.fun token creation events."""
|
||||||
|
|
||||||
def __init__(self, geyser_endpoint: str, geyser_api_token: str, pump_program: Pubkey):
|
def __init__(self, geyser_endpoint: str, geyser_api_token: str, geyser_auth_type: str, pump_program: Pubkey):
|
||||||
"""Initialize token listener.
|
"""Initialize token listener.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
geyser_endpoint: Geyser gRPC endpoint URL
|
geyser_endpoint: Geyser gRPC endpoint URL
|
||||||
geyser_api_token: API token for authentication
|
geyser_api_token: API token for authentication
|
||||||
|
geyser_auth_type: authentication type ('x-token' or 'basic')
|
||||||
pump_program: Pump.fun program address
|
pump_program: Pump.fun program address
|
||||||
"""
|
"""
|
||||||
self.geyser_endpoint = geyser_endpoint
|
self.geyser_endpoint = geyser_endpoint
|
||||||
self.geyser_api_token = geyser_api_token
|
self.geyser_api_token = geyser_api_token
|
||||||
|
self.auth_type = geyser_auth_type or "x-token"
|
||||||
self.pump_program = pump_program
|
self.pump_program = pump_program
|
||||||
self.event_processor = GeyserEventProcessor(pump_program)
|
self.event_processor = GeyserEventProcessor(pump_program)
|
||||||
|
|
||||||
async def _create_geyser_connection(self):
|
async def _create_geyser_connection(self):
|
||||||
"""Establish a secure connection to the Geyser endpoint."""
|
"""Establish a secure connection to the Geyser endpoint."""
|
||||||
auth = grpc.metadata_call_credentials(
|
if self.auth_type == "x-token":
|
||||||
lambda context, callback: callback(
|
auth = grpc.metadata_call_credentials(
|
||||||
(("authorization", f"Basic {self.geyser_api_token}"),), None
|
lambda _, callback: callback((("x-token", self.geyser_api_token),), None)
|
||||||
|
)
|
||||||
|
else: # Default to basic auth
|
||||||
|
auth = grpc.metadata_call_credentials(
|
||||||
|
lambda _, callback: callback((("authorization", f"Basic {self.geyser_api_token}"),), None)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
creds = grpc.composite_channel_credentials(
|
creds = grpc.composite_channel_credentials(
|
||||||
grpc.ssl_channel_credentials(), auth
|
grpc.ssl_channel_credentials(), auth
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class PumpTrader:
|
|||||||
listener_type: str = "logs",
|
listener_type: str = "logs",
|
||||||
geyser_endpoint: str | None = None,
|
geyser_endpoint: str | None = None,
|
||||||
geyser_api_token: str | None = None,
|
geyser_api_token: str | None = None,
|
||||||
|
geyser_auth_type: str = "x-token",
|
||||||
|
|
||||||
extreme_fast_mode: bool = False,
|
extreme_fast_mode: bool = False,
|
||||||
extreme_fast_token_amount: int = 30,
|
extreme_fast_token_amount: int = 30,
|
||||||
@@ -90,6 +91,7 @@ class PumpTrader:
|
|||||||
listener_type: Type of listener to use ('logs', 'blocks', or 'geyser')
|
listener_type: Type of listener to use ('logs', 'blocks', or 'geyser')
|
||||||
geyser_endpoint: Geyser endpoint URL (required for geyser listener)
|
geyser_endpoint: Geyser endpoint URL (required for geyser listener)
|
||||||
geyser_api_token: Geyser API token (required for geyser listener)
|
geyser_api_token: Geyser API token (required for geyser listener)
|
||||||
|
geyser_auth_type: Geyser authentication type ('x-token' or 'basic')
|
||||||
|
|
||||||
extreme_fast_mode: Whether to enable extreme fast mode
|
extreme_fast_mode: Whether to enable extreme fast mode
|
||||||
extreme_fast_token_amount: Maximum token amount for extreme fast mode
|
extreme_fast_token_amount: Maximum token amount for extreme fast mode
|
||||||
@@ -155,7 +157,8 @@ class PumpTrader:
|
|||||||
|
|
||||||
self.token_listener = GeyserListener(
|
self.token_listener = GeyserListener(
|
||||||
geyser_endpoint,
|
geyser_endpoint,
|
||||||
geyser_api_token,
|
geyser_api_token,
|
||||||
|
geyser_auth_type,
|
||||||
PumpAddresses.PROGRAM
|
PumpAddresses.PROGRAM
|
||||||
)
|
)
|
||||||
logger.info("Using Geyser listener for token monitoring")
|
logger.info("Using Geyser listener for token monitoring")
|
||||||
|
|||||||
Reference in New Issue
Block a user