docs(cursor): add uv rules, formatting
This commit is contained in:
@@ -172,7 +172,7 @@ class PumpEventProcessor:
|
||||
args["user"] = str(accounts[7])
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def _find_creator_vault(self, creator: Pubkey) -> Pubkey:
|
||||
"""
|
||||
Find the creator vault for a creator.
|
||||
@@ -184,10 +184,7 @@ class PumpEventProcessor:
|
||||
Creator vault address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
b"creator-vault",
|
||||
bytes(creator)
|
||||
],
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
|
||||
@@ -105,7 +105,7 @@ class BlockListener(BaseTokenListener):
|
||||
{"mentionsAccountOrProgram": str(self.pump_program)},
|
||||
{
|
||||
"commitment": "confirmed",
|
||||
"encoding": "base64", # base64 is faster than other encoding options
|
||||
"encoding": "base64", # base64 is faster than other encoding options
|
||||
"showRewards": False,
|
||||
"transactionDetails": "full",
|
||||
"maxSupportedTransactionVersion": 0,
|
||||
|
||||
@@ -28,7 +28,9 @@ class GeyserEventProcessor:
|
||||
"""
|
||||
self.pump_program = pump_program
|
||||
|
||||
def process_transaction_data(self, instruction_data: bytes, accounts: list, keys: list) -> TokenInfo | None:
|
||||
def process_transaction_data(
|
||||
self, instruction_data: bytes, accounts: list, keys: list
|
||||
) -> TokenInfo | None:
|
||||
"""Process transaction data and extract token creation info.
|
||||
|
||||
Args:
|
||||
@@ -45,7 +47,7 @@ class GeyserEventProcessor:
|
||||
try:
|
||||
# Skip past the 8-byte discriminator
|
||||
offset = 8
|
||||
|
||||
|
||||
# Helper to read strings (prefixed with length)
|
||||
def read_string():
|
||||
nonlocal offset
|
||||
@@ -53,16 +55,18 @@ class GeyserEventProcessor:
|
||||
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")
|
||||
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")
|
||||
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):
|
||||
@@ -71,7 +75,7 @@ class GeyserEventProcessor:
|
||||
if account_index >= len(keys):
|
||||
return None
|
||||
return Pubkey.from_bytes(keys[account_index])
|
||||
|
||||
|
||||
name = read_string()
|
||||
symbol = read_string()
|
||||
uri = read_string()
|
||||
@@ -83,11 +87,11 @@ class GeyserEventProcessor:
|
||||
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,
|
||||
@@ -99,11 +103,11 @@ class GeyserEventProcessor:
|
||||
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.
|
||||
@@ -115,10 +119,7 @@ class GeyserEventProcessor:
|
||||
Creator vault address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
b"creator-vault",
|
||||
bytes(creator)
|
||||
],
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
return derived_address
|
||||
|
||||
@@ -20,9 +20,15 @@ logger = get_logger(__name__)
|
||||
class GeyserListener(BaseTokenListener):
|
||||
"""Geyser listener for pump.fun token creation events."""
|
||||
|
||||
def __init__(self, geyser_endpoint: str, geyser_api_token: str, geyser_auth_type: str, pump_program: Pubkey):
|
||||
def __init__(
|
||||
self,
|
||||
geyser_endpoint: str,
|
||||
geyser_api_token: str,
|
||||
geyser_auth_type: str,
|
||||
pump_program: Pubkey,
|
||||
):
|
||||
"""Initialize token listener.
|
||||
|
||||
|
||||
Args:
|
||||
geyser_endpoint: Geyser gRPC endpoint URL
|
||||
geyser_api_token: API token for authentication
|
||||
@@ -40,27 +46,31 @@ class GeyserListener(BaseTokenListener):
|
||||
)
|
||||
self.pump_program = pump_program
|
||||
self.event_processor = GeyserEventProcessor(pump_program)
|
||||
|
||||
|
||||
async def _create_geyser_connection(self):
|
||||
"""Establish a secure connection to the Geyser endpoint."""
|
||||
if self.auth_type == "x-token":
|
||||
auth = grpc.metadata_call_credentials(
|
||||
lambda _, callback: callback((("x-token", 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)
|
||||
lambda _, callback: callback(
|
||||
(("authorization", f"Basic {self.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(self.geyser_endpoint, creds)
|
||||
return geyser_pb2_grpc.GeyserStub(channel), channel
|
||||
|
||||
def _create_subscription_request(self):
|
||||
"""Create a subscription request for Pump.fun transactions."""
|
||||
request = geyser_pb2.SubscribeRequest()
|
||||
request.transactions["pump_filter"].account_include.append(str(self.pump_program))
|
||||
request.transactions["pump_filter"].account_include.append(
|
||||
str(self.pump_program)
|
||||
)
|
||||
request.transactions["pump_filter"].failed = False
|
||||
request.commitment = geyser_pb2.CommitmentLevel.PROCESSED
|
||||
return request
|
||||
@@ -72,7 +82,7 @@ class GeyserListener(BaseTokenListener):
|
||||
creator_address: str | None = None,
|
||||
) -> None:
|
||||
"""Listen for new token creations using Geyser subscription.
|
||||
|
||||
|
||||
Args:
|
||||
token_callback: Callback function for new tokens
|
||||
match_string: Optional string to match in token name/symbol
|
||||
@@ -82,20 +92,22 @@ class GeyserListener(BaseTokenListener):
|
||||
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 for transactions involving program: {self.pump_program}"
|
||||
)
|
||||
|
||||
try:
|
||||
async for update in stub.Subscribe(iter([request])):
|
||||
token_info = await self._process_update(update)
|
||||
if not token_info:
|
||||
continue
|
||||
|
||||
|
||||
logger.info(
|
||||
f"New token detected: {token_info.name} ({token_info.symbol})"
|
||||
)
|
||||
|
||||
|
||||
if match_string and not (
|
||||
match_string.lower() in token_info.name.lower()
|
||||
or match_string.lower() in token_info.symbol.lower()
|
||||
@@ -104,43 +116,40 @@ class GeyserListener(BaseTokenListener):
|
||||
f"Token does not match filter '{match_string}'. Skipping..."
|
||||
)
|
||||
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..."
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
await token_callback(token_info)
|
||||
|
||||
|
||||
except grpc.aio.AioRpcError as e:
|
||||
logger.error(f"gRPC error: {e.details()}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
finally:
|
||||
await channel.close()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Geyser connection error: {e}")
|
||||
logger.info("Reconnecting in 10 seconds...")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
|
||||
async def _process_update(self, update) -> TokenInfo | None:
|
||||
"""Process a Geyser update and extract token creation info.
|
||||
|
||||
|
||||
Args:
|
||||
update: Geyser update from the subscription
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if a token creation is found, None otherwise
|
||||
"""
|
||||
try:
|
||||
if not update.HasField("transaction"):
|
||||
return None
|
||||
|
||||
|
||||
tx = update.transaction.transaction.transaction
|
||||
msg = getattr(tx, "message", None)
|
||||
if msg is None:
|
||||
@@ -151,20 +160,20 @@ class GeyserListener(BaseTokenListener):
|
||||
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
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Geyser update: {e}")
|
||||
return None
|
||||
|
||||
@@ -43,7 +43,7 @@ class LogsEventProcessor:
|
||||
# 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
|
||||
@@ -55,7 +55,7 @@ class LogsEventProcessor:
|
||||
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"])
|
||||
@@ -64,7 +64,7 @@ class LogsEventProcessor:
|
||||
)
|
||||
creator = Pubkey.from_string(parsed_data["creator"])
|
||||
creator_vault = self._find_creator_vault(creator)
|
||||
|
||||
|
||||
return TokenInfo(
|
||||
name=parsed_data["name"],
|
||||
symbol=parsed_data["symbol"],
|
||||
@@ -78,7 +78,7 @@ class LogsEventProcessor:
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process log data: {e}")
|
||||
|
||||
|
||||
return None
|
||||
|
||||
def _parse_create_instruction(self, data: bytes) -> dict | None:
|
||||
@@ -92,11 +92,13 @@ class LogsEventProcessor:
|
||||
"""
|
||||
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}")
|
||||
logger.info(
|
||||
f"Skipping non-Create instruction with discriminator: {discriminator}"
|
||||
)
|
||||
return None
|
||||
|
||||
offset = 8
|
||||
@@ -154,7 +156,7 @@ class LogsEventProcessor:
|
||||
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
|
||||
|
||||
def _find_creator_vault(self, creator: Pubkey) -> Pubkey:
|
||||
"""
|
||||
Find the creator vault for a creator.
|
||||
@@ -166,10 +168,7 @@ class LogsEventProcessor:
|
||||
Creator vault address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
b"creator-vault",
|
||||
bytes(creator)
|
||||
],
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
|
||||
@@ -55,7 +55,7 @@ class PumpPortalEventProcessor:
|
||||
mint = Pubkey.from_string(mint_str)
|
||||
bonding_curve = Pubkey.from_string(bonding_curve_str)
|
||||
user = Pubkey.from_string(creator_str)
|
||||
|
||||
|
||||
# For PumpPortal, we assume the creator is the same as the user
|
||||
# since PumpPortal doesn't distinguish between them
|
||||
creator = user
|
||||
@@ -117,10 +117,7 @@ class PumpPortalEventProcessor:
|
||||
Creator vault address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
b"creator-vault",
|
||||
bytes(creator)
|
||||
],
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
return derived_address
|
||||
|
||||
@@ -20,7 +20,11 @@ logger = get_logger(__name__)
|
||||
class PumpPortalListener(BaseTokenListener):
|
||||
"""PumpPortal listener for pump.fun token creation events."""
|
||||
|
||||
def __init__(self, pump_program: Pubkey, pumpportal_url: str = "wss://pumpportal.fun/api/data"):
|
||||
def __init__(
|
||||
self,
|
||||
pump_program: Pubkey,
|
||||
pumpportal_url: str = "wss://pumpportal.fun/api/data",
|
||||
):
|
||||
"""Initialize token listener.
|
||||
|
||||
Args:
|
||||
@@ -82,7 +86,9 @@ class PumpPortalListener(BaseTokenListener):
|
||||
await token_callback(token_info)
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.warning("PumpPortal WebSocket connection closed. Reconnecting...")
|
||||
logger.warning(
|
||||
"PumpPortal WebSocket connection closed. Reconnecting..."
|
||||
)
|
||||
finally:
|
||||
ping_task.cancel()
|
||||
try:
|
||||
@@ -94,16 +100,14 @@ class PumpPortalListener(BaseTokenListener):
|
||||
logger.exception("PumpPortal WebSocket connection error")
|
||||
logger.info("Reconnecting in 5 seconds...")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _subscribe_to_new_tokens(self, websocket) -> None:
|
||||
"""Subscribe to new token events from PumpPortal.
|
||||
|
||||
Args:
|
||||
websocket: Active WebSocket connection
|
||||
"""
|
||||
subscription_message = json.dumps({
|
||||
"method": "subscribeNewToken",
|
||||
"params": []
|
||||
})
|
||||
subscription_message = json.dumps({"method": "subscribeNewToken", "params": []})
|
||||
|
||||
await websocket.send(subscription_message)
|
||||
logger.info("Subscribed to PumpPortal new token events")
|
||||
@@ -167,4 +171,4 @@ class PumpPortalListener(BaseTokenListener):
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing PumpPortal WebSocket message: {e}")
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user