diff --git a/learning-examples/manual_buy_geyser.py b/learning-examples/manual_buy_geyser.py new file mode 100644 index 0000000..73f2c89 --- /dev/null +++ b/learning-examples/manual_buy_geyser.py @@ -0,0 +1,397 @@ +import asyncio +import json +import os +import struct + +import base58 +import grpc +from construct import Bytes, Flag, Int64ul, Struct +from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Confirmed +from solana.rpc.types import TxOpts +from solders.compute_budget import set_compute_unit_price +from solders.instruction import AccountMeta, Instruction +from solders.keypair import Keypair +from solders.message import Message +from solders.pubkey import Pubkey +from solders.transaction import Transaction +from spl.token.instructions import ( + create_idempotent_associated_token_account, + get_associated_token_address, +) + +from src.geyser.generated import ( + geyser_pb2, + geyser_pb2_grpc, +) + +# Here and later all the discriminators are precalculated. See learning-examples/calculate_discriminator.py +EXPECTED_DISCRIMINATOR = struct.pack(" None: + """Parse bonding curve data.""" + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + + parsed = self._STRUCT.parse(data[8:]) + self.__dict__.update(parsed) + + # Convert raw bytes to Pubkey for creator field + if hasattr(self, "creator") and isinstance(self.creator, bytes): + self.creator = Pubkey.from_bytes(self.creator) + + +async def get_pump_curve_state( + conn: AsyncClient, curve_address: Pubkey +) -> BondingCurveState: + response = await conn.get_account_info(curve_address, encoding="base64") + if not response.value or not response.value.data: + raise ValueError("Invalid curve state: No data") + + data = response.value.data + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + + return BondingCurveState(data) + + +def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: + if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: + raise ValueError("Invalid reserve state") + + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( + curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS + ) + + +def _find_creator_vault(creator: Pubkey) -> Pubkey: + derived_address, _ = Pubkey.find_program_address( + [b"creator-vault", bytes(creator)], + PUMP_PROGRAM, + ) + return derived_address + + +def _find_global_volume_accumulator() -> Pubkey: + derived_address, _ = Pubkey.find_program_address( + [b"global_volume_accumulator"], + PUMP_PROGRAM, + ) + return derived_address + + +def _find_user_volume_accumulator(user: Pubkey) -> Pubkey: + derived_address, _ = Pubkey.find_program_address( + [b"user_volume_accumulator", bytes(user)], + PUMP_PROGRAM, + ) + return derived_address + + +async def create_geyser_connection(): + """Establish a secure connection to the Geyser endpoint using the configured auth type.""" + if AUTH_TYPE == "x-token": + 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) + channel = grpc.aio.secure_channel(GEYSER_ENDPOINT, creds) + return geyser_pb2_grpc.GeyserStub(channel) + + +def create_subscription_request(): + """Create a subscription request for Pump.fun transactions.""" + request = geyser_pb2.SubscribeRequest() + request.transactions["pump_filter"].account_include.append(str(PUMP_PROGRAM)) + request.transactions["pump_filter"].failed = False + request.commitment = geyser_pb2.CommitmentLevel.PROCESSED + return request + + +def decode_create_instruction_geyser(ix_data: bytes, keys, accounts) -> dict: + """Decode a create instruction from Geyser transaction data.""" + # Skip past the 8-byte discriminator prefix + offset = 8 + + # Extract account keys in base58 format + def get_account_key(index): + if index >= len(accounts): + return "N/A" + account_index = accounts[index] + return base58.b58encode(keys[account_index]).decode() + + # Read string fields (prefixed with length) + def read_string(): + nonlocal offset + # Get string length (4-byte uint) + length = struct.unpack_from("