From 8bf37001871d5a8101b332d1d049f9494574aa96 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Wed, 5 Mar 2025 07:03:32 +0000 Subject: [PATCH 01/65] fixed formatting --- .env.example | 3 + .gitignore | 3 + .vscode/settings.json | 12 + buy.py | 248 +++++++++----- config.py | 30 +- .../blockSubscribe_extract_transactions.py | 80 +++-- learning-examples/calculate_discriminator.py | 16 +- .../check_boding_curve_status.py | 73 +++-- .../compute_associated_bonding_curve.py | 40 +-- .../decode_from_blockSubscribe.py | 139 ++++---- .../decode_from_getAccountInfo.py | 21 +- .../decode_from_getTransaction.py | 64 ++-- learning-examples/fetch_price.py | 23 +- .../listen_create_from_blocksubscribe.py | 166 ++++++---- learning-examples/listen_new_direct.py | 107 +++--- .../listen_new_direct_full_details.py | 135 +++++--- learning-examples/listen_new_portal.py | 39 ++- .../listen_to_raydium_migration.py | 100 +++--- learning-examples/manual_buy.py | 307 ++++++++++++------ learning-examples/manual_sell.py | 110 +++++-- pyproject.toml | 6 + requirements.txt | 11 +- sell.py | 96 ++++-- trade.py | 140 +++++--- 24 files changed, 1261 insertions(+), 708 deletions(-) create mode 100644 .env.example create mode 100644 .vscode/settings.json create mode 100644 pyproject.toml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ade398d --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +RPC_ENDPOINT=... +WSS_ENDPOINT=... +PRIVATE_KEY=... \ No newline at end of file diff --git a/.gitignore b/.gitignore index efa407c..55804a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.pylintrc +.ruff_cache + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..87a399e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "python.languageServer": "Pylance", + "python.analysis.typeCheckingMode": "basic", + + "ruff.lint.enable": true, + "ruff.format.enable": true, + + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff" + } +} \ No newline at end of file diff --git a/buy.py b/buy.py index 0d39617..4e35ee1 100644 --- a/buy.py +++ b/buy.py @@ -1,34 +1,32 @@ import asyncio -import json import base64 -import struct -import base58 import hashlib -import websockets +import json +import struct import time +import base58 +import spl.token.instructions as spl_token +import websockets +from construct import Flag, Int64ul, Struct from solana.rpc.async_api import AsyncClient -from solana.transaction import Transaction from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts - -from solders.pubkey import Pubkey +from solana.transaction import Transaction +from solders.instruction import AccountMeta, Instruction from solders.keypair import Keypair -from solders.instruction import Instruction, AccountMeta +from solders.pubkey import Pubkey from solders.system_program import TransferParams, transfer from solders.transaction import VersionedTransaction - from spl.token.instructions import get_associated_token_address -import spl.token.instructions as spl_token from config import * -from construct import Struct, Int64ul, Flag - # Here and later all the discriminators are precalculated. See learning-examples/discriminator.py EXPECTED_DISCRIMINATOR = struct.pack(" None: parsed = self._STRUCT.parse(data[8:]) self.__dict__.update(parsed) -async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState: + +async def get_pump_curve_state( + conn: AsyncClient, curve_address: Pubkey +) -> BondingCurveState: response = await conn.get_account_info(curve_address) if not response.value or not response.value.data: raise ValueError("Invalid curve state: No data") @@ -54,13 +55,24 @@ async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> Bond 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) + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( + curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS + ) -async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, amount: float, slippage: float = 0.01, max_retries=5): + +async def buy_token( + mint: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + amount: float, + slippage: float = 0.01, + max_retries=5, +): private_key = base58.b58decode(PRIVATE_KEY) payer = Keypair.from_bytes(private_key) @@ -81,11 +93,11 @@ async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curv try: account_info = await client.get_account_info(associated_token_account) if account_info.value is None: - print(f"Creating associated token account (Attempt {ata_attempt + 1})...") + print( + f"Creating associated token account (Attempt {ata_attempt + 1})..." + ) create_ata_ix = spl_token.create_associated_token_account( - payer=payer.pubkey(), - owner=payer.pubkey(), - mint=mint + payer=payer.pubkey(), owner=payer.pubkey(), mint=mint ) create_ata_tx = Transaction() create_ata_tx.add(create_ata_ix) @@ -93,20 +105,28 @@ async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curv create_ata_tx.recent_blockhash = recent_blockhash.value.blockhash await client.send_transaction(create_ata_tx, payer) print("Associated token account created.") - print(f"Associated token account address: {associated_token_account}") + print( + f"Associated token account address: {associated_token_account}" + ) break else: print("Associated token account already exists.") - print(f"Associated token account address: {associated_token_account}") + print( + f"Associated token account address: {associated_token_account}" + ) break except Exception as e: - print(f"Attempt {ata_attempt + 1} to create associated token account failed: {str(e)}") + print( + f"Attempt {ata_attempt + 1} to create associated token account failed: {str(e)}" + ) if ata_attempt < max_retries - 1: - wait_time = 2 ** ata_attempt + wait_time = 2**ata_attempt print(f"Retrying in {wait_time} seconds...") await asyncio.sleep(wait_time) else: - print("Max retries reached. Unable to create associated token account.") + print( + "Max retries reached. Unable to create associated token account." + ) return # Continue with the buy transaction @@ -116,19 +136,43 @@ async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curv AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), - AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + AccountMeta( + pubkey=bonding_curve, is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=associated_bonding_curve, + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=associated_token_account, + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=payer.pubkey(), is_signer=True, is_writable=True + ), + AccountMeta( + pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False + ), AccountMeta(pubkey=SYSTEM_RENT, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), + AccountMeta( + pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False + ), ] discriminator = struct.pack(" 0: - tx_signature = tx['transaction'][0] - elif isinstance(tx['transaction'], dict) and 'signatures' in tx['transaction']: - tx_signature = tx['transaction']['signatures'][0] + if isinstance(tx, dict) and "transaction" in tx: + if ( + isinstance(tx["transaction"], list) + and len(tx["transaction"]) > 0 + ): + tx_signature = tx["transaction"][0] + elif ( + isinstance(tx["transaction"], dict) + and "signatures" in tx["transaction"] + ): + tx_signature = tx["transaction"][ + "signatures" + ][0] else: continue await save_transaction(tx, tx_signature) - elif 'result' in data: + elif "result" in data: print(f"Subscription confirmed") except Exception as e: print(f"An error occurred: {str(e)}") + if __name__ == "__main__": - asyncio.run(listen_for_transactions()) \ No newline at end of file + asyncio.run(listen_for_transactions()) diff --git a/learning-examples/calculate_discriminator.py b/learning-examples/calculate_discriminator.py index 438262c..1671a22 100644 --- a/learning-examples/calculate_discriminator.py +++ b/learning-examples/calculate_discriminator.py @@ -5,21 +5,23 @@ import struct # Set the instruction name here instruction_name = "account:BondingCurve" + def calculate_discriminator(instruction_name): # Create a SHA256 hash object sha = hashlib.sha256() - + # Update the hash with the instruction name - sha.update(instruction_name.encode('utf-8')) - + sha.update(instruction_name.encode("utf-8")) + # Get the first 8 bytes of the hash discriminator_bytes = sha.digest()[:8] - + # Convert the bytes to a 64-bit unsigned integer (little-endian) - discriminator = struct.unpack(' None: parsed = self._STRUCT.parse(data[8:]) self.__dict__.update(parsed) -def get_associated_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> tuple[Pubkey, int]: + +def get_associated_bonding_curve_address( + mint: Pubkey, program_id: Pubkey +) -> tuple[Pubkey, int]: """ Derives the associated bonding curve address for a given mint """ - return Pubkey.find_program_address( - [ - b"bonding-curve", - bytes(mint) - ], - program_id - ) + return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], program_id) -async def get_bonding_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState: + +async def get_bonding_curve_state( + conn: AsyncClient, curve_address: Pubkey +) -> BondingCurveState: response = await conn.get_account_info(curve_address) if not response.value or not response.value.data: raise ValueError("Invalid curve state: No data") @@ -51,46 +53,57 @@ async def get_bonding_curve_state(conn: AsyncClient, curve_address: Pubkey) -> B return BondingCurveState(data) + async def check_token_status(mint_address: str) -> None: try: mint = Pubkey.from_string(mint_address) - + # Get the associated bonding curve address - bonding_curve_address, bump = get_associated_bonding_curve_address(mint, PUMP_PROGRAM) - + bonding_curve_address, bump = get_associated_bonding_curve_address( + mint, PUMP_PROGRAM + ) + print("\nToken Status:") print("-" * 50) print(f"Token Mint: {mint}") print(f"Associated Bonding Curve: {bonding_curve_address}") print(f"Bump Seed: {bump}") print("-" * 50) - + # Check completion status async with AsyncClient(RPC_ENDPOINT) as client: try: - curve_state = await get_bonding_curve_state(client, bonding_curve_address) - + curve_state = await get_bonding_curve_state( + client, bonding_curve_address + ) + print("\nBonding Curve Status:") print("-" * 50) - print(f"Completion Status: {'Completed' if curve_state.complete else 'Not Completed'}") + print( + f"Completion Status: {'Completed' if curve_state.complete else 'Not Completed'}" + ) if curve_state.complete: - print("\nNote: This bonding curve has completed and liquidity has been migrated to Raydium.") + print( + "\nNote: This bonding curve has completed and liquidity has been migrated to Raydium." + ) print("-" * 50) - + except ValueError as e: print(f"\nError accessing bonding curve: {e}") - + except ValueError as e: print(f"\nError: Invalid address format - {e}") except Exception as e: print(f"\nUnexpected error: {e}") + def main(): - parser = argparse.ArgumentParser(description='Check token bonding curve status') - parser.add_argument('mint_address', help='The token mint address') - + parser = argparse.ArgumentParser(description="Check token bonding curve status") + parser.add_argument("mint_address", help="The token mint address") + args = parser.parse_args() asyncio.run(check_token_status(args.mint_address)) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/learning-examples/compute_associated_bonding_curve.py b/learning-examples/compute_associated_bonding_curve.py index 3caa9aa..d354eb1 100644 --- a/learning-examples/compute_associated_bonding_curve.py +++ b/learning-examples/compute_associated_bonding_curve.py @@ -1,52 +1,51 @@ -import sys import os +import sys + from solders.pubkey import Pubkey -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from config import PUMP_PROGRAM + def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> tuple[Pubkey, int]: """ Derives the bonding curve address for a given mint """ - return Pubkey.find_program_address( - [ - b"bonding-curve", - bytes(mint) - ], - program_id - ) + return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], program_id) + def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey: """ Find the associated bonding curve for a given mint and bonding curve. This uses the standard ATA derivation. """ - from config import SYSTEM_TOKEN_PROGRAM as TOKEN_PROGRAM_ID from config import SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM as ATA_PROGRAM_ID - + from config import SYSTEM_TOKEN_PROGRAM as TOKEN_PROGRAM_ID + derived_address, _ = Pubkey.find_program_address( [ bytes(bonding_curve), bytes(TOKEN_PROGRAM_ID), - bytes(mint), + bytes(mint), ], - ATA_PROGRAM_ID + ATA_PROGRAM_ID, ) return derived_address -def main(): +def main(): mint_address = input("Enter the token mint address: ") - + try: mint = Pubkey.from_string(mint_address) - + bonding_curve_address, bump = get_bonding_curve_address(mint, PUMP_PROGRAM) - + # Calculate the associated bonding curve - associated_bonding_curve = find_associated_bonding_curve(mint, bonding_curve_address) - + associated_bonding_curve = find_associated_bonding_curve( + mint, bonding_curve_address + ) + print("\nResults:") print("-" * 50) print(f"Token Mint: {mint}") @@ -54,9 +53,10 @@ def main(): print(f"Associated Bonding Curve: {associated_bonding_curve}") print(f"Bonding Curve Bump: {bump}") print("-" * 50) - + except ValueError as e: print(f"Error: Invalid address format - {str(e)}") + if __name__ == "__main__": main() diff --git a/learning-examples/decode_from_blockSubscribe.py b/learning-examples/decode_from_blockSubscribe.py index f0a87e4..4124568 100644 --- a/learning-examples/decode_from_blockSubscribe.py +++ b/learning-examples/decode_from_blockSubscribe.py @@ -1,59 +1,65 @@ import base64 +import hashlib import json import struct -import hashlib -from solana.transaction import Transaction -from solders.transaction import VersionedTransaction -from solders.pubkey import Pubkey import sys +from solana.transaction import Transaction +from solders.pubkey import Pubkey +from solders.transaction import VersionedTransaction + + def load_idl(file_path): - with open(file_path, 'r') as f: + with open(file_path, "r") as f: return json.load(f) + def load_transaction(file_path): - with open(file_path, 'r') as f: + with open(file_path, "r") as f: data = json.load(f) return data + def decode_instruction(ix_data, ix_def): args = {} offset = 8 # Skip 8-byte discriminator - for arg in ix_def['args']: - if arg['type'] == 'u64': - value = struct.unpack_from('") sys.exit(1) tx_file_path = sys.argv[1] -idl = load_idl('../idl/pump_fun_idl.json') +idl = load_idl("../idl/pump_fun_idl.json") tx_data = load_transaction(tx_file_path) decoded_instructions = decode_transaction(tx_data, idl) -print(json.dumps(decoded_instructions, indent=2)) \ No newline at end of file +print(json.dumps(decoded_instructions, indent=2)) diff --git a/learning-examples/decode_from_getAccountInfo.py b/learning-examples/decode_from_getAccountInfo.py index 7dd5e1c..1a62ac3 100644 --- a/learning-examples/decode_from_getAccountInfo.py +++ b/learning-examples/decode_from_getAccountInfo.py @@ -1,12 +1,14 @@ -import json import base64 +import json import struct -from construct import Struct, Int64ul, Flag + +from construct import Flag, Int64ul, Struct LAMPORTS_PER_SOL = 1_000_000_000 TOKEN_DECIMALS = 6 EXPECTED_DISCRIMINATOR = struct.pack(" None: parsed = self._STRUCT.parse(data[8:]) self.__dict__.update(parsed) + def calculate_bonding_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) + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( + curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS + ) + def decode_bonding_curve_data(raw_data: str) -> BondingCurveState: decoded_data = base64.b64decode(raw_data) @@ -33,12 +39,13 @@ def decode_bonding_curve_data(raw_data: str) -> BondingCurveState: raise ValueError("Invalid curve state discriminator") return BondingCurveState(decoded_data) + # Load the JSON data -with open('raw_bondingCurve_from_getAccountInfo.json', 'r') as file: +with open("raw_bondingCurve_from_getAccountInfo.json", "r") as file: json_data = json.load(file) # Extract the base64 encoded data -encoded_data = json_data['result']['value']['data'][0] +encoded_data = json_data["result"]["value"]["data"][0] # Decode the data bonding_curve_state = decode_bonding_curve_data(encoded_data) @@ -53,4 +60,4 @@ print(f" Real Token Reserves: {bonding_curve_state.real_token_reserves}") print(f" Real SOL Reserves: {bonding_curve_state.real_sol_reserves}") print(f" Token Total Supply: {bonding_curve_state.token_total_supply}") print(f" Complete: {bonding_curve_state.complete}") -print(f"\nToken Price: {token_price_sol:.10f} SOL") \ No newline at end of file +print(f"\nToken Price: {token_price_sol:.10f} SOL") diff --git a/learning-examples/decode_from_getTransaction.py b/learning-examples/decode_from_getTransaction.py index 519f146..5be9b6d 100644 --- a/learning-examples/decode_from_getTransaction.py +++ b/learning-examples/decode_from_getTransaction.py @@ -1,12 +1,11 @@ +import base64 import json +import struct +import sys + import base58 from solana.transaction import Transaction from solders.pubkey import Pubkey -import struct -import sys -import base64 - -import sys if len(sys.argv) != 2: print("Usage: python decode_getTransaction.py ") @@ -15,18 +14,19 @@ if len(sys.argv) != 2: tx_file_path = sys.argv[1] # Load the IDL -with open('../idl/pump_fun_idl.json', 'r') as f: +with open("../idl/pump_fun_idl.json", "r") as f: idl = json.load(f) # Load the transaction log -with open(tx_file_path, 'r') as f: +with open(tx_file_path, "r") as f: tx_log = json.load(f) # Extract the transaction data -tx_data = tx_log['result']['transaction'] +tx_data = tx_log["result"]["transaction"] print(json.dumps(tx_data, indent=2)) + def decode_create_instruction(data): # The Create instruction has 3 string arguments: name, symbol, uri offset = 8 # Skip the 8-byte discriminator @@ -34,59 +34,61 @@ def decode_create_instruction(data): for _ in range(3): length = struct.unpack_from(" None: parsed = self._STRUCT.parse(data[8:]) self.__dict__.update(parsed) -async def get_bonding_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState: + +async def get_bonding_curve_state( + conn: AsyncClient, curve_address: Pubkey +) -> BondingCurveState: response = await conn.get_account_info(curve_address) if not response.value or not response.value.data: raise ValueError("Invalid curve state: No data") @@ -43,11 +47,15 @@ async def get_bonding_curve_state(conn: AsyncClient, curve_address: Pubkey) -> B return BondingCurveState(data) + def calculate_bonding_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) + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( + curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS + ) + async def main() -> None: try: @@ -63,5 +71,6 @@ async def main() -> None: except Exception as e: print(f"An unexpected error occurred: {e}") + if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/learning-examples/listen_create_from_blocksubscribe.py b/learning-examples/listen_create_from_blocksubscribe.py index d3d726a..8233b51 100644 --- a/learning-examples/listen_create_from_blocksubscribe.py +++ b/learning-examples/listen_create_from_blocksubscribe.py @@ -1,68 +1,74 @@ import asyncio -import json -import websockets import base64 -import struct import hashlib -import sys +import json import os -from solders.transaction import VersionedTransaction -from solders.pubkey import Pubkey +import struct +import sys + +import websockets +from solders.pubkey import Pubkey +from solders.transaction import VersionedTransaction + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from config import PUMP_PROGRAM, WSS_ENDPOINT -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from config import WSS_ENDPOINT, PUMP_PROGRAM def load_idl(file_path): - with open(file_path, 'r') as f: + with open(file_path, "r") as f: return json.load(f) + def decode_create_instruction(ix_data, ix_def, accounts): args = {} offset = 8 # Skip 8-byte discriminator - for arg in ix_def['args']: - if arg['type'] == 'string': - length = struct.unpack_from(' Pubkey: """ @@ -27,16 +31,20 @@ def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey bytes(TOKEN_PROGRAM_ID), bytes(mint), ], - ATA_PROGRAM_ID + ATA_PROGRAM_ID, ) return derived_address + # Load the IDL JSON file -with open('../idl/pump_fun_idl.json', 'r') as f: +with open("../idl/pump_fun_idl.json", "r") as f: idl = json.load(f) # Extract the "create" instruction definition -create_instruction = next(instr for instr in idl['instructions'] if instr['name'] == 'create') +create_instruction = next( + instr for instr in idl["instructions"] if instr["name"] == "create" +) + def parse_create_instruction(data): if len(data) < 8: @@ -46,23 +54,23 @@ def parse_create_instruction(data): # Parse fields based on CreateEvent structure fields = [ - ('name', 'string'), - ('symbol', 'string'), - ('uri', 'string'), - ('mint', 'publicKey'), - ('bondingCurve', 'publicKey'), - ('user', 'publicKey'), + ("name", "string"), + ("symbol", "string"), + ("uri", "string"), + ("mint", "publicKey"), + ("bondingCurve", "publicKey"), + ("user", "publicKey"), ] try: for field_name, field_type in fields: - if field_type == 'string': - length = struct.unpack(' 18: token_address = account_keys[18] liquidity_address = account_keys[2] - + print(f"\nSignature: {signature}") print(f"Token Address: {token_address}") print(f"Liquidity Address: {liquidity_address}") print("=" * 50) else: print(f"\nError: Not enough account keys (found {len(account_keys)})") - + except Exception as e: print(f"\nError: {str(e)}") + async def listen_for_events(): while True: try: async with websockets.connect(WSS_ENDPOINT) as websocket: - subscription_message = json.dumps({ - "jsonrpc": "2.0", - "id": 1, - "method": "blockSubscribe", - "params": [ - {"mentionsAccountOrProgram": str(PUMP_LIQUIDITY_MIGRATOR)}, - { - "commitment": "confirmed", - "encoding": "json", - "showRewards": False, - "transactionDetails": "full", - "maxSupportedTransactionVersion": 0 - } - ] - }) - + subscription_message = json.dumps( + { + "jsonrpc": "2.0", + "id": 1, + "method": "blockSubscribe", + "params": [ + {"mentionsAccountOrProgram": str(PUMP_LIQUIDITY_MIGRATOR)}, + { + "commitment": "confirmed", + "encoding": "json", + "showRewards": False, + "transactionDetails": "full", + "maxSupportedTransactionVersion": 0, + }, + ], + } + ) + await websocket.send(subscription_message) response = await websocket.recv() print(f"Subscription response: {response}") @@ -61,32 +66,43 @@ async def listen_for_events(): try: response = await asyncio.wait_for(websocket.recv(), timeout=30) data = json.loads(response) - - if 'method' in data and data['method'] == 'blockNotification': - if 'params' in data and 'result' in data['params']: - block_data = data['params']['result'] - if 'value' in block_data and 'block' in block_data['value']: - block = block_data['value']['block'] - if 'transactions' in block: - for tx in block['transactions']: - logs = tx.get('meta', {}).get('logMessages', []) - + + if "method" in data and data["method"] == "blockNotification": + if "params" in data and "result" in data["params"]: + block_data = data["params"]["result"] + if ( + "value" in block_data + and "block" in block_data["value"] + ): + block = block_data["value"]["block"] + if "transactions" in block: + for tx in block["transactions"]: + logs = tx.get("meta", {}).get( + "logMessages", [] + ) + # Check for initialize2 instruction for log in logs: - if "Program log: initialize2: InitializeInstruction2" in log: - print("Found initialize2 instruction!") + if ( + "Program log: initialize2: InitializeInstruction2" + in log + ): + print( + "Found initialize2 instruction!" + ) process_initialize2_transaction(tx) break - + except asyncio.TimeoutError: print("\nChecking connection...") print("Connection alive") continue - + except Exception as e: print(f"\nConnection error: {str(e)}") print("Retrying in 5 seconds...") await asyncio.sleep(5) + if __name__ == "__main__": - asyncio.run(listen_for_events()) \ No newline at end of file + asyncio.run(listen_for_events()) diff --git a/learning-examples/manual_buy.py b/learning-examples/manual_buy.py index 2482f5b..91d8616 100644 --- a/learning-examples/manual_buy.py +++ b/learning-examples/manual_buy.py @@ -1,29 +1,26 @@ import asyncio -import json import base64 -import struct -import base58 +import hashlib +import json import os +import struct -from solana.transaction import Message +import base58 +import spl.token.instructions as spl_token +import websockets +from construct import Flag, Int64ul, Struct from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts - -from solders.pubkey import Pubkey -from solders.keypair import Keypair -from solders.instruction import Instruction, AccountMeta -from solders.system_program import TransferParams, transfer -from solders.transaction import VersionedTransaction, Transaction +from solana.transaction import Message from solders.compute_budget import set_compute_unit_price - -import spl.token.instructions as spl_token +from solders.instruction import AccountMeta, Instruction +from solders.keypair import Keypair +from solders.pubkey import Pubkey +from solders.system_program import TransferParams, transfer +from solders.transaction import Transaction, VersionedTransaction from spl.token.instructions import get_associated_token_address -import websockets -import hashlib -from construct import Struct, Int64ul, Flag - # Here and later all the discriminators are precalculated. See learning-examples/discriminator.py EXPECTED_DISCRIMINATOR = struct.pack(" None: parsed = self._STRUCT.parse(data[8:]) self.__dict__.update(parsed) -async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState: + +async def get_pump_curve_state( + conn: AsyncClient, curve_address: Pubkey +) -> BondingCurveState: response = await conn.get_account_info(curve_address) if not response.value or not response.value.data: raise ValueError("Invalid curve state: No data") @@ -69,13 +74,24 @@ async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> Bond 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) + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( + curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS + ) -async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, amount: float, slippage: float = 0.25, max_retries=5): + +async def buy_token( + mint: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + amount: float, + slippage: float = 0.25, + max_retries=5, +): private_key = base58.b58decode("ENTER_PRIVATE_KEY") payer = Keypair.from_bytes(private_key) @@ -96,36 +112,52 @@ async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curv try: account_info = await client.get_account_info(associated_token_account) if account_info.value is None: - print(f"Creating associated token account (Attempt {ata_attempt + 1})...") + print( + f"Creating associated token account (Attempt {ata_attempt + 1})..." + ) create_ata_ix = spl_token.create_associated_token_account( - payer=payer.pubkey(), - owner=payer.pubkey(), - mint=mint + payer=payer.pubkey(), owner=payer.pubkey(), mint=mint ) msg = Message([create_ata_ix], payer.pubkey()) tx_ata = await client.send_transaction( - Transaction([payer], msg, (await client.get_latest_blockhash()).value.blockhash), - opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed) - ) - - await client.confirm_transaction(tx_ata.value, commitment="confirmed") + Transaction( + [payer], + msg, + (await client.get_latest_blockhash()).value.blockhash, + ), + opts=TxOpts( + skip_preflight=True, preflight_commitment=Confirmed + ), + ) + + await client.confirm_transaction( + tx_ata.value, commitment="confirmed" + ) print("Associated token account created.") - print(f"Associated token account address: {associated_token_account}") + print( + f"Associated token account address: {associated_token_account}" + ) break else: print("Associated token account already exists.") - print(f"Associated token account address: {associated_token_account}") + print( + f"Associated token account address: {associated_token_account}" + ) break except Exception as e: - print(f"Attempt {ata_attempt + 1} to create associated token account failed: {str(e)}") + print( + f"Attempt {ata_attempt + 1} to create associated token account failed: {str(e)}" + ) if ata_attempt < max_retries - 1: - wait_time = 2 ** ata_attempt + wait_time = 2**ata_attempt print(f"Retrying in {wait_time} seconds...") await asyncio.sleep(wait_time) else: - print("Max retries reached. Unable to create associated token account.") + print( + "Max retries reached. Unable to create associated token account." + ) return # Continue with the buy transaction @@ -135,28 +167,58 @@ async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curv AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), - AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + AccountMeta( + pubkey=bonding_curve, is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=associated_bonding_curve, + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=associated_token_account, + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=payer.pubkey(), is_signer=True, is_writable=True + ), + AccountMeta( + pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False + ), AccountMeta(pubkey=SYSTEM_RENT, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), + AccountMeta( + pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False + ), ] discriminator = struct.pack(" None: parsed = self._STRUCT.parse(data[8:]) self.__dict__.update(parsed) -async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState: + +async def get_pump_curve_state( + conn: AsyncClient, curve_address: Pubkey +) -> BondingCurveState: response = await conn.get_account_info(curve_address) if not response.value or not response.value.data: raise ValueError("Invalid curve state: No data") @@ -61,11 +70,15 @@ async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> Bond 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) + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( + curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS + ) + async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey): response = await conn.get_token_account_balance(associated_token_account) @@ -73,13 +86,20 @@ async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey) return int(response.value.amount) return 0 -async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, slippage: float = 0.25, max_retries=5): + +async def sell_token( + mint: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + slippage: float = 0.25, + max_retries=5, +): private_key = base58.b58decode("SOLANA_PRIVATE_KEY") payer = Keypair.from_bytes(private_key) async with AsyncClient(RPC_ENDPOINT) as client: associated_token_account = get_associated_token_address(payer.pubkey(), mint) - + # Get token balance token_balance = await get_token_balance(client, associated_token_account) token_balance_decimal = token_balance / 10**TOKEN_DECIMALS @@ -98,7 +118,7 @@ async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_cur min_sol_output = float(token_balance_decimal) * float(token_price_sol) slippage_factor = 1 - slippage min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL) - + print(f"Selling {token_balance_decimal} tokens") print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") @@ -109,19 +129,47 @@ async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_cur AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), - AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), + AccountMeta( + pubkey=bonding_curve, is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=associated_bonding_curve, + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=associated_token_account, + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=payer.pubkey(), is_signer=True, is_writable=True + ), + AccountMeta( + pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, + is_signer=False, + is_writable=False, + ), + AccountMeta( + pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False + ), ] discriminator = struct.pack("=2.1.1 borsh-construct>=0.1.0 -construct>=2.10.68 -construct-typing>=0.5.6 -solana==0.34.3 -solders>=0.21.0 -websockets>=10.4 +construct>=2.10.67 +construct-typing>=0.5.2 +solana==0.36.6 +solders>=0.26.0 +websockets>=15.0 +python-dotenv>=1.0.1 \ No newline at end of file diff --git a/sell.py b/sell.py index f35efb2..9c57e20 100644 --- a/sell.py +++ b/sell.py @@ -1,24 +1,21 @@ import asyncio -import json import base64 +import json import struct -import base58 from typing import Final +import base58 +import spl.token.instructions as spl_token +from construct import Flag, Int64ul, Struct from solana.rpc.async_api import AsyncClient -from solana.transaction import Transaction from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts - -from solders.pubkey import Pubkey +from solana.transaction import Transaction +from solders.instruction import AccountMeta, Instruction from solders.keypair import Keypair -from solders.instruction import Instruction, AccountMeta +from solders.pubkey import Pubkey from solders.system_program import TransferParams, transfer - from spl.token.instructions import get_associated_token_address -import spl.token.instructions as spl_token - -from construct import Struct, Int64ul, Flag from config import * @@ -26,6 +23,7 @@ from config import * EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" None: parsed = self._STRUCT.parse(data[8:]) self.__dict__.update(parsed) -async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState: + +async def get_pump_curve_state( + conn: AsyncClient, curve_address: Pubkey +) -> BondingCurveState: response = await conn.get_account_info(curve_address) if not response.value or not response.value.data: raise ValueError("Invalid curve state: No data") @@ -51,11 +52,15 @@ async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> Bond 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) + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( + curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS + ) + async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey): response = await conn.get_token_account_balance(associated_token_account) @@ -63,13 +68,20 @@ async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey) return int(response.value.amount) return 0 -async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, slippage: float = 0.25, max_retries=5): + +async def sell_token( + mint: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + slippage: float = 0.25, + max_retries=5, +): private_key = base58.b58decode(PRIVATE_KEY) payer = Keypair.from_bytes(private_key) async with AsyncClient(RPC_ENDPOINT) as client: associated_token_account = get_associated_token_address(payer.pubkey(), mint) - + # Get token balance token_balance = await get_token_balance(client, associated_token_account) token_balance_decimal = token_balance / 10**TOKEN_DECIMALS @@ -88,7 +100,7 @@ async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_cur min_sol_output = float(token_balance_decimal) * float(token_price_sol) slippage_factor = 1 - slippage min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL) - + print(f"Selling {token_balance_decimal} tokens") print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") @@ -98,19 +110,47 @@ async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_cur AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), - AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True), - AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), - AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), + AccountMeta( + pubkey=bonding_curve, is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=associated_bonding_curve, + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=associated_token_account, + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=payer.pubkey(), is_signer=True, is_writable=True + ), + AccountMeta( + pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, + is_signer=False, + is_writable=False, + ), + AccountMeta( + pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False + ), ] discriminator = struct.pack(" Date: Wed, 5 Mar 2025 16:44:55 +0000 Subject: [PATCH 02/65] updated code structure --- buy.py => .archive/buy.py | 1 + sell.py => .archive/sell.py | 0 trade.py => .archive/trade.py | 0 .env.example | 6 +- .vscode/settings.json | 5 + cli.py | 128 +++++++++++++++++ config.py | 53 ++++--- pyproject.toml | 13 +- src/core/__init__.py | 0 src/core/client.py | 162 ++++++++++++++++++++++ src/core/curve.py | 136 ++++++++++++++++++ src/core/pubkeys.py | 51 +++++++ src/core/wallet.py | 55 ++++++++ src/monitoring/__init__.py | 0 src/monitoring/events.py | 168 ++++++++++++++++++++++ src/monitoring/listener.py | 181 ++++++++++++++++++++++++ src/trading/__init__.py | 0 src/trading/base.py | 83 +++++++++++ src/trading/buyer.py | 253 ++++++++++++++++++++++++++++++++++ src/trading/seller.py | 214 ++++++++++++++++++++++++++++ src/trading/trader.py | 215 +++++++++++++++++++++++++++++ src/utils/__init__.py | 0 src/utils/logger.py | 65 +++++++++ 23 files changed, 1764 insertions(+), 25 deletions(-) rename buy.py => .archive/buy.py (99%) rename sell.py => .archive/sell.py (100%) rename trade.py => .archive/trade.py (100%) create mode 100644 cli.py create mode 100644 src/core/__init__.py create mode 100644 src/core/client.py create mode 100644 src/core/curve.py create mode 100644 src/core/pubkeys.py create mode 100644 src/core/wallet.py create mode 100644 src/monitoring/__init__.py create mode 100644 src/monitoring/events.py create mode 100644 src/monitoring/listener.py create mode 100644 src/trading/__init__.py create mode 100644 src/trading/base.py create mode 100644 src/trading/buyer.py create mode 100644 src/trading/seller.py create mode 100644 src/trading/trader.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/logger.py diff --git a/buy.py b/.archive/buy.py similarity index 99% rename from buy.py rename to .archive/buy.py index 4e35ee1..7b78290 100644 --- a/buy.py +++ b/.archive/buy.py @@ -21,6 +21,7 @@ from solders.transaction import VersionedTransaction from spl.token.instructions import get_associated_token_address from config import * +from trade import trade # Here and later all the discriminators are precalculated. See learning-examples/discriminator.py EXPECTED_DISCRIMINATOR = struct.pack(" argparse.Namespace: + """Parse command line arguments. + + Returns: + Parsed arguments + """ + parser = argparse.ArgumentParser(description="Trade tokens on pump.fun.") + parser.add_argument( + "--yolo", action="store_true", help="Run in YOLO mode (continuous trading)" + ) + parser.add_argument( + "--match", + type=str, + help="Only trade tokens with names or symbols matching this string", + ) + parser.add_argument( + "--bro", type=str, help="Only trade tokens created by this user address" + ) + parser.add_argument( + "--marry", action="store_true", help="Only buy tokens, skip selling" + ) + parser.add_argument( + "--amount", + type=float, + help=f"Amount of SOL to spend on each buy (default: {config.BUY_AMOUNT})", + ) + parser.add_argument( + "--buy-slippage", + type=float, + help=f"Buy slippage tolerance (default: {config.BUY_SLIPPAGE})", + ) + parser.add_argument( + "--sell-slippage", + type=float, + help=f"Sell slippage tolerance (default: {config.SELL_SLIPPAGE})", + ) + parser.add_argument("--rpc", type=str, help="Solana RPC endpoint") + parser.add_argument("--wss", type=str, help="Solana WebSocket endpoint") + + parser.add_argument("--wss", type=str, help="Solana WebSocket endpoint") + parser.add_argument( + "--key", + type=str, + help="Solana private key (better to use environment variable)", + ) + parser.add_argument("--log", type=str, help="Log file path") + return parser.parse_args() + + +async def main() -> None: + """Main entry point for the CLI.""" + setup_file_logging("pump_trading.log") + + args = parse_args() + + # Get configuration values, preferring command line args over config.py + rpc_endpoint: str | None = args.rpc or os.environ.get("SOLANA_NODE_RPC_ENDPOINT") + wss_endpoint: str | None = args.wss or os.environ.get("SOLANA_NODE_WSS_ENDPOINT") + private_key: str | None = args.key or os.environ.get("SOLANA_PRIVATE_KEY") + + if not rpc_endpoint or rpc_endpoint.startswith(("http://", "https://")): + logger.error("Invalid RPC endpoint. Must start with http:// or https://") + sys.exit(1) + + if not wss_endpoint or wss_endpoint.startswith(("ws://", "wss://")): + logger.error("Invalid WebSocket endpoint. Must start with ws:// or wss://") + sys.exit(1) + + if not private_key or len(private_key) < 80: + logger.error("Invalid private key. Key appears to be missing or too short") + sys.exit(1) + + buy_amount: float = args.amount if args.amount is not None else config.BUY_AMOUNT + buy_slippage: float = ( + args.buy_slippage if args.buy_slippage is not None else config.BUY_SLIPPAGE + ) + sell_slippage: float = ( + args.sell_slippage if args.sell_slippage is not None else config.SELL_SLIPPAGE + ) + + trader: PumpTrader = PumpTrader( + rpc_endpoint=rpc_endpoint, # type: ignore + wss_endpoint=wss_endpoint, # type: ignore + private_key=private_key, + buy_amount=buy_amount, + buy_slippage=buy_slippage, + sell_slippage=sell_slippage, + max_retries=config.MAX_RETRIES, + ) + + try: + await trader.start( + match_string=args.match, + bro_address=args.bro, + marry_mode=args.marry, + yolo_mode=args.yolo, + ) + except KeyboardInterrupt: + logger.info("Trading stopped by user") + except Exception as e: + logger.error(f"Trading stopped due to error: {str(e)}") + finally: + try: + await trader.solana_client.close() + except Exception: + pass + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/config.py b/config.py index 5d0c72c..677a551 100644 --- a/config.py +++ b/config.py @@ -1,3 +1,9 @@ +""" +Configuration for the pump.fun trading bot. +""" + +from typing import Final + import os from dotenv import load_dotenv @@ -6,23 +12,33 @@ from solders.pubkey import Pubkey load_dotenv() # System & pump.fun addresses -PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") -PUMP_GLOBAL = Pubkey.from_string("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf") -PUMP_EVENT_AUTHORITY = Pubkey.from_string( +PUMP_PROGRAM: Final[Pubkey] = Pubkey.from_string( + "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" +) +PUMP_GLOBAL: Final[Pubkey] = Pubkey.from_string( + "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf" +) +PUMP_EVENT_AUTHORITY: Final[Pubkey] = Pubkey.from_string( "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1" ) -PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM") -PUMP_LIQUIDITY_MIGRATOR = Pubkey.from_string( +PUMP_FEE: Final[Pubkey] = Pubkey.from_string( + "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM" +) +PUMP_LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string( "39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg" ) -SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") -SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") -SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string( +SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111") +SYSTEM_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" +) +SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM: Final[Pubkey] = Pubkey.from_string( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" ) -SYSTEM_RENT = Pubkey.from_string("SysvarRent111111111111111111111111111111111") -SOL = Pubkey.from_string("So11111111111111111111111111111111111111112") -LAMPORTS_PER_SOL = 1_000_000_000 +SYSTEM_RENT: Final[Pubkey] = Pubkey.from_string( + "SysvarRent111111111111111111111111111111111" +) +SOL: Final[Pubkey] = Pubkey.from_string("So11111111111111111111111111111111111111112") +LAMPORTS_PER_SOL: Final[int] = 1_000_000_000 # Trading parameters BUY_AMOUNT = 0.0001 # Amount of SOL to spend when buying @@ -33,11 +49,12 @@ ENABLE_DYNAMIC_PRIORITY_FEE = ( ) EXTRA_PRIORITY_FEE = 0.1 # 10% increase in dynamic priority fee -# Your nodes -# You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes -RPC_ENDPOINT = os.getenv("RPC_ENDPOINT") -WSS_ENDPOINT = os.getenv("WSS_ENDPOINT") -MAX_RPS = 25 # RPS of your node to avoid 429 errors +TOKEN_DECIMALS: Final[int] = 6 +MAX_RETRIES: int = 5 +WAIT_TIME_AFTER_BUY: int = 20 +WAIT_TIME_BEFORE_NEW_TOKEN: int = 5 +WAIT_TIME_AFTER_CREATION: int = 15 -# Private key -PRIVATE_KEY = os.getenv("PRIVATE_KEY") +# RPS of your node to avoid rate limit errors +# You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes +MAX_RPS = 25 diff --git a/pyproject.toml b/pyproject.toml index d873eca..15adee2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,11 @@ -[tool.black] +[tool.ruff] line-length = 88 -target-version = ['py39'] +target-version = "py311" -[tool.isort] -profile = "black" \ No newline at end of file +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "N", "B", "A", "C4", "T10", "ARG", "PTH"] +ignore = ["E501"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" \ No newline at end of file diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/client.py b/src/core/client.py new file mode 100644 index 0000000..4f0335e --- /dev/null +++ b/src/core/client.py @@ -0,0 +1,162 @@ +""" +Solana client abstraction for blockchain operations. +""" + +import asyncio +from typing import Any, Dict, List, Optional, Tuple, Union + +from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Confirmed +from solana.rpc.types import TxOpts +from solana.transaction import Transaction +from solders.instruction import Instruction +from solders.keypair import Keypair +from solders.pubkey import Pubkey + +from src.utils.logger import get_logger + +logger = get_logger(__name__) + + +class SolanaClient: + """Abstraction for Solana RPC client operations.""" + + def __init__(self, rpc_endpoint: str): + """Initialize Solana client with RPC endpoint. + + Args: + rpc_endpoint: URL of the Solana RPC endpoint + """ + self.rpc_endpoint = rpc_endpoint + self._client = None + + async def get_client(self) -> AsyncClient: + """Get or create the AsyncClient instance. + + Returns: + AsyncClient instance + """ + if self._client is None: + self._client = AsyncClient(self.rpc_endpoint) + return self._client + + async def close(self): + """Close the client connection if open.""" + if self._client: + await self._client.close() + self._client = None + + async def get_account_info(self, pubkey: Pubkey) -> Dict[str, Any]: + """Get account info from the blockchain. + + Args: + pubkey: Public key of the account + + Returns: + Account info response + + Raises: + ValueError: If account doesn't exist or has no data + """ + client = await self.get_client() + response = await client.get_account_info(pubkey) + if not response.value: + raise ValueError(f"Account {pubkey} not found") + return response.value + + async def get_token_account_balance(self, token_account: Pubkey) -> int: + """Get token balance for an account. + + Args: + token_account: Token account address + + Returns: + Token balance as integer + """ + client = await self.get_client() + response = await client.get_token_account_balance(token_account) + if response.value: + return int(response.value.amount) + return 0 + + async def get_latest_blockhash(self) -> str: + """Get the latest blockhash. + + Returns: + Recent blockhash as string + """ + client = await self.get_client() + response = await client.get_latest_blockhash() + return response.value.blockhash + + async def send_transaction( + self, + transaction: Transaction, + signer: Keypair, + skip_preflight: bool = True, + max_retries: int = 3, + ) -> str: + """Send a transaction to the network. + + Args: + transaction: Prepared transaction + signer: Transaction signer + skip_preflight: Whether to skip preflight checks + max_retries: Maximum number of sending attempts + + Returns: + Transaction signature + + Raises: + Exception: If transaction fails after all retries + """ + client = await self.get_client() + + # Ensure transaction has a recent blockhash + if not transaction.recent_blockhash: + blockhash = await self.get_latest_blockhash() + transaction.recent_blockhash = blockhash + + # Attempt to send with retries + for attempt in range(max_retries): + try: + tx_opts = TxOpts( + skip_preflight=skip_preflight, preflight_commitment=Confirmed + ) + response = await client.send_transaction( + transaction, signer, opts=tx_opts + ) + return response.value + + except Exception as e: + if attempt == max_retries - 1: + logger.error( + f"Failed to send transaction after {max_retries} attempts" + ) + raise + + wait_time = 2**attempt + logger.warning( + f"Transaction attempt {attempt + 1} failed: {str(e)}, retrying in {wait_time}s" + ) + await asyncio.sleep(wait_time) + + async def confirm_transaction( + self, signature: str, commitment: str = "confirmed" + ) -> bool: + """Wait for transaction confirmation. + + Args: + signature: Transaction signature + commitment: Confirmation commitment level + + Returns: + Whether transaction was confirmed + """ + client = await self.get_client() + try: + await client.confirm_transaction(signature, commitment=commitment) + return True + except Exception as e: + logger.error(f"Failed to confirm transaction {signature}: {str(e)}") + return False diff --git a/src/core/curve.py b/src/core/curve.py new file mode 100644 index 0000000..efca883 --- /dev/null +++ b/src/core/curve.py @@ -0,0 +1,136 @@ +""" +Bonding curve operations for pump.fun tokens. +""" + +import struct +from typing import Final + +from construct import Flag, Int64ul, Struct +from solana.rpc.async_api import AsyncClient +from solders.pubkey import Pubkey + +from src.core.client import SolanaClient +from src.core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS +from src.utils.logger import get_logger + +logger = get_logger(__name__) + +# Discriminator for the bonding curve account +EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" None: + """Parse bonding curve data. + + Args: + data: Raw account data + + Raises: + ValueError: If data cannot be parsed + """ + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + + parsed = self._STRUCT.parse(data[8:]) + self.__dict__.update(parsed) + + def calculate_price(self) -> float: + """Calculate token price in SOL. + + Returns: + Token price in SOL + + Raises: + ValueError: If reserve state is invalid + """ + if self.virtual_token_reserves <= 0 or self.virtual_sol_reserves <= 0: + raise ValueError("Invalid reserve state") + + return (self.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( + self.virtual_token_reserves / 10**TOKEN_DECIMALS + ) + + @property + def token_reserves(self) -> float: + """Get token reserves in decimal form.""" + return self.virtual_token_reserves / 10**TOKEN_DECIMALS + + @property + def sol_reserves(self) -> float: + """Get SOL reserves in decimal form.""" + return self.virtual_sol_reserves / LAMPORTS_PER_SOL + + +class BondingCurveManager: + """Manager for bonding curve operations.""" + + def __init__(self, client: SolanaClient): + """Initialize with Solana client. + + Args: + client: Solana client for RPC calls + """ + self.client = client + + async def get_curve_state(self, curve_address: Pubkey) -> BondingCurveState: + """Get the state of a bonding curve. + + Args: + curve_address: Address of the bonding curve account + + Returns: + Bonding curve state + + Raises: + ValueError: If curve data is invalid + """ + try: + account = await self.client.get_account_info(curve_address) + if not account.data: + raise ValueError(f"No data in bonding curve account {curve_address}") + + return BondingCurveState(account.data) + + except Exception as e: + logger.error(f"Failed to get curve state: {str(e)}") + raise ValueError(f"Invalid curve state: {str(e)}") + + async def calculate_price(self, curve_address: Pubkey) -> float: + """Calculate the current price of a token. + + Args: + curve_address: Address of the bonding curve account + + Returns: + Token price in SOL + """ + curve_state = await self.get_curve_state(curve_address) + return curve_state.calculate_price() + + async def calculate_expected_tokens( + self, curve_address: Pubkey, sol_amount: float + ) -> float: + """Calculate the expected token amount for a given SOL input. + + Args: + curve_address: Address of the bonding curve account + sol_amount: Amount of SOL to spend + + Returns: + Expected token amount + """ + curve_state = await self.get_curve_state(curve_address) + price = curve_state.calculate_price() + return sol_amount / price diff --git a/src/core/pubkeys.py b/src/core/pubkeys.py new file mode 100644 index 0000000..7790ef7 --- /dev/null +++ b/src/core/pubkeys.py @@ -0,0 +1,51 @@ +""" +System and program addresses for Solana and pump.fun interactions. +""" + +from dataclasses import dataclass +from typing import Final + +from solders.pubkey import Pubkey + +LAMPORTS_PER_SOL: Final[int] = 1_000_000_000 +TOKEN_DECIMALS: Final[int] = 6 + + +@dataclass +class SystemAddresses: + """System-level Solana addresses.""" + + PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111") + TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + ) + ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + ) + RENT: Final[Pubkey] = Pubkey.from_string( + "SysvarRent111111111111111111111111111111111" + ) + SOL: Final[Pubkey] = Pubkey.from_string( + "So11111111111111111111111111111111111111112" + ) + + +@dataclass +class PumpAddresses: + """Pump.fun program addresses.""" + + PROGRAM: Final[Pubkey] = Pubkey.from_string( + "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + ) + GLOBAL: Final[Pubkey] = Pubkey.from_string( + "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf" + ) + EVENT_AUTHORITY: Final[Pubkey] = Pubkey.from_string( + "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1" + ) + FEE: Final[Pubkey] = Pubkey.from_string( + "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM" + ) + LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string( + "39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg" + ) diff --git a/src/core/wallet.py b/src/core/wallet.py new file mode 100644 index 0000000..18c5956 --- /dev/null +++ b/src/core/wallet.py @@ -0,0 +1,55 @@ +""" +Wallet management for Solana transactions. +""" + +import base58 +from solders.keypair import Keypair +from solders.pubkey import Pubkey +from spl.token.instructions import get_associated_token_address + + +class Wallet: + """Manages a Solana wallet for trading operations.""" + + def __init__(self, private_key: str): + """Initialize wallet from private key. + + Args: + private_key: Base58 encoded private key + """ + self._private_key = private_key + self._keypair = self._load_keypair(private_key) + + @property + def pubkey(self) -> Pubkey: + """Get the public key of the wallet.""" + return self._keypair.pubkey() + + @property + def keypair(self) -> Keypair: + """Get the keypair for signing transactions.""" + return self._keypair + + def get_associated_token_address(self, mint: Pubkey) -> Pubkey: + """Get the associated token account address for a mint. + + Args: + mint: Token mint address + + Returns: + Associated token account address + """ + return get_associated_token_address(self.pubkey, mint) + + @staticmethod + def _load_keypair(private_key: str) -> Keypair: + """Load keypair from private key. + + Args: + private_key: Base58 encoded private key + + Returns: + Solana keypair + """ + private_key_bytes = base58.b58decode(private_key) + return Keypair.from_bytes(private_key_bytes) diff --git a/src/monitoring/__init__.py b/src/monitoring/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/monitoring/events.py b/src/monitoring/events.py new file mode 100644 index 0000000..d23c52d --- /dev/null +++ b/src/monitoring/events.py @@ -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(" 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(" 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 diff --git a/src/trading/__init__.py b/src/trading/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/trading/base.py b/src/trading/base.py new file mode 100644 index 0000000..e6fd6b5 --- /dev/null +++ b/src/trading/base.py @@ -0,0 +1,83 @@ +""" +Base interfaces for trading operations. +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple + +from solders.pubkey import Pubkey +from solders.signature import Signature + + +@dataclass +class TokenInfo: + """Token information.""" + + name: str + symbol: str + uri: str + mint: Pubkey + bonding_curve: Pubkey + associated_bonding_curve: Pubkey + user: Pubkey + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "TokenInfo": + """Create TokenInfo from dictionary. + + Args: + data: Dictionary with token data + + Returns: + TokenInfo instance + """ + return cls( + name=data["name"], + symbol=data["symbol"], + uri=data["uri"], + mint=Pubkey.from_string(data["mint"]), + bonding_curve=Pubkey.from_string(data["bondingCurve"]), + associated_bonding_curve=Pubkey.from_string(data["associatedBondingCurve"]), + user=Pubkey.from_string(data["user"]), + ) + + def to_dict(self) -> Dict[str, str]: + """Convert to dictionary. + + Returns: + Dictionary representation + """ + return { + "name": self.name, + "symbol": self.symbol, + "uri": self.uri, + "mint": str(self.mint), + "bondingCurve": str(self.bonding_curve), + "associatedBondingCurve": str(self.associated_bonding_curve), + "user": str(self.user), + } + + +@dataclass +class TradeResult: + """Result of a trading operation.""" + + success: bool + tx_signature: Optional[str] = None + error_message: Optional[str] = None + amount: Optional[float] = None + price: Optional[float] = None + + +class Trader(ABC): + """Base interface for trading operations.""" + + @abstractmethod + async def execute(self, *args, **kwargs) -> TradeResult: + """Execute trading operation. + + Returns: + TradeResult with operation outcome + """ + pass diff --git a/src/trading/buyer.py b/src/trading/buyer.py new file mode 100644 index 0000000..52dd329 --- /dev/null +++ b/src/trading/buyer.py @@ -0,0 +1,253 @@ +""" +Buy operations for pump.fun tokens. +""" + +import asyncio +import struct +from typing import List, Optional + +import spl.token.instructions as spl_token +from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Confirmed +from solana.rpc.types import TxOpts +from solana.transaction import Transaction +from solders.instruction import AccountMeta, Instruction +from solders.keypair import Keypair +from solders.pubkey import Pubkey +from solders.system_program import TransferParams, transfer +from spl.token.instructions import get_associated_token_address + +from src.core.client import SolanaClient +from src.core.curve import BondingCurveManager +from src.core.pubkeys import ( + LAMPORTS_PER_SOL, + TOKEN_DECIMALS, + PumpAddresses, + SystemAddresses, +) +from src.core.wallet import Wallet +from src.trading.base import TokenInfo, Trader, TradeResult +from src.utils.logger import get_logger + +logger = get_logger(__name__) + + +class TokenBuyer(Trader): + """Handles buying tokens on pump.fun.""" + + def __init__( + self, + client: SolanaClient, + wallet: Wallet, + curve_manager: BondingCurveManager, + amount: float, + slippage: float = 0.01, + max_retries: int = 5, + ): + """Initialize token buyer. + + Args: + client: Solana client for RPC calls + wallet: Wallet for signing transactions + curve_manager: Bonding curve manager + amount: Amount of SOL to spend + slippage: Slippage tolerance (0.01 = 1%) + max_retries: Maximum number of retry attempts + """ + self.client = client + self.wallet = wallet + self.curve_manager = curve_manager + self.amount = amount + self.slippage = slippage + self.max_retries = max_retries + + async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult: + """Execute buy operation. + + Args: + token_info: Token information + + Returns: + TradeResult with buy outcome + """ + try: + # Extract token info + mint = token_info.mint + bonding_curve = token_info.bonding_curve + associated_bonding_curve = token_info.associated_bonding_curve + + # Convert amount to lamports + amount_lamports = int(self.amount * LAMPORTS_PER_SOL) + + # Fetch token price + curve_state = await self.curve_manager.get_curve_state(bonding_curve) + token_price_sol = curve_state.calculate_price() + token_amount = self.amount / token_price_sol + + # Calculate maximum SOL to spend with slippage + max_amount_lamports = int(amount_lamports * (1 + self.slippage)) + + logger.info( + f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token" + ) + logger.info( + f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)" + ) + + associated_token_account = self.wallet.get_associated_token_address(mint) + + await self._ensure_associated_token_account(mint, associated_token_account) + + tx_signature = await self._send_buy_transaction( + mint, + bonding_curve, + associated_bonding_curve, + associated_token_account, + token_amount, + max_amount_lamports, + ) + + success = await self.client.confirm_transaction(tx_signature) + + if success: + logger.info(f"Buy transaction confirmed: {tx_signature}") + return TradeResult( + success=True, + tx_signature=tx_signature, + amount=token_amount, + price=token_price_sol, + ) + else: + return TradeResult( + success=False, + error_message=f"Transaction failed to confirm: {tx_signature}", + ) + + except Exception as e: + logger.error(f"Buy operation failed: {str(e)}") + return TradeResult(success=False, error_message=str(e)) + + async def _ensure_associated_token_account( + self, mint: Pubkey, associated_token_account: Pubkey + ) -> None: + """Ensure associated token account exists. + + Args: + mint: Token mint + associated_token_account: Associated token account address + """ + try: + solana_client = await self.client.get_client() + account_info = await solana_client.get_account_info( + associated_token_account + ) + + if account_info.value is None: + logger.info(f"Creating associated token account for {mint}...") + + create_ata_ix = spl_token.create_associated_token_account( + payer=self.wallet.pubkey, owner=self.wallet.pubkey, mint=mint + ) + + create_ata_tx = Transaction() + create_ata_tx.add(create_ata_ix) + blockhash = await self.client.get_latest_blockhash() + create_ata_tx.recent_blockhash = blockhash + + tx_sig = await self.client.send_transaction( + create_ata_tx, self.wallet.keypair + ) + + await self.client.confirm_transaction(tx_sig) + logger.info( + f"Associated token account created: {associated_token_account}" + ) + else: + logger.info( + f"Associated token account already exists: {associated_token_account}" + ) + + except Exception as e: + logger.error(f"Error creating associated token account: {str(e)}") + raise + + async def _send_buy_transaction( + self, + mint: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + associated_token_account: Pubkey, + token_amount: float, + max_amount_lamports: int, + ) -> str: + """Send buy transaction. + + Args: + mint: Token mint + bonding_curve: Bonding curve address + associated_bonding_curve: Associated bonding curve address + associated_token_account: User's token account + token_amount: Amount of tokens to buy + max_amount_lamports: Maximum SOL to spend in lamports + + Returns: + Transaction signature + + Raises: + Exception: If transaction fails after all retries + """ + accounts = [ + AccountMeta( + pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False + ), + AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta( + pubkey=associated_bonding_curve, is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=associated_token_account, is_signer=False, is_writable=True + ), + AccountMeta(pubkey=self.wallet.pubkey, is_signer=True, is_writable=True), + AccountMeta( + pubkey=SystemAddresses.PROGRAM, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=PumpAddresses.EVENT_AUTHORITY, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=PumpAddresses.PROGRAM, is_signer=False, is_writable=False + ), + ] + + # Prepare buy instruction data + # Discriminator for buy instruction + discriminator = struct.pack(" TradeResult: + """Execute sell operation. + + Args: + token_info: Token information + + Returns: + TradeResult with sell outcome + """ + try: + # Extract token info + mint = token_info.mint + bonding_curve = token_info.bonding_curve + associated_bonding_curve = token_info.associated_bonding_curve + + # Get associated token account + associated_token_account = self.wallet.get_associated_token_address(mint) + + # Get token balance + token_balance = await self.client.get_token_account_balance( + associated_token_account + ) + token_balance_decimal = token_balance / 10**TOKEN_DECIMALS + + logger.info(f"Token balance: {token_balance_decimal}") + + if token_balance == 0: + logger.info("No tokens to sell.") + return TradeResult(success=False, error_message="No tokens to sell") + + # Fetch token price + curve_state = await self.curve_manager.get_curve_state(bonding_curve) + token_price_sol = curve_state.calculate_price() + + logger.info(f"Price per Token: {token_price_sol:.8f} SOL") + + # Calculate minimum SOL output with slippage + amount = token_balance + expected_sol_output = float(token_balance_decimal) * float(token_price_sol) + slippage_factor = 1 - self.slippage + min_sol_output = int( + (expected_sol_output * slippage_factor) * LAMPORTS_PER_SOL + ) + + logger.info(f"Selling {token_balance_decimal} tokens") + logger.info(f"Expected SOL output: {expected_sol_output:.8f} SOL") + logger.info( + f"Minimum SOL output (with {self.slippage * 100}% slippage): {min_sol_output / LAMPORTS_PER_SOL:.8f} SOL" + ) + + tx_signature = await self._send_sell_transaction( + mint, + bonding_curve, + associated_bonding_curve, + associated_token_account, + amount, + min_sol_output, + ) + + success = await self.client.confirm_transaction(tx_signature) + + if success: + logger.info(f"Sell transaction confirmed: {tx_signature}") + return TradeResult( + success=True, + tx_signature=tx_signature, + amount=token_balance_decimal, + price=token_price_sol, + ) + else: + return TradeResult( + success=False, + error_message=f"Transaction failed to confirm: {tx_signature}", + ) + + except Exception as e: + logger.error(f"Sell operation failed: {str(e)}") + return TradeResult(success=False, error_message=str(e)) + + async def _send_sell_transaction( + self, + mint: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + associated_token_account: Pubkey, + token_amount: int, + min_sol_output: int, + ) -> str: + """Send sell transaction. + + Args: + mint: Token mint + bonding_curve: Bonding curve address + associated_bonding_curve: Associated bonding curve address + associated_token_account: User's token account + token_amount: Amount of tokens to sell in raw units + min_sol_output: Minimum SOL to receive in lamports + + Returns: + Transaction signature + + Raises: + Exception: If transaction fails after all retries + """ + # Prepare sell instruction accounts + accounts = [ + AccountMeta( + pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False + ), + AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta( + pubkey=associated_bonding_curve, is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=associated_token_account, is_signer=False, is_writable=True + ), + AccountMeta(pubkey=self.wallet.pubkey, is_signer=True, is_writable=True), + AccountMeta( + pubkey=SystemAddresses.PROGRAM, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, + is_signer=False, + is_writable=False, + ), + AccountMeta( + pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=PumpAddresses.EVENT_AUTHORITY, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=PumpAddresses.PROGRAM, is_signer=False, is_writable=False + ), + ] + + # Prepare sell instruction data + # Discriminator for sell instruction + discriminator = struct.pack(" None: + """Start the trading bot. + + Args: + match_string: Optional string to match in token name/symbol + bro_address: Optional creator address to filter by + marry_mode: If True, only buy tokens and skip selling + yolo_mode: If True, trade continuously + """ + logger.info("Starting pump.fun trader") + logger.info(f"Match filter: {match_string if match_string else 'None'}") + logger.info(f"Creator filter: {bro_address if bro_address else 'None'}") + logger.info(f"Marry mode: {marry_mode}") + logger.info(f"YOLO mode: {yolo_mode}") + + try: + await self.token_listener.listen_for_tokens( + lambda token: self._handle_new_token(token, marry_mode, yolo_mode), + match_string, + bro_address, + ) + + except Exception as e: + logger.error(f"Trading stopped due to error: {str(e)}") + await self.solana_client.close() + + async def _handle_new_token( + self, token_info: TokenInfo, marry_mode: bool, yolo_mode: bool + ) -> None: + """Handle a new token creation event. + + Args: + token_info: Token information + marry_mode: If True, only buy tokens and skip selling + yolo_mode: If True, continue trading after this token + """ + try: + await self._save_token_info(token_info) + + logger.info("Waiting for 15 seconds for the bonding curve to stabilize...") + await asyncio.sleep(15) + + try: + token_price = await self.curve_manager.calculate_price( + token_info.bonding_curve + ) + logger.info(f"Token price: {token_price:.10f} SOL") + except Exception as e: + logger.error(f"Failed to get token price: {str(e)}") + token_price = 0 + + logger.info( + f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol}..." + ) + buy_result = await self.buyer.execute(token_info) + + if buy_result.success: + logger.info(f"Successfully bought {token_info.symbol}") + self._log_trade("buy", token_info, token_price, buy_result.tx_signature) + else: + logger.error( + f"Failed to buy {token_info.symbol}: {buy_result.error_message}" + ) + + # Sell token if not in marry mode + if not marry_mode and buy_result.success: + logger.info("Waiting for 20 seconds before selling...") + await asyncio.sleep(20) + + logger.info(f"Selling {token_info.symbol}...") + sell_result = await self.seller.execute(token_info) + + if sell_result.success: + logger.info(f"Successfully sold {token_info.symbol}") + self._log_trade( + "sell", token_info, token_price, sell_result.tx_signature + ) + else: + logger.error( + f"Failed to sell {token_info.symbol}: {sell_result.error_message}" + ) + elif marry_mode: + logger.info("Marry mode enabled. Skipping sell operation.") + + # Wait before looking for the next token + if yolo_mode: + logger.info( + "YOLO mode enabled. Waiting 5 seconds before looking for next token..." + ) + await asyncio.sleep(5) + + except Exception as e: + logger.error(f"Error handling token {token_info.symbol}: {str(e)}") + + async def _save_token_info(self, token_info: TokenInfo) -> None: + """Save token information to a file. + + Args: + token_info: Token information + """ + os.makedirs("trades", exist_ok=True) + file_name = os.path.join("trades", f"{token_info.mint}.txt") + + with open(file_name, "w") as file: + file.write(json.dumps(token_info.to_dict(), indent=2)) + + logger.info(f"Token information saved to {file_name}") + + def _log_trade( + self, action: str, token_info: TokenInfo, price: float, tx_hash: str | None + ) -> None: + """Log trade information. + + Args: + action: Trade action (buy/sell) + token_info: Token information + price: Token price in SOL + tx_hash: Transaction hash + """ + os.makedirs("trades", exist_ok=True) + + log_entry = { + "timestamp": datetime.utcnow().isoformat(), + "action": action, + "token_address": str(token_info.mint), + "symbol": token_info.symbol, + "price": price, + "tx_hash": tx_hash, + } + + with open("trades/trades.log", "a") as log_file: + log_file.write(json.dumps(log_entry) + "\n") diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/logger.py b/src/utils/logger.py new file mode 100644 index 0000000..35dabd2 --- /dev/null +++ b/src/utils/logger.py @@ -0,0 +1,65 @@ +""" +Logging utilities for the pump.fun trading bot. +""" + +import logging +import sys +from typing import Dict + +# Global dict to store loggers +_loggers: Dict[str, logging.Logger] = {} + + +def get_logger(name: str, level: int = logging.INFO) -> logging.Logger: + """Get or create a logger with the given name. + + Args: + name: Logger name, typically __name__ + level: Logging level + + Returns: + Configured logger + """ + global _loggers + + if name in _loggers: + return _loggers[name] + + logger = logging.getLogger(name) + logger.setLevel(level) + + if not logger.handlers: + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + _loggers[name] = logger + return logger + + +def setup_file_logging( + filename: str = "pump_trading.log", level: int = logging.INFO +) -> None: + """Set up file logging for all loggers. + + Args: + filename: Log file path + level: Logging level for file handler + """ + root_logger = logging.getLogger() + + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + file_handler = logging.FileHandler(filename) + file_handler.setLevel(level) + file_handler.setFormatter(formatter) + + root_logger.addHandler(file_handler) From 46a636f91287d0377813d44bcf10f44fbc4f6000 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Wed, 5 Mar 2025 17:04:25 +0000 Subject: [PATCH 03/65] fixed transaction import --- config.py | 11 +++-------- src/core/client.py | 2 +- src/trading/base.py | 8 ++++---- src/trading/buyer.py | 2 +- src/trading/seller.py | 2 +- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/config.py b/config.py index 677a551..2ce3ba1 100644 --- a/config.py +++ b/config.py @@ -4,13 +4,8 @@ Configuration for the pump.fun trading bot. from typing import Final -import os - -from dotenv import load_dotenv from solders.pubkey import Pubkey -load_dotenv() - # System & pump.fun addresses PUMP_PROGRAM: Final[Pubkey] = Pubkey.from_string( "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" @@ -45,9 +40,9 @@ BUY_AMOUNT = 0.0001 # Amount of SOL to spend when buying BUY_SLIPPAGE = 0.2 # 20% slippage tolerance for buying SELL_SLIPPAGE = 0.2 # 20% slippage tolerance for selling ENABLE_DYNAMIC_PRIORITY_FEE = ( - True # getRecentPriorityFee is used to get current priority fee + True # TODO: getRecentPriorityFee is used to get current priority fee ) -EXTRA_PRIORITY_FEE = 0.1 # 10% increase in dynamic priority fee +EXTRA_PRIORITY_FEE = 0.1 # TODO: 10% increase in dynamic priority fee TOKEN_DECIMALS: Final[int] = 6 MAX_RETRIES: int = 5 @@ -55,6 +50,6 @@ WAIT_TIME_AFTER_BUY: int = 20 WAIT_TIME_BEFORE_NEW_TOKEN: int = 5 WAIT_TIME_AFTER_CREATION: int = 15 -# RPS of your node to avoid rate limit errors +# TODO: RPS of your node to avoid rate limit errors # You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes MAX_RPS = 25 diff --git a/src/core/client.py b/src/core/client.py index 4f0335e..9a7b51c 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -8,10 +8,10 @@ from typing import Any, Dict, List, Optional, Tuple, Union from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts -from solana.transaction import Transaction from solders.instruction import Instruction from solders.keypair import Keypair from solders.pubkey import Pubkey +from solders.transaction import Transaction from src.utils.logger import get_logger diff --git a/src/trading/base.py b/src/trading/base.py index e6fd6b5..c4cb7f1 100644 --- a/src/trading/base.py +++ b/src/trading/base.py @@ -64,10 +64,10 @@ class TradeResult: """Result of a trading operation.""" success: bool - tx_signature: Optional[str] = None - error_message: Optional[str] = None - amount: Optional[float] = None - price: Optional[float] = None + tx_signature: str | None = None + error_message: str | None = None + amount: float | None = None + price: float | None = None class Trader(ABC): diff --git a/src/trading/buyer.py b/src/trading/buyer.py index 52dd329..d85aa89 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -10,11 +10,11 @@ import spl.token.instructions as spl_token from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts -from solana.transaction import Transaction from solders.instruction import AccountMeta, Instruction from solders.keypair import Keypair from solders.pubkey import Pubkey from solders.system_program import TransferParams, transfer +from solders.transaction import Transaction from spl.token.instructions import get_associated_token_address from src.core.client import SolanaClient diff --git a/src/trading/seller.py b/src/trading/seller.py index 74585c9..9724b33 100644 --- a/src/trading/seller.py +++ b/src/trading/seller.py @@ -6,9 +6,9 @@ import asyncio import struct from typing import Optional -from solana.transaction import Transaction from solders.instruction import AccountMeta, Instruction from solders.pubkey import Pubkey +from solders.transaction import Transaction from src.core.client import SolanaClient from src.core.curve import BondingCurveManager From 4f800bbbbe4692d56e4b1d0fd972b52c84073a99 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 6 Mar 2025 12:40:40 +0000 Subject: [PATCH 04/65] added listener test --- config.py | 38 ++-------------- setup.py | 8 ++++ src/monitoring/events.py | 8 ++-- src/monitoring/listener.py | 10 ++--- tests/test_listener.py | 89 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 44 deletions(-) create mode 100644 setup.py create mode 100644 tests/test_listener.py diff --git a/config.py b/config.py index 2ce3ba1..518f442 100644 --- a/config.py +++ b/config.py @@ -2,39 +2,6 @@ Configuration for the pump.fun trading bot. """ -from typing import Final - -from solders.pubkey import Pubkey - -# System & pump.fun addresses -PUMP_PROGRAM: Final[Pubkey] = Pubkey.from_string( - "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" -) -PUMP_GLOBAL: Final[Pubkey] = Pubkey.from_string( - "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf" -) -PUMP_EVENT_AUTHORITY: Final[Pubkey] = Pubkey.from_string( - "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1" -) -PUMP_FEE: Final[Pubkey] = Pubkey.from_string( - "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM" -) -PUMP_LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string( - "39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg" -) -SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111") -SYSTEM_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( - "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" -) -SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM: Final[Pubkey] = Pubkey.from_string( - "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" -) -SYSTEM_RENT: Final[Pubkey] = Pubkey.from_string( - "SysvarRent111111111111111111111111111111111" -) -SOL: Final[Pubkey] = Pubkey.from_string("So11111111111111111111111111111111111111112") -LAMPORTS_PER_SOL: Final[int] = 1_000_000_000 - # Trading parameters BUY_AMOUNT = 0.0001 # Amount of SOL to spend when buying BUY_SLIPPAGE = 0.2 # 20% slippage tolerance for buying @@ -44,7 +11,7 @@ ENABLE_DYNAMIC_PRIORITY_FEE = ( ) EXTRA_PRIORITY_FEE = 0.1 # TODO: 10% increase in dynamic priority fee -TOKEN_DECIMALS: Final[int] = 6 +TOKEN_DECIMALS: int = 6 MAX_RETRIES: int = 5 WAIT_TIME_AFTER_BUY: int = 20 WAIT_TIME_BEFORE_NEW_TOKEN: int = 5 @@ -53,3 +20,6 @@ WAIT_TIME_AFTER_CREATION: int = 15 # TODO: RPS of your node to avoid rate limit errors # You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes MAX_RPS = 25 + +PUBLIC_RPC_ENDPOINT = "https://api.mainnet-beta.solana.com" +PUBLIC_WSS_ENDPOINT = "wss://api.mainnet-beta.solana.com" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..28ea38b --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ +from setuptools import find_packages, setup + +setup( + name="pump_bot", + version="0.1", + package_dir={"": "src"}, + packages=find_packages(where="src"), +) diff --git a/src/monitoring/events.py b/src/monitoring/events.py index d23c52d..0069e32 100644 --- a/src/monitoring/events.py +++ b/src/monitoring/events.py @@ -5,13 +5,13 @@ Event processing for pump.fun tokens. import base64 import json import struct -from typing import Any, Callable, Dict, List, Optional +from typing import Any, 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 +from trading.base import TokenInfo +from utils.logger import get_logger logger = get_logger(__name__) @@ -38,7 +38,7 @@ class PumpEventProcessor: IDL as dictionary """ try: - with open("idl/pump_fun_idl.json", "r") as f: + with open("idl/pump_fun_idl.json") as f: return json.load(f) except Exception as e: logger.error(f"Failed to load IDL: {str(e)}") diff --git a/src/monitoring/listener.py b/src/monitoring/listener.py index f679aad..b529243 100644 --- a/src/monitoring/listener.py +++ b/src/monitoring/listener.py @@ -4,15 +4,14 @@ WebSocket monitoring for pump.fun tokens. import asyncio import json -import time -from typing import Any, Awaitable, Callable, Dict, Optional +from typing import Awaitable, Callable, 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 +from monitoring.events import PumpEventProcessor +from trading.base import TokenInfo +from utils.logger import get_logger logger = get_logger(__name__) @@ -61,7 +60,6 @@ class PumpTokenListener: 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() diff --git a/tests/test_listener.py b/tests/test_listener.py new file mode 100644 index 0000000..e729a6e --- /dev/null +++ b/tests/test_listener.py @@ -0,0 +1,89 @@ +""" +Test script for PumpTokenListener +Tests websocket monitoring for new pump.fun tokens +""" + +import asyncio +import logging +import os +from core.pubkeys import PumpAddresses +from monitoring.listener import PumpTokenListener +from trading.base import TokenInfo + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("token-listener-test") + + +class TestTokenCallback: + def __init__(self): + self.detected_tokens = [] + + async def on_token_created(self, token_info: TokenInfo) -> None: + """Process detected token""" + logger.info(f"New token detected: {token_info.name} ({token_info.symbol})") + logger.info(f"Mint: {token_info.mint}") + self.detected_tokens.append(token_info) + print(f"\n{'=' * 50}") + print(f"NEW TOKEN: {token_info.name}") + print(f"Symbol: {token_info.symbol}") + print(f"Mint: {token_info.mint}") + print(f"URI: {token_info.uri}") + print(f"Creator: {token_info.user}") + print(f"Bonding Curve: {token_info.bonding_curve}") + print(f"Associated Bonding Curve: {token_info.associated_bonding_curve}") + print(f"{'=' * 50}\n") + + +async def test_pump_token_listener( + match_string: str | None = None, + creator_address: str | None = None, + test_duration: int = 60, +): + """Test the token listener functionality""" + wss_endpoint = os.environ.get( + "SOLANA_NODE_WSS_ENDPOINT", "wss://api.mainnet-beta.solana.com" + ) + logger.info(f"Connecting to WebSocket: {wss_endpoint}") + listener = PumpTokenListener(wss_endpoint, PumpAddresses.PROGRAM) + callback = TestTokenCallback() + + if match_string: + logger.info(f"Filtering tokens matching: {match_string}") + if creator_address: + logger.info(f"Filtering tokens by creator: {creator_address}") + + listen_task = asyncio.create_task( + listener.listen_for_tokens( + callback.on_token_created, + match_string=match_string, + creator_address=creator_address, + ) + ) + + logger.info(f"Listening for {test_duration} seconds...") + try: + await asyncio.sleep(test_duration) + except KeyboardInterrupt: + logger.info("Test interrupted by user") + finally: + listen_task.cancel() + try: + await listen_task + except asyncio.CancelledError: + pass + + logger.info(f"Detected {len(callback.detected_tokens)} tokens") + for token in callback.detected_tokens: + logger.info(f" - 🪙 {token.name} ({token.symbol}): {token.mint}") + + return callback.detected_tokens + + +if __name__ == "__main__": + match_string = None # Update if you want to filter tokens by name/symbol + creator_address = None # Update if you want to filter tokens by creator address + test_duration = 60 + logger.info("Starting token listener test") + asyncio.run(test_pump_token_listener(match_string, creator_address, test_duration)) From 25c304f0fe97239961a1cc5d000aed35678b8041 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 6 Mar 2025 13:52:40 +0000 Subject: [PATCH 05/65] minor fixes --- cli.py | 20 +++++++++++++-- config.py | 17 ++++++------- src/core/client.py | 3 +-- src/trading/trader.py | 56 +++++++++++++++++++++++++----------------- tests/test_listener.py | 9 ++++--- 5 files changed, 65 insertions(+), 40 deletions(-) diff --git a/cli.py b/cli.py index 8a0feaf..2701092 100644 --- a/cli.py +++ b/cli.py @@ -72,10 +72,19 @@ async def main() -> None: args = parse_args() # Get configuration values, preferring command line args over config.py - rpc_endpoint: str | None = args.rpc or os.environ.get("SOLANA_NODE_RPC_ENDPOINT") - wss_endpoint: str | None = args.wss or os.environ.get("SOLANA_NODE_WSS_ENDPOINT") + rpc_endpoint: str | None = ( + args.rpc + or os.environ.get("SOLANA_NODE_RPC_ENDPOINT") + or config.PUBLIC_RPC_ENDPOINT + ) + wss_endpoint: str | None = ( + args.wss + or os.environ.get("SOLANA_NODE_WSS_ENDPOINT") + or config.PUBLIC_WSS_ENDPOINT + ) private_key: str | None = args.key or os.environ.get("SOLANA_PRIVATE_KEY") + # Validate configuration values if not rpc_endpoint or rpc_endpoint.startswith(("http://", "https://")): logger.error("Invalid RPC endpoint. Must start with http:// or https://") sys.exit(1) @@ -88,6 +97,7 @@ async def main() -> None: logger.error("Invalid private key. Key appears to be missing or too short") sys.exit(1) + # Get trading parameters buy_amount: float = args.amount if args.amount is not None else config.BUY_AMOUNT buy_slippage: float = ( args.buy_slippage if args.buy_slippage is not None else config.BUY_SLIPPAGE @@ -96,6 +106,12 @@ async def main() -> None: args.sell_slippage if args.sell_slippage is not None else config.SELL_SLIPPAGE ) + # Not implemented parameters + enable_dynamic_prior__fee = ( + config.ENABLE_DYNAMIC_PRIORITY_FEE + ) # TODO: to be implemented + prior_fee_multiplier = config.EXTRA_PRIORITY_FEE # TODO: to be implemented + trader: PumpTrader = PumpTrader( rpc_endpoint=rpc_endpoint, # type: ignore wss_endpoint=wss_endpoint, # type: ignore diff --git a/config.py b/config.py index 518f442..36c273d 100644 --- a/config.py +++ b/config.py @@ -6,20 +6,17 @@ Configuration for the pump.fun trading bot. BUY_AMOUNT = 0.0001 # Amount of SOL to spend when buying BUY_SLIPPAGE = 0.2 # 20% slippage tolerance for buying SELL_SLIPPAGE = 0.2 # 20% slippage tolerance for selling -ENABLE_DYNAMIC_PRIORITY_FEE = ( - True # TODO: getRecentPriorityFee is used to get current priority fee -) -EXTRA_PRIORITY_FEE = 0.1 # TODO: 10% increase in dynamic priority fee +ENABLE_DYNAMIC_PRIORITY_FEE = True # TODO: not implemented. getRecentPriorityFee is used to get current priority fee +EXTRA_PRIORITY_FEE = 0.1 # TODO: not implemented. 10% increase in dynamic priority fee -TOKEN_DECIMALS: int = 6 +# Retries and timeouts MAX_RETRIES: int = 5 -WAIT_TIME_AFTER_BUY: int = 20 +WAIT_TIME_AFTER_BUY: int = 5 WAIT_TIME_BEFORE_NEW_TOKEN: int = 5 -WAIT_TIME_AFTER_CREATION: int = 15 +WAIT_TIME_AFTER_CREATION: int = 5 -# TODO: RPS of your node to avoid rate limit errors +# Node provier configuration # You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes -MAX_RPS = 25 - +MAX_RPS = 25 # TODO: not implemented. Max RPS to avoid rate limit errors PUBLIC_RPC_ENDPOINT = "https://api.mainnet-beta.solana.com" PUBLIC_WSS_ENDPOINT = "wss://api.mainnet-beta.solana.com" diff --git a/src/core/client.py b/src/core/client.py index 9a7b51c..7f9ea2c 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -3,12 +3,11 @@ Solana client abstraction for blockchain operations. """ import asyncio -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts -from solders.instruction import Instruction from solders.keypair import Keypair from solders.pubkey import Pubkey from solders.transaction import Transaction diff --git a/src/trading/trader.py b/src/trading/trader.py index 485f866..d56ae61 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -6,10 +6,8 @@ import asyncio import json import os from datetime import datetime -from typing import Any, Dict, List, Optional - -from solders.pubkey import Pubkey +import config from src.core.client import SolanaClient from src.core.curve import BondingCurveManager from src.core.pubkeys import PumpAddresses @@ -120,26 +118,25 @@ class PumpTrader: try: await self._save_token_info(token_info) - logger.info("Waiting for 15 seconds for the bonding curve to stabilize...") - await asyncio.sleep(15) - - try: - token_price = await self.curve_manager.calculate_price( - token_info.bonding_curve - ) - logger.info(f"Token price: {token_price:.10f} SOL") - except Exception as e: - logger.error(f"Failed to get token price: {str(e)}") - token_price = 0 + logger.info( + f"Waiting for {config.WAIT_TIME_AFTER_CREATION} seconds for the bonding curve to stabilize..." + ) + await asyncio.sleep(config.WAIT_TIME_AFTER_CREATION) logger.info( f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol}..." ) - buy_result = await self.buyer.execute(token_info) + buy_result: TradeResult = await self.buyer.execute(token_info) if buy_result.success: logger.info(f"Successfully bought {token_info.symbol}") - self._log_trade("buy", token_info, token_price, buy_result.tx_signature) + self._log_trade( + "buy", + token_info, + buy_result.price, # type: ignore + buy_result.amount, # type: ignore + buy_result.tx_signature, + ) else: logger.error( f"Failed to buy {token_info.symbol}: {buy_result.error_message}" @@ -147,16 +144,22 @@ class PumpTrader: # Sell token if not in marry mode if not marry_mode and buy_result.success: - logger.info("Waiting for 20 seconds before selling...") - await asyncio.sleep(20) + logger.info( + f"Waiting for {config.WAIT_TIME_AFTER_BUY} seconds before selling..." + ) + await asyncio.sleep(config.WAIT_TIME_AFTER_BUY) logger.info(f"Selling {token_info.symbol}...") - sell_result = await self.seller.execute(token_info) + sell_result: TradeResult = await self.seller.execute(token_info) if sell_result.success: logger.info(f"Successfully sold {token_info.symbol}") self._log_trade( - "sell", token_info, token_price, sell_result.tx_signature + "sell", + token_info, + sell_result.price, # type: ignore + sell_result.amount, # type: ignore + sell_result.tx_signature, ) else: logger.error( @@ -168,9 +171,9 @@ class PumpTrader: # Wait before looking for the next token if yolo_mode: logger.info( - "YOLO mode enabled. Waiting 5 seconds before looking for next token..." + f"YOLO mode enabled. Waiting {config.WAIT_TIME_BEFORE_NEW_TOKEN} seconds before looking for next token..." ) - await asyncio.sleep(5) + await asyncio.sleep(config.WAIT_TIME_BEFORE_NEW_TOKEN) except Exception as e: logger.error(f"Error handling token {token_info.symbol}: {str(e)}") @@ -190,7 +193,12 @@ class PumpTrader: logger.info(f"Token information saved to {file_name}") def _log_trade( - self, action: str, token_info: TokenInfo, price: float, tx_hash: str | None + self, + action: str, + token_info: TokenInfo, + price: float, + amount: float, + tx_hash: str | None, ) -> None: """Log trade information. @@ -198,6 +206,7 @@ class PumpTrader: action: Trade action (buy/sell) token_info: Token information price: Token price in SOL + amount: Trade amount in SOL tx_hash: Transaction hash """ os.makedirs("trades", exist_ok=True) @@ -208,6 +217,7 @@ class PumpTrader: "token_address": str(token_info.mint), "symbol": token_info.symbol, "price": price, + "amount": amount, "tx_hash": tx_hash, } diff --git a/tests/test_listener.py b/tests/test_listener.py index e729a6e..13a40d8 100644 --- a/tests/test_listener.py +++ b/tests/test_listener.py @@ -6,6 +6,8 @@ Tests websocket monitoring for new pump.fun tokens import asyncio import logging import os + +import config from core.pubkeys import PumpAddresses from monitoring.listener import PumpTokenListener from trading.base import TokenInfo @@ -43,7 +45,7 @@ async def test_pump_token_listener( ): """Test the token listener functionality""" wss_endpoint = os.environ.get( - "SOLANA_NODE_WSS_ENDPOINT", "wss://api.mainnet-beta.solana.com" + "SOLANA_NODE_WSS_ENDPOINT", config.PUBLIC_WSS_ENDPOINT ) logger.info(f"Connecting to WebSocket: {wss_endpoint}") listener = PumpTokenListener(wss_endpoint, PumpAddresses.PROGRAM) @@ -76,7 +78,7 @@ async def test_pump_token_listener( logger.info(f"Detected {len(callback.detected_tokens)} tokens") for token in callback.detected_tokens: - logger.info(f" - 🪙 {token.name} ({token.symbol}): {token.mint}") + logger.info(f" - {token.name} ({token.symbol}): {token.mint}") return callback.detected_tokens @@ -84,6 +86,7 @@ async def test_pump_token_listener( if __name__ == "__main__": match_string = None # Update if you want to filter tokens by name/symbol creator_address = None # Update if you want to filter tokens by creator address - test_duration = 60 + test_duration = 15 + logger.info("Starting token listener test") asyncio.run(test_pump_token_listener(match_string, creator_address, test_duration)) From a7b2c135f9fc6b19996f5f1dd8801afaf45ca8df Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 6 Mar 2025 16:13:59 +0000 Subject: [PATCH 06/65] fixed buy and sell transactions --- src/trading/trader.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/trading/trader.py b/src/trading/trader.py index d56ae61..6ba6c7f 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -168,13 +168,6 @@ class PumpTrader: elif marry_mode: logger.info("Marry mode enabled. Skipping sell operation.") - # Wait before looking for the next token - if yolo_mode: - logger.info( - f"YOLO mode enabled. Waiting {config.WAIT_TIME_BEFORE_NEW_TOKEN} seconds before looking for next token..." - ) - await asyncio.sleep(config.WAIT_TIME_BEFORE_NEW_TOKEN) - except Exception as e: logger.error(f"Error handling token {token_info.symbol}: {str(e)}") From fce547ec9e142fdd9c50e15d38ce6b9bca2c776c Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 6 Mar 2025 16:14:17 +0000 Subject: [PATCH 07/65] fixed and buy transactions --- .gitignore | 5 ++- cli.py | 23 +++-------- config.py | 14 +++---- src/core/client.py | 14 ++----- src/monitoring/listener.py | 9 ++++- src/trading/buyer.py | 37 +++++++++--------- src/trading/seller.py | 20 ++++++---- src/trading/trader.py | 78 ++++++++++++++++++++++++++++++++++++-- 8 files changed, 133 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index 55804a2..13525c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +trades/* + .pylintrc .ruff_cache @@ -162,4 +164,5 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ +trades/trades.log diff --git a/cli.py b/cli.py index 2701092..5923dde 100644 --- a/cli.py +++ b/cli.py @@ -52,16 +52,7 @@ def parse_args() -> argparse.Namespace: type=float, help=f"Sell slippage tolerance (default: {config.SELL_SLIPPAGE})", ) - parser.add_argument("--rpc", type=str, help="Solana RPC endpoint") - parser.add_argument("--wss", type=str, help="Solana WebSocket endpoint") - parser.add_argument("--wss", type=str, help="Solana WebSocket endpoint") - parser.add_argument( - "--key", - type=str, - help="Solana private key (better to use environment variable)", - ) - parser.add_argument("--log", type=str, help="Log file path") return parser.parse_args() @@ -73,23 +64,19 @@ async def main() -> None: # Get configuration values, preferring command line args over config.py rpc_endpoint: str | None = ( - args.rpc - or os.environ.get("SOLANA_NODE_RPC_ENDPOINT") - or config.PUBLIC_RPC_ENDPOINT + os.environ.get("SOLANA_NODE_RPC_ENDPOINT") or config.PUBLIC_RPC_ENDPOINT ) wss_endpoint: str | None = ( - args.wss - or os.environ.get("SOLANA_NODE_WSS_ENDPOINT") - or config.PUBLIC_WSS_ENDPOINT + os.environ.get("SOLANA_NODE_WSS_ENDPOINT") or config.PUBLIC_WSS_ENDPOINT ) - private_key: str | None = args.key or os.environ.get("SOLANA_PRIVATE_KEY") + private_key: str | None = os.environ.get("SOLANA_PRIVATE_KEY") # Validate configuration values - if not rpc_endpoint or rpc_endpoint.startswith(("http://", "https://")): + if not rpc_endpoint or not rpc_endpoint.startswith(("http://", "https://")): logger.error("Invalid RPC endpoint. Must start with http:// or https://") sys.exit(1) - if not wss_endpoint or wss_endpoint.startswith(("ws://", "wss://")): + if not wss_endpoint or not wss_endpoint.startswith(("ws://", "wss://")): logger.error("Invalid WebSocket endpoint. Must start with ws:// or wss://") sys.exit(1) diff --git a/config.py b/config.py index 36c273d..168b997 100644 --- a/config.py +++ b/config.py @@ -3,17 +3,17 @@ Configuration for the pump.fun trading bot. """ # Trading parameters -BUY_AMOUNT = 0.0001 # Amount of SOL to spend when buying -BUY_SLIPPAGE = 0.2 # 20% slippage tolerance for buying -SELL_SLIPPAGE = 0.2 # 20% slippage tolerance for selling +BUY_AMOUNT = 0.000001 # Amount of SOL to spend when buying +BUY_SLIPPAGE = 0.4 # 40% slippage tolerance for buying +SELL_SLIPPAGE = 0.4 # 40% slippage tolerance for selling ENABLE_DYNAMIC_PRIORITY_FEE = True # TODO: not implemented. getRecentPriorityFee is used to get current priority fee EXTRA_PRIORITY_FEE = 0.1 # TODO: not implemented. 10% increase in dynamic priority fee # Retries and timeouts -MAX_RETRIES: int = 5 -WAIT_TIME_AFTER_BUY: int = 5 -WAIT_TIME_BEFORE_NEW_TOKEN: int = 5 -WAIT_TIME_AFTER_CREATION: int = 5 +MAX_RETRIES: int = 2 +WAIT_TIME_AFTER_BUY: int = 15 +WAIT_TIME_BEFORE_NEW_TOKEN: int = 120 +WAIT_TIME_AFTER_CREATION: int = 15 # Node provier configuration # You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes diff --git a/src/core/client.py b/src/core/client.py index 7f9ea2c..159b908 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -8,6 +8,7 @@ from typing import Any, Dict from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts +from solders.hash import Hash from solders.keypair import Keypair from solders.pubkey import Pubkey from solders.transaction import Transaction @@ -78,7 +79,7 @@ class SolanaClient: return int(response.value.amount) return 0 - async def get_latest_blockhash(self) -> str: + async def get_latest_blockhash(self) -> Hash: """Get the latest blockhash. Returns: @@ -91,7 +92,6 @@ class SolanaClient: async def send_transaction( self, transaction: Transaction, - signer: Keypair, skip_preflight: bool = True, max_retries: int = 3, ) -> str: @@ -99,7 +99,6 @@ class SolanaClient: Args: transaction: Prepared transaction - signer: Transaction signer skip_preflight: Whether to skip preflight checks max_retries: Maximum number of sending attempts @@ -111,20 +110,13 @@ class SolanaClient: """ client = await self.get_client() - # Ensure transaction has a recent blockhash - if not transaction.recent_blockhash: - blockhash = await self.get_latest_blockhash() - transaction.recent_blockhash = blockhash - # Attempt to send with retries for attempt in range(max_retries): try: tx_opts = TxOpts( skip_preflight=skip_preflight, preflight_commitment=Confirmed ) - response = await client.send_transaction( - transaction, signer, opts=tx_opts - ) + response = await client.send_transaction(transaction, opts=tx_opts) return response.value except Exception as e: diff --git a/src/monitoring/listener.py b/src/monitoring/listener.py index b529243..1b29028 100644 --- a/src/monitoring/listener.py +++ b/src/monitoring/listener.py @@ -125,7 +125,14 @@ class PumpTokenListener: try: while True: await asyncio.sleep(self.ping_interval) - await websocket.ping() + try: + pong_waiter = await websocket.ping() + await asyncio.wait_for(pong_waiter, timeout=10) + except asyncio.TimeoutError: + logger.warning("Ping timeout - server not responding") + # Force reconnection + await websocket.close() + return except asyncio.CancelledError: pass except Exception as e: diff --git a/src/trading/buyer.py b/src/trading/buyer.py index d85aa89..a26e27e 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -4,18 +4,20 @@ Buy operations for pump.fun tokens. import asyncio import struct -from typing import List, Optional +from typing import Final, List, Optional import spl.token.instructions as spl_token from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts +from solders.hash import Hash from solders.instruction import AccountMeta, Instruction from solders.keypair import Keypair +from solders.message import Message from solders.pubkey import Pubkey from solders.system_program import TransferParams, transfer from solders.transaction import Transaction -from spl.token.instructions import get_associated_token_address +from spl.token.instructions import create_associated_token_account from src.core.client import SolanaClient from src.core.curve import BondingCurveManager @@ -31,6 +33,9 @@ from src.utils.logger import get_logger logger = get_logger(__name__) +# Discriminator for the buy instruction +EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" None: + """Queue a token for processing if not already processed.""" + token_key = str(token_info.mint) + + if token_key in self.processed_tokens: + logger.debug(f"Token {token_info.symbol} already processed. Skipping...") + return + + # Record timestamp when token was discovered + self.token_timestamps[token_key] = asyncio.get_event_loop().time() + + # Add to queue + await self.token_queue.put(token_info) + logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})") + + async def _process_token_queue(self, marry_mode: bool, yolo_mode: bool) -> None: + """Continuously process tokens from the queue, only if they're fresh.""" + while True: + # Get next token + token_info = await self.token_queue.get() + token_key = str(token_info.mint) + + # Check if token is still fresh + current_time = asyncio.get_event_loop().time() + token_age = current_time - self.token_timestamps.get( + token_key, current_time + ) + + if token_age > self.max_token_age: + logger.info( + f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)" + ) + self.token_queue.task_done() + continue + + # Mark as processing + self.processed_tokens.add(token_key) + + # Process token + logger.info( + f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)" + ) + await self._handle_token(token_info, marry_mode, yolo_mode) + + # Mark queue task as done + self.token_queue.task_done() + + async def _handle_token( self, token_info: TokenInfo, marry_mode: bool, yolo_mode: bool ) -> None: """Handle a new token creation event. @@ -168,6 +231,13 @@ class PumpTrader: elif marry_mode: logger.info("Marry mode enabled. Skipping sell operation.") + # Wait before looking for the next token + if yolo_mode: + logger.info( + f"YOLO mode enabled. Waiting {config.WAIT_TIME_BEFORE_NEW_TOKEN} seconds before looking for next token..." + ) + await asyncio.sleep(config.WAIT_TIME_BEFORE_NEW_TOKEN) + except Exception as e: logger.error(f"Error handling token {token_info.symbol}: {str(e)}") @@ -211,7 +281,7 @@ class PumpTrader: "symbol": token_info.symbol, "price": price, "amount": amount, - "tx_hash": tx_hash, + "tx_hash": str(tx_hash), } with open("trades/trades.log", "a") as log_file: From ab186210fd07041ac9551a0fe0f3fe11fa1c70ef Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 6 Mar 2025 21:52:43 +0000 Subject: [PATCH 08/65] removed unused imports, minor fixes --- cli.py | 1 - src/core/client.py | 5 ++--- src/core/curve.py | 1 - src/monitoring/events.py | 10 +++++----- src/monitoring/listener.py | 4 ++-- src/trading/base.py | 7 +++---- src/trading/buyer.py | 9 +-------- src/trading/seller.py | 3 +-- src/trading/trader.py | 11 +++-------- src/utils/logger.py | 3 +-- 10 files changed, 18 insertions(+), 36 deletions(-) diff --git a/cli.py b/cli.py index 5923dde..c5228d8 100644 --- a/cli.py +++ b/cli.py @@ -7,7 +7,6 @@ import argparse import asyncio import os import sys -from typing import Any, Dict import config from src.trading.trader import PumpTrader diff --git a/src/core/client.py b/src/core/client.py index 159b908..ab9a485 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -3,13 +3,12 @@ Solana client abstraction for blockchain operations. """ import asyncio -from typing import Any, Dict +from typing import Any from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts from solders.hash import Hash -from solders.keypair import Keypair from solders.pubkey import Pubkey from solders.transaction import Transaction @@ -46,7 +45,7 @@ class SolanaClient: await self._client.close() self._client = None - async def get_account_info(self, pubkey: Pubkey) -> Dict[str, Any]: + async def get_account_info(self, pubkey: Pubkey) -> dict[str, Any]: """Get account info from the blockchain. Args: diff --git a/src/core/curve.py b/src/core/curve.py index efca883..c332170 100644 --- a/src/core/curve.py +++ b/src/core/curve.py @@ -6,7 +6,6 @@ import struct from typing import Final from construct import Flag, Int64ul, Struct -from solana.rpc.async_api import AsyncClient from solders.pubkey import Pubkey from src.core.client import SolanaClient diff --git a/src/monitoring/events.py b/src/monitoring/events.py index 0069e32..a7e540d 100644 --- a/src/monitoring/events.py +++ b/src/monitoring/events.py @@ -5,7 +5,7 @@ Event processing for pump.fun tokens. import base64 import json import struct -from typing import Any, Dict, List, Optional +from typing import Any from solders.pubkey import Pubkey from solders.transaction import VersionedTransaction @@ -31,7 +31,7 @@ class PumpEventProcessor: self.pump_program = pump_program self._idl = self._load_idl() - def _load_idl(self) -> Dict[str, Any]: + def _load_idl(self) -> dict[str, Any]: """Load IDL from file. Returns: @@ -56,7 +56,7 @@ class PumpEventProcessor: ] } - def process_transaction(self, tx_data: str) -> Optional[TokenInfo]: + def process_transaction(self, tx_data: str) -> TokenInfo | None: """Process a transaction and extract token info. Args: @@ -130,8 +130,8 @@ class PumpEventProcessor: return None def _decode_create_instruction( - self, ix_data: bytes, ix_def: Dict[str, Any], accounts: List[Pubkey] - ) -> Dict[str, Any]: + self, ix_data: bytes, ix_def: dict[str, Any], accounts: list[Pubkey] + ) -> dict[str, Any]: """Decode create instruction data. Args: diff --git a/src/monitoring/listener.py b/src/monitoring/listener.py index 1b29028..7af8647 100644 --- a/src/monitoring/listener.py +++ b/src/monitoring/listener.py @@ -4,7 +4,7 @@ WebSocket monitoring for pump.fun tokens. import asyncio import json -from typing import Awaitable, Callable, Optional +from collections.abc import Awaitable, Callable import websockets from solders.pubkey import Pubkey @@ -138,7 +138,7 @@ class PumpTokenListener: except Exception as e: logger.error(f"Ping error: {str(e)}") - async def _wait_for_token_creation(self, websocket) -> Optional[TokenInfo]: + async def _wait_for_token_creation(self, websocket) -> TokenInfo | None: """Wait for token creation event. Args: diff --git a/src/trading/base.py b/src/trading/base.py index c4cb7f1..7144e7f 100644 --- a/src/trading/base.py +++ b/src/trading/base.py @@ -4,10 +4,9 @@ Base interfaces for trading operations. from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Tuple +from typing import Any from solders.pubkey import Pubkey -from solders.signature import Signature @dataclass @@ -23,7 +22,7 @@ class TokenInfo: user: Pubkey @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "TokenInfo": + def from_dict(cls, data: dict[str, Any]) -> "TokenInfo": """Create TokenInfo from dictionary. Args: @@ -42,7 +41,7 @@ class TokenInfo: user=Pubkey.from_string(data["user"]), ) - def to_dict(self) -> Dict[str, str]: + def to_dict(self) -> dict[str, str]: """Convert to dictionary. Returns: diff --git a/src/trading/buyer.py b/src/trading/buyer.py index a26e27e..1ddc99a 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -2,20 +2,13 @@ Buy operations for pump.fun tokens. """ -import asyncio import struct -from typing import Final, List, Optional +from typing import Final -import spl.token.instructions as spl_token -from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Confirmed -from solana.rpc.types import TxOpts from solders.hash import Hash from solders.instruction import AccountMeta, Instruction -from solders.keypair import Keypair from solders.message import Message from solders.pubkey import Pubkey -from solders.system_program import TransferParams, transfer from solders.transaction import Transaction from spl.token.instructions import create_associated_token_account diff --git a/src/trading/seller.py b/src/trading/seller.py index d202e0d..a91a7f0 100644 --- a/src/trading/seller.py +++ b/src/trading/seller.py @@ -2,9 +2,8 @@ Sell operations for pump.fun tokens. """ -import asyncio import struct -from typing import Final, Optional +from typing import Final from solders.hash import Hash from solders.instruction import AccountMeta, Instruction diff --git a/src/trading/trader.py b/src/trading/trader.py index 0a72367..f0ff56e 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -7,7 +7,6 @@ import asyncio import json import os from datetime import datetime -from typing import Dict, Optional, Set import config from src.core.client import SolanaClient @@ -74,13 +73,13 @@ class PumpTrader: self.buy_slippage = buy_slippage self.sell_slippage = sell_slippage self.max_retries = max_retries - self.max_token_age = max_token_age + self.max_token_age = 1 # seconds # Token processing state self.token_queue = asyncio.Queue() self.processing = False - self.processed_tokens: Set[str] = set() - self.token_timestamps: Dict[str, float] = {} + self.processed_tokens: set[str] = set() + self.token_timestamps: dict[str, float] = {} async def start( self, @@ -139,7 +138,6 @@ class PumpTrader: async def _process_token_queue(self, marry_mode: bool, yolo_mode: bool) -> None: """Continuously process tokens from the queue, only if they're fresh.""" while True: - # Get next token token_info = await self.token_queue.get() token_key = str(token_info.mint) @@ -156,16 +154,13 @@ class PumpTrader: self.token_queue.task_done() continue - # Mark as processing self.processed_tokens.add(token_key) - # Process token logger.info( f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)" ) await self._handle_token(token_info, marry_mode, yolo_mode) - # Mark queue task as done self.token_queue.task_done() async def _handle_token( diff --git a/src/utils/logger.py b/src/utils/logger.py index 35dabd2..9fa07f5 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -4,10 +4,9 @@ Logging utilities for the pump.fun trading bot. import logging import sys -from typing import Dict # Global dict to store loggers -_loggers: Dict[str, logging.Logger] = {} +_loggers: dict[str, logging.Logger] = {} def get_logger(name: str, level: int = logging.INFO) -> logging.Logger: From bfcea298fd651b3bd2db817c68360a474ab53088 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 6 Mar 2025 22:05:51 +0000 Subject: [PATCH 09/65] added max_token_age --- config.py | 11 ++++++++--- src/trading/trader.py | 7 +++---- trades/trades.log | 6 ++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/config.py b/config.py index 168b997..7e9c156 100644 --- a/config.py +++ b/config.py @@ -11,9 +11,14 @@ EXTRA_PRIORITY_FEE = 0.1 # TODO: not implemented. 10% increase in dynamic prior # Retries and timeouts MAX_RETRIES: int = 2 -WAIT_TIME_AFTER_BUY: int = 15 -WAIT_TIME_BEFORE_NEW_TOKEN: int = 120 -WAIT_TIME_AFTER_CREATION: int = 15 +WAIT_TIME_AFTER_BUY: int = 12 +WAIT_TIME_BEFORE_NEW_TOKEN: int = 5 +WAIT_TIME_AFTER_CREATION: int = 12 + +# Maximum age (in seconds) for a token to be considered "fresh" and eligible for processing. +# This threshold is checked before processing starts - tokens older than this are skipped +# since they likely contain outdated information from the websocket stream +MAX_TOKEN_AGE: int = 1 # Node provier configuration # You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes diff --git a/src/trading/trader.py b/src/trading/trader.py index f0ff56e..38fe7e1 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -73,7 +73,7 @@ class PumpTrader: self.buy_slippage = buy_slippage self.sell_slippage = sell_slippage self.max_retries = max_retries - self.max_token_age = 1 # seconds + self.max_token_age = config.MAX_TOKEN_AGE # Token processing state self.token_queue = asyncio.Queue() @@ -131,9 +131,8 @@ class PumpTrader: # Record timestamp when token was discovered self.token_timestamps[token_key] = asyncio.get_event_loop().time() - # Add to queue await self.token_queue.put(token_info) - logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})") + # logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})") async def _process_token_queue(self, marry_mode: bool, yolo_mode: bool) -> None: """Continuously process tokens from the queue, only if they're fresh.""" @@ -141,7 +140,7 @@ class PumpTrader: token_info = await self.token_queue.get() token_key = str(token_info.mint) - # Check if token is still fresh + # Check if token is still "fresh" current_time = asyncio.get_event_loop().time() token_age = current_time - self.token_timestamps.get( token_key, current_time diff --git a/trades/trades.log b/trades/trades.log index 03d0e64..3a4a9aa 100644 --- a/trades/trades.log +++ b/trades/trades.log @@ -1,2 +1,8 @@ {"timestamp": "2024-08-22T10:51:30.306580", "action": "buy", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "XZDRuEnaBNEeuFAiGyJsqBmVF3uZvFqAQUKsZVKTxsThHxN43LiRqy3T4cSH4dRXGB7c5A4855iYQDLY4BNJJhP"} {"timestamp": "2024-08-22T10:51:56.116951", "action": "sell", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "WQHnEoyztfDf5FJyQRXDGxxtqf2EcMGs8SET43KuYU8Dpdunruku22SHnfiH2xsUi1hTBjC1msvTcnU78fFKrc2"} +{"timestamp": "2025-03-06T21:53:16.417950", "action": "buy", "token_address": "7ztobvXwBd8UBn2mUuatjpmEmn7EZpifXv98xjCrpump", "symbol": "$Auti", "price": 3.3860819583417394e-08, "amount": 29.532657871333047, "tx_hash": "4fUuh16912cK7xiZsvY1scu5P5NtWGwowcAYXQvvr1FWAvPD57mPKfrq5PMMzMULhMiVn86fiDCUfwWt6B7NPNxV"} +{"timestamp": "2025-03-06T21:53:33.704455", "action": "sell", "token_address": "7ztobvXwBd8UBn2mUuatjpmEmn7EZpifXv98xjCrpump", "symbol": "$Auti", "price": 3.38403327639918e-08, "amount": 29.532657, "tx_hash": "4TRFmKYkibrgn2fX4UT7mM8i7Lpu3FgXKvY1d6AsEUaxLj11JxWo61xxqYGW7B6wzdaY9LWVWZnTSDTZUUNs5pFU"} +{"timestamp": "2025-03-06T22:04:01.067407", "action": "buy", "token_address": "5jch2xH14RBWZgKwmxEUJwC4howFNBCBqbnZ5uguGujj", "symbol": "$MOONWEN", "price": 2.8543914418196173e-08, "amount": 35.03373732659877, "tx_hash": "VCWbh79TmNWtt9kjex5LLtWKGHjc3nPBQVJdynC7B85nbnXx4DBgwguqAe6StYVAUS7jzP17pN5FWWGvbiUBRfk"} +{"timestamp": "2025-03-06T22:04:48.843998", "action": "sell", "token_address": "5jch2xH14RBWZgKwmxEUJwC4howFNBCBqbnZ5uguGujj", "symbol": "$MOONWEN", "price": 2.8056120698010214e-08, "amount": 35.033737, "tx_hash": "RLx8hFVd5DVZdUg5vN41Fs8Yz66yTEUEvegasuHwYQ7DTJhPcCdggoNBtipb93VPVdtQLZydxc9ZKRZ1uzK6KzH"} +{"timestamp": "2025-03-06T22:05:11.444317", "action": "buy", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.383038210726932e-08, "amount": 29.559228649242023, "tx_hash": "3E4RT5kvRezWJmQKJ9wmcAiDcYZefohxFVNdEZPweE9vhq215rSmqBn4Dv4ypH5KUUuNJzKdhjyrGU2b47woWr9e"} +{"timestamp": "2025-03-06T22:05:25.789876", "action": "sell", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.3830384158620706e-08, "amount": 29.559228, "tx_hash": "3oyJVzsRvnQV5qBsEp4hSk97eFWzUjzfWWa16GMUXtMd7JBmbQkTkmvs7N8nagYcJvqfFhzhCKZEG6onxjYYQbwX"} From 3791a1c109deca60d9a43a65f91a96f2e70739cf Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 6 Mar 2025 22:19:40 +0000 Subject: [PATCH 10/65] decreased default max_token_age --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 7e9c156..afbc85c 100644 --- a/config.py +++ b/config.py @@ -18,7 +18,7 @@ WAIT_TIME_AFTER_CREATION: int = 12 # Maximum age (in seconds) for a token to be considered "fresh" and eligible for processing. # This threshold is checked before processing starts - tokens older than this are skipped # since they likely contain outdated information from the websocket stream -MAX_TOKEN_AGE: int = 1 +MAX_TOKEN_AGE: float = 0.1 # Node provier configuration # You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes From 3084a1cf68c4dcfd8293c442d3b40eb5a85a5a49 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 6 Mar 2025 22:26:01 +0000 Subject: [PATCH 11/65] increased delays for beter resilience --- config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.py b/config.py index afbc85c..54a316c 100644 --- a/config.py +++ b/config.py @@ -11,9 +11,9 @@ EXTRA_PRIORITY_FEE = 0.1 # TODO: not implemented. 10% increase in dynamic prior # Retries and timeouts MAX_RETRIES: int = 2 -WAIT_TIME_AFTER_BUY: int = 12 -WAIT_TIME_BEFORE_NEW_TOKEN: int = 5 -WAIT_TIME_AFTER_CREATION: int = 12 +WAIT_TIME_AFTER_BUY: int = 15 +WAIT_TIME_BEFORE_NEW_TOKEN: int = 30 +WAIT_TIME_AFTER_CREATION: int = 15 # Maximum age (in seconds) for a token to be considered "fresh" and eligible for processing. # This threshold is checked before processing starts - tokens older than this are skipped From 4cda58265a07c6656e10c0cb5b917d20a850efee Mon Sep 17 00:00:00 2001 From: smypmsa Date: Fri, 7 Mar 2025 21:09:41 +0000 Subject: [PATCH 12/65] updated readme --- README.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e351b95..5c363b9 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,42 @@ Not everyone is a scammer though, sometimes there are helpful outside devs who c **>>END OF WARNING<<** +**>> FURTHER ROADMAP <<** + +Hey guys, starting from the next week (**week of March 10**) we'll be rolling out updates to the bot v2 based on the feedback and the reported issues, including updating to the latest libs, better error handling etc. We are already actively working on it. + +That'll be in a separate branch. + +Overall, it'll be a gradual development & rollout: + +* Stage 1: General updates & QoL + * Lib updates + * Error handling + * Ability to set RPS in the config to match your provider's and plan RPS (preferably [Chainstack](https://console.chainstack.com/) 🤩) + * Ability to set dynamic priority fees +* Stage 2: Bonding curve and migration management + * Integrate `logsSubscribe` instead of `blockSubscribe` for sniping minted tokens into the main bot (currently it's separate in the learning examples section that you can integrate yourself) + * Keep both `logsSubscribe` & `blockSubscribe` in the main bot — so that you can try out/choose which one works best for you — plus the Solana node architecture and provders change, so it's useful to have both + * Do retries instead of cooldown and/or keep the cooldown + * Checking a bonding curve status progress. As in to predict how soon a token will start the migration process. + * Script to close the associated bonding curve account if the rest of the flow txs fails + * Add listening to Raydium migration (and try and figure out the `logSubscribe` way for it as well) — still not sure if I can FAFO this one out, but had some progress in the past +* Stage 3: Trading experience + * Take profit, stop loss + * Sell when a specific market cap has been reached + * Copy trading + * Script for basic token analysis (market cap, creator investment, liquidity, token age) + being to go back with Solana archive nodes (e.g. accounts that consistently print token, average mint to raydium time for winning printing accounts and so on) +* Stage 4: Minting experience + * Ability to mint tokens? (there is a request and there was someone who minted 18k tokens) +* Bonus: Vector.fun + * There's a lot of pump.fun tokens on vector.fun but I didn't investigate yet if there's anything we can do with it. + +Note that the stage progression is from simple to more complex and we don't guarantee everything as move from Stage 1 to the rest. + +And we appreciate all your feedback and we'll keep you posted! + +**>> END OF ROADMPAP <<** + For the full walkthrough, see [Solana: Creating a trading and sniping pump.fun bot](https://docs.chainstack.com/docs/solana-creating-a-pumpfun-bot). For near-instantaneous transaction propagation, you can use the [Chainstack Solana Trader nodes](https://docs.chainstack.com/docs/trader-nodes). @@ -97,4 +133,4 @@ To run: So now you can run `listen_create_from_blocksubscribe.py` and `listen_new_direct_full_details.py` at the same time and see which one is faster. -Also here's a doc on this: [Solana: Listening to pump.fun token mint using only logsSubscribe](https://docs.chainstack.com/docs/solana-listening-to-pumpfun-token-mint-using-only-logssubscribe) +Also here's a doc on this: [Solana: Listening to pump.fun token mint using only logsSubscribe](https://docs.chainstack.com/docs/solana-listening-to-pumpfun-token-mint-using-only-logssubscribe) \ No newline at end of file From 4c4b9a270be4fdcf4cf528abd13abac551f31f62 Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Fri, 7 Mar 2025 22:11:21 +0100 Subject: [PATCH 13/65] updated readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5c363b9..a74b95c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ That'll be in a separate branch. Overall, it'll be a gradual development & rollout: * Stage 1: General updates & QoL - * Lib updates + * ✅ Lib updates * Error handling * Ability to set RPS in the config to match your provider's and plan RPS (preferably [Chainstack](https://console.chainstack.com/) 🤩) * Ability to set dynamic priority fees @@ -133,4 +133,4 @@ To run: So now you can run `listen_create_from_blocksubscribe.py` and `listen_new_direct_full_details.py` at the same time and see which one is faster. -Also here's a doc on this: [Solana: Listening to pump.fun token mint using only logsSubscribe](https://docs.chainstack.com/docs/solana-listening-to-pumpfun-token-mint-using-only-logssubscribe) \ No newline at end of file +Also here's a doc on this: [Solana: Listening to pump.fun token mint using only logsSubscribe](https://docs.chainstack.com/docs/solana-listening-to-pumpfun-token-mint-using-only-logssubscribe) From 3fb5d151049ef34ad0e0b2017c7e51a509ae147c Mon Sep 17 00:00:00 2001 From: smypmsa Date: Fri, 7 Mar 2025 21:20:32 +0000 Subject: [PATCH 14/65] removed unused files --- .archive/buy.py | 356 ---------------------------------------------- .archive/sell.py | 181 ----------------------- .archive/trade.py | 211 --------------------------- 3 files changed, 748 deletions(-) delete mode 100644 .archive/buy.py delete mode 100644 .archive/sell.py delete mode 100644 .archive/trade.py diff --git a/.archive/buy.py b/.archive/buy.py deleted file mode 100644 index 7b78290..0000000 --- a/.archive/buy.py +++ /dev/null @@ -1,356 +0,0 @@ -import asyncio -import base64 -import hashlib -import json -import struct -import time - -import base58 -import spl.token.instructions as spl_token -import websockets -from construct import Flag, Int64ul, Struct -from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Confirmed -from solana.rpc.types import TxOpts -from solana.transaction import Transaction -from solders.instruction import AccountMeta, Instruction -from solders.keypair import Keypair -from solders.pubkey import Pubkey -from solders.system_program import TransferParams, transfer -from solders.transaction import VersionedTransaction -from spl.token.instructions import get_associated_token_address - -from config import * -from trade import trade - -# Here and later all the discriminators are precalculated. See learning-examples/discriminator.py -EXPECTED_DISCRIMINATOR = struct.pack(" None: - parsed = self._STRUCT.parse(data[8:]) - self.__dict__.update(parsed) - - -async def get_pump_curve_state( - conn: AsyncClient, curve_address: Pubkey -) -> BondingCurveState: - response = await conn.get_account_info(curve_address) - 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 - ) - - -async def buy_token( - mint: Pubkey, - bonding_curve: Pubkey, - associated_bonding_curve: Pubkey, - amount: float, - slippage: float = 0.01, - max_retries=5, -): - private_key = base58.b58decode(PRIVATE_KEY) - payer = Keypair.from_bytes(private_key) - - async with AsyncClient(RPC_ENDPOINT) as client: - associated_token_account = get_associated_token_address(payer.pubkey(), mint) - amount_lamports = int(amount * LAMPORTS_PER_SOL) - - # Fetch the token price - curve_state = await get_pump_curve_state(client, bonding_curve) - token_price_sol = calculate_pump_curve_price(curve_state) - token_amount = amount / token_price_sol - - # Calculate maximum SOL to spend with slippage - max_amount_lamports = int(amount_lamports * (1 + slippage)) - - # Create associated token account with retries - for ata_attempt in range(max_retries): - try: - account_info = await client.get_account_info(associated_token_account) - if account_info.value is None: - print( - f"Creating associated token account (Attempt {ata_attempt + 1})..." - ) - create_ata_ix = spl_token.create_associated_token_account( - payer=payer.pubkey(), owner=payer.pubkey(), mint=mint - ) - create_ata_tx = Transaction() - create_ata_tx.add(create_ata_ix) - recent_blockhash = await client.get_latest_blockhash() - create_ata_tx.recent_blockhash = recent_blockhash.value.blockhash - await client.send_transaction(create_ata_tx, payer) - print("Associated token account created.") - print( - f"Associated token account address: {associated_token_account}" - ) - break - else: - print("Associated token account already exists.") - print( - f"Associated token account address: {associated_token_account}" - ) - break - except Exception as e: - print( - f"Attempt {ata_attempt + 1} to create associated token account failed: {str(e)}" - ) - if ata_attempt < max_retries - 1: - wait_time = 2**ata_attempt - print(f"Retrying in {wait_time} seconds...") - await asyncio.sleep(wait_time) - else: - print( - "Max retries reached. Unable to create associated token account." - ) - return - - # Continue with the buy transaction - for attempt in range(max_retries): - try: - accounts = [ - AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta( - pubkey=bonding_curve, is_signer=False, is_writable=True - ), - AccountMeta( - pubkey=associated_bonding_curve, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=associated_token_account, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=payer.pubkey(), is_signer=True, is_writable=True - ), - AccountMeta( - pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False - ), - AccountMeta(pubkey=SYSTEM_RENT, is_signer=False, is_writable=False), - AccountMeta( - pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False - ), - ] - - discriminator = struct.pack(" ping_interval: - await websocket.ping() - last_ping_time = current_time - - response = await asyncio.wait_for(websocket.recv(), timeout=30) - data = json.loads(response) - - if "method" in data and data["method"] == "blockNotification": - if "params" in data and "result" in data["params"]: - block_data = data["params"]["result"] - if "value" in block_data and "block" in block_data["value"]: - block = block_data["value"]["block"] - if "transactions" in block: - for tx in block["transactions"]: - if isinstance(tx, dict) and "transaction" in tx: - tx_data_decoded = base64.b64decode( - tx["transaction"][0] - ) - transaction = VersionedTransaction.from_bytes( - tx_data_decoded - ) - - for ix in transaction.message.instructions: - if str( - transaction.message.account_keys[ - ix.program_id_index - ] - ) == str(PUMP_PROGRAM): - ix_data = bytes(ix.data) - discriminator = struct.unpack( - " None: - parsed = self._STRUCT.parse(data[8:]) - self.__dict__.update(parsed) - - -async def get_pump_curve_state( - conn: AsyncClient, curve_address: Pubkey -) -> BondingCurveState: - response = await conn.get_account_info(curve_address) - 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 - ) - - -async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey): - response = await conn.get_token_account_balance(associated_token_account) - if response.value: - return int(response.value.amount) - return 0 - - -async def sell_token( - mint: Pubkey, - bonding_curve: Pubkey, - associated_bonding_curve: Pubkey, - slippage: float = 0.25, - max_retries=5, -): - private_key = base58.b58decode(PRIVATE_KEY) - payer = Keypair.from_bytes(private_key) - - async with AsyncClient(RPC_ENDPOINT) as client: - associated_token_account = get_associated_token_address(payer.pubkey(), mint) - - # Get token balance - token_balance = await get_token_balance(client, associated_token_account) - token_balance_decimal = token_balance / 10**TOKEN_DECIMALS - print(f"Token balance: {token_balance_decimal}") - if token_balance == 0: - print("No tokens to sell.") - return - - # Fetch the token price - curve_state = await get_pump_curve_state(client, bonding_curve) - token_price_sol = calculate_pump_curve_price(curve_state) - print(f"Price per Token: {token_price_sol:.20f} SOL") - - # Calculate minimum SOL output - amount = token_balance - min_sol_output = float(token_balance_decimal) * float(token_price_sol) - slippage_factor = 1 - slippage - min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL) - - print(f"Selling {token_balance_decimal} tokens") - print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") - - for attempt in range(max_retries): - try: - accounts = [ - AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), - AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta( - pubkey=bonding_curve, is_signer=False, is_writable=True - ), - AccountMeta( - pubkey=associated_bonding_curve, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=associated_token_account, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=payer.pubkey(), is_signer=True, is_writable=True - ), - AccountMeta( - pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, - is_signer=False, - is_writable=False, - ), - AccountMeta( - pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False - ), - ] - - discriminator = struct.pack(" Date: Wed, 12 Mar 2025 20:44:56 +0100 Subject: [PATCH 15/65] chore: delete .vscode directory --- .vscode/settings.json | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 40d30ee..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "python.languageServer": "Pylance", - "python.analysis.typeCheckingMode": "basic", - "python.analysis.diagnosticSeverityOverrides": { - "reportMissingTypeStubs": "none", - "reportUnknownMemberType": "warning", - "reportUnknownParameterType": "warning" - }, - - "ruff.lint.enable": true, - "ruff.format.enable": true, - - "[python]": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "charliermarsh.ruff" - } -} \ No newline at end of file From ac3d463d200e96c2ce92805c0cc70f88561a52fb Mon Sep 17 00:00:00 2001 From: smypmsa Date: Wed, 12 Mar 2025 22:11:31 +0000 Subject: [PATCH 16/65] feat: add priority fee support with dynamic/fixed options, hard cap, and extra fee --- .gitignore | 1 + config.py | 37 ++++++++++-- src/core/client.py | 40 +++++++++---- src/core/priority_fee/__init__.py | 15 +++++ src/core/priority_fee/dynamic_fee.py | 38 +++++++++++++ src/core/priority_fee/fixed_fee.py | 25 +++++++++ src/core/priority_fee/manager.py | 84 ++++++++++++++++++++++++++++ src/trading/buyer.py | 36 +++++++----- src/trading/seller.py | 18 +++--- src/trading/trader.py | 12 ++++ 10 files changed, 266 insertions(+), 40 deletions(-) create mode 100644 src/core/priority_fee/__init__.py create mode 100644 src/core/priority_fee/dynamic_fee.py create mode 100644 src/core/priority_fee/fixed_fee.py create mode 100644 src/core/priority_fee/manager.py diff --git a/.gitignore b/.gitignore index 13525c6..c5d2607 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ trades/* +.vscode .pylintrc .ruff_cache diff --git a/config.py b/config.py index 54a316c..32dc0b3 100644 --- a/config.py +++ b/config.py @@ -3,11 +3,18 @@ Configuration for the pump.fun trading bot. """ # Trading parameters -BUY_AMOUNT = 0.000001 # Amount of SOL to spend when buying -BUY_SLIPPAGE = 0.4 # 40% slippage tolerance for buying -SELL_SLIPPAGE = 0.4 # 40% slippage tolerance for selling -ENABLE_DYNAMIC_PRIORITY_FEE = True # TODO: not implemented. getRecentPriorityFee is used to get current priority fee -EXTRA_PRIORITY_FEE = 0.1 # TODO: not implemented. 10% increase in dynamic priority fee +BUY_AMOUNT: float = 0.000001 # Amount of SOL to spend when buying +BUY_SLIPPAGE: float = 0.4 # 40% slippage tolerance for buying +SELL_SLIPPAGE: float = 0.4 # 40% slippage tolerance for selling + +# Configuration for priority fee settings +ENABLE_DYNAMIC_PRIORITY_FEE: bool = True # Enable dynamic priority fee calculation +ENABLE_FIXED_PRIORITY_FEE: bool = True # Enable fixed priority fee +FIXED_PRIORITY_FEE: int = 200000 # Fixed priority fee in lamports (0 means no fee) +EXTRA_PRIORITY_FEE: float = ( + 0.1 # Percentage increase applied to priority fee (0.1 = 10%) +) +HARD_CAP_PRIOR_FEE: int = 1000000 # Maximum allowed priority fee in lamports (hard cap) # Retries and timeouts MAX_RETRIES: int = 2 @@ -22,6 +29,24 @@ MAX_TOKEN_AGE: float = 0.1 # Node provier configuration # You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes -MAX_RPS = 25 # TODO: not implemented. Max RPS to avoid rate limit errors +MAX_RPS: int = 25 # TODO: not implemented. Max RPS to avoid rate limit errors PUBLIC_RPC_ENDPOINT = "https://api.mainnet-beta.solana.com" PUBLIC_WSS_ENDPOINT = "wss://api.mainnet-beta.solana.com" + + +def validate_priority_fee_config() -> None: + """Validate priority fee configuration values.""" + if not isinstance(ENABLE_DYNAMIC_PRIORITY_FEE, bool): + raise ValueError("ENABLE_DYNAMIC_PRIORITY_FEE must be a boolean") + if not isinstance(ENABLE_FIXED_PRIORITY_FEE, bool): + raise ValueError("ENABLE_FIXED_PRIORITY_FEE must be a boolean") + if not isinstance(FIXED_PRIORITY_FEE, int) or FIXED_PRIORITY_FEE < 0: + raise ValueError("FIXED_PRIORITY_FEE must be a non-negative integer") + if not isinstance(EXTRA_PRIORITY_FEE, float) or EXTRA_PRIORITY_FEE < 0: + raise ValueError("EXTRA_PRIORITY_FEE must be a non-negative float") + if not isinstance(HARD_CAP_PRIOR_FEE, int) or HARD_CAP_PRIOR_FEE < 0: + raise ValueError("HARD_CAP_PRIOR_FEE must be a non-negative integer") + + +# Validate config on import +validate_priority_fee_config() diff --git a/src/core/client.py b/src/core/client.py index ab9a485..8a2da66 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -8,7 +8,11 @@ from typing import Any 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_limit, set_compute_unit_price from solders.hash import Hash +from solders.instruction import Instruction +from solders.keypair import Keypair +from solders.message import Message from solders.pubkey import Pubkey from solders.transaction import Transaction @@ -88,34 +92,46 @@ class SolanaClient: response = await client.get_latest_blockhash() return response.value.blockhash - async def send_transaction( + async def build_and_send_transaction( self, - transaction: Transaction, + instructions: list[Instruction], + signer_keypair: Keypair, skip_preflight: bool = True, max_retries: int = 3, + priority_fee: int | None = None, ) -> str: - """Send a transaction to the network. + """ + Send a transaction with optional priority fee. Args: - transaction: Prepared transaction - skip_preflight: Whether to skip preflight checks - max_retries: Maximum number of sending attempts + instructions: List of instructions to include in the transaction. + skip_preflight: Whether to skip preflight checks. + max_retries: Maximum number of retry attempts. + priority_fee: Optional priority fee in lamports. Returns: - Transaction signature - - Raises: - Exception: If transaction fails after all retries + Transaction signature. """ client = await self.get_client() - # Attempt to send with retries + # Add priority fee instructions if applicable + if priority_fee is not None: + fee_instructions = [ + set_compute_unit_limit(200_000), # Default compute unit limit + set_compute_unit_price(priority_fee), + ] + instructions = fee_instructions + instructions + + recent_blockhash = await self.get_latest_blockhash() + message = Message(instructions, signer_keypair.pubkey()) + transaction = Transaction([signer_keypair], message, recent_blockhash) + for attempt in range(max_retries): try: tx_opts = TxOpts( skip_preflight=skip_preflight, preflight_commitment=Confirmed ) - response = await client.send_transaction(transaction, opts=tx_opts) + response = await client.send_transaction(transaction, tx_opts) return response.value except Exception as e: diff --git a/src/core/priority_fee/__init__.py b/src/core/priority_fee/__init__.py new file mode 100644 index 0000000..276aa74 --- /dev/null +++ b/src/core/priority_fee/__init__.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod + + +class PriorityFeePlugin(ABC): + """Base class for priority fee calculation plugins.""" + + @abstractmethod + async def get_priority_fee(self) -> int | None: + """ + Calculate the priority fee. + + Returns: + Optional[int]: Priority fee in lamports, or None if no fee should be applied. + """ + pass diff --git a/src/core/priority_fee/dynamic_fee.py b/src/core/priority_fee/dynamic_fee.py new file mode 100644 index 0000000..02804ab --- /dev/null +++ b/src/core/priority_fee/dynamic_fee.py @@ -0,0 +1,38 @@ +from src.core.client import SolanaClient +from src.utils.logger import get_logger + +from . import PriorityFeePlugin + +logger = get_logger(__name__) + + +class DynamicPriorityFee(PriorityFeePlugin): + """Default dynamic priority fee plugin using getRecentPriorityFee.""" + + def __init__(self, client: SolanaClient): + """ + Initialize the dynamic fee plugin. + + Args: + client: Solana RPC client for network requests. + """ + self.client = client + + async def get_priority_fee(self) -> int | None: + """ + Fetch the recent priority fee from the Solana network. + + Returns: + Optional[int]: Recent priority fee in lamports, or None if the request fails. + """ + try: + client = await self.client.get_client() + response = await client.get_recent_prioritization_fees() + if response and response.value: + return response.value[ + 0 + ].prioritization_fee # Use the first fee from the list + return None + except Exception as e: + logger.error(f"Failed to fetch recent priority fee: {str(e)}") + return None diff --git a/src/core/priority_fee/fixed_fee.py b/src/core/priority_fee/fixed_fee.py new file mode 100644 index 0000000..48cfc27 --- /dev/null +++ b/src/core/priority_fee/fixed_fee.py @@ -0,0 +1,25 @@ +from . import PriorityFeePlugin + + +class FixedPriorityFee(PriorityFeePlugin): + """Fixed priority fee plugin.""" + + def __init__(self, fixed_fee: int): + """ + Initialize the fixed fee plugin. + + Args: + fixed_fee: Fixed priority fee in lamports. + """ + self.fixed_fee = fixed_fee + + async def get_priority_fee(self) -> int | None: + """ + Return the fixed priority fee. + + Returns: + Optional[int]: Fixed priority fee in lamports, or None if fixed_fee is 0. + """ + if self.fixed_fee == 0: + return None + return self.fixed_fee diff --git a/src/core/priority_fee/manager.py b/src/core/priority_fee/manager.py new file mode 100644 index 0000000..641d05d --- /dev/null +++ b/src/core/priority_fee/manager.py @@ -0,0 +1,84 @@ +from src.core.client import SolanaClient +from src.core.priority_fee.dynamic_fee import DynamicPriorityFee +from src.core.priority_fee.fixed_fee import FixedPriorityFee +from src.utils.logger import get_logger + +logger = get_logger(__name__) + + +class PriorityFeeManager: + """Manager for priority fee calculation and validation.""" + + def __init__( + self, + client: SolanaClient, + enable_dynamic_fee: bool, + enable_fixed_fee: bool, + fixed_fee: int, + extra_fee: float, + hard_cap: int, + ): + """ + Initialize the priority fee manager. + + Args: + client: Solana RPC client for dynamic fee calculation. + enable_dynamic_fee: Whether to enable dynamic fee calculation. + enable_fixed_fee: Whether to enable fixed fee. + fixed_fee: Fixed priority fee in lamports. + extra_fee: Percentage increase to apply to the base fee. + hard_cap: Maximum allowed priority fee in lamports. + """ + self.client = client + self.enable_dynamic_fee = enable_dynamic_fee + self.enable_fixed_fee = enable_fixed_fee + self.fixed_fee = fixed_fee + self.extra_fee = extra_fee + self.hard_cap = hard_cap + + # Initialize plugins + self.dynamic_fee_plugin = DynamicPriorityFee(client) + self.fixed_fee_plugin = FixedPriorityFee(fixed_fee) + + async def calculate_priority_fee(self) -> int | None: + """ + Calculate the priority fee based on the configuration. + + Returns: + Optional[int]: Calculated priority fee in lamports, or None if no fee should be applied. + """ + base_fee = await self._get_base_fee() + if base_fee is None: + return None + + # Apply extra fee (percentage increase) + final_fee = int(base_fee * (1 + self.extra_fee)) + + # Enforce hard cap + if final_fee > self.hard_cap: + logger.warning( + f"Calculated priority fee {final_fee} exceeds hard cap {self.hard_cap}. Applying hard cap." + ) + final_fee = self.hard_cap + + return final_fee + + async def _get_base_fee(self) -> int | None: + """ + Determine the base fee based on the configuration. + + Returns: + Optional[int]: Base fee in lamports, or None if no fee should be applied. + """ + # Prefer dynamic fee if both are enabled + if self.enable_dynamic_fee: + dynamic_fee = await self.dynamic_fee_plugin.get_priority_fee() + if dynamic_fee is not None: + return dynamic_fee + + # Fall back to fixed fee if enabled + if self.enable_fixed_fee: + return await self.fixed_fee_plugin.get_priority_fee() + + # No fee if both are disabled or return None + return None diff --git a/src/trading/buyer.py b/src/trading/buyer.py index 1ddc99a..c1f6a69 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -5,13 +5,11 @@ Buy operations for pump.fun tokens. import struct from typing import Final -from solders.hash import Hash from solders.instruction import AccountMeta, Instruction -from solders.message import Message from solders.pubkey import Pubkey -from solders.transaction import Transaction from spl.token.instructions import create_associated_token_account +from core.priority_fee.manager import PriorityFeeManager from src.core.client import SolanaClient from src.core.curve import BondingCurveManager from src.core.pubkeys import ( @@ -38,6 +36,7 @@ class TokenBuyer(Trader): client: SolanaClient, wallet: Wallet, curve_manager: BondingCurveManager, + priority_fee_manager: PriorityFeeManager, amount: float, slippage: float = 0.01, max_retries: int = 5, @@ -55,6 +54,7 @@ class TokenBuyer(Trader): self.client = client self.wallet = wallet self.curve_manager = curve_manager + self.priority_fee_manager = priority_fee_manager self.amount = amount self.slippage = slippage self.max_retries = max_retries @@ -147,13 +147,19 @@ class TokenBuyer(Trader): payer=self.wallet.pubkey, owner=self.wallet.pubkey, mint=mint ) - recent_blockhash: Hash = await self.client.get_latest_blockhash() - create_ata_msg = Message([create_ata_ix], self.wallet.keypair.pubkey()) - create_ata_tx = Transaction( - [self.wallet.keypair], create_ata_msg, recent_blockhash - ) + # recent_blockhash: Hash = await self.client.get_latest_blockhash() + # create_ata_msg = Message([create_ata_ix], self.wallet.keypair.pubkey()) + # create_ata_tx = Transaction( + # [self.wallet.keypair], create_ata_msg, recent_blockhash + # ) - tx_sig = await self.client.send_transaction(create_ata_tx) + tx_sig = await self.client.build_and_send_transaction( + [create_ata_ix], + self.wallet.keypair, + skip_preflight=True, + max_retries=self.max_retries, + priority_fee=await self.priority_fee_manager.calculate_priority_fee(), # Get priority fee from manager + ) await self.client.confirm_transaction(tx_sig) logger.info( @@ -234,15 +240,17 @@ class TokenBuyer(Trader): buy_ix = Instruction(PumpAddresses.PROGRAM, data, accounts) # Prepare buy transaction data - recent_blockhash: Hash = await self.client.get_latest_blockhash() - buy_message = Message([buy_ix], self.wallet.keypair.pubkey()) - buy_tx = Transaction([self.wallet.keypair], buy_message, recent_blockhash) + # recent_blockhash: Hash = await self.client.get_latest_blockhash() + # buy_message = Message([buy_ix], self.wallet.keypair.pubkey()) + # buy_tx = Transaction([self.wallet.keypair], buy_message, recent_blockhash) try: - return await self.client.send_transaction( - buy_tx, + return await self.client.build_and_send_transaction( + [buy_ix], + self.wallet.keypair, skip_preflight=True, max_retries=self.max_retries, + priority_fee=await self.priority_fee_manager.calculate_priority_fee(), # Get priority fee from manager ) except Exception as e: logger.error(f"Buy transaction failed: {str(e)}") diff --git a/src/trading/seller.py b/src/trading/seller.py index a91a7f0..244221d 100644 --- a/src/trading/seller.py +++ b/src/trading/seller.py @@ -5,12 +5,10 @@ Sell operations for pump.fun tokens. import struct from typing import Final -from solders.hash import Hash from solders.instruction import AccountMeta, Instruction -from solders.message import Message from solders.pubkey import Pubkey -from solders.transaction import Transaction +from core.priority_fee.manager import PriorityFeeManager from src.core.client import SolanaClient from src.core.curve import BondingCurveManager from src.core.pubkeys import ( @@ -37,6 +35,7 @@ class TokenSeller(Trader): client: SolanaClient, wallet: Wallet, curve_manager: BondingCurveManager, + priority_fee_manager: PriorityFeeManager, slippage: float = 0.25, max_retries: int = 5, ): @@ -52,6 +51,7 @@ class TokenSeller(Trader): self.client = client self.wallet = wallet self.curve_manager = curve_manager + self.priority_fee_manager = priority_fee_manager self.slippage = slippage self.max_retries = max_retries @@ -202,15 +202,17 @@ class TokenSeller(Trader): sell_ix = Instruction(PumpAddresses.PROGRAM, data, accounts) # Prepare sell transaction data - recent_blockhash: Hash = await self.client.get_latest_blockhash() - sell_message = Message([sell_ix], self.wallet.keypair.pubkey()) - sell_tx = Transaction([self.wallet.keypair], sell_message, recent_blockhash) + # recent_blockhash: Hash = await self.client.get_latest_blockhash() + # sell_message = Message([sell_ix], self.wallet.keypair.pubkey()) + # sell_tx = Transaction([self.wallet.keypair], sell_message, recent_blockhash) try: - return await self.client.send_transaction( - sell_tx, + return await self.client.build_and_send_transaction( + [sell_ix], + self.wallet.keypair, skip_preflight=True, max_retries=self.max_retries, + priority_fee=await self.priority_fee_manager.calculate_priority_fee(), # Get priority fee from manager ) except Exception as e: logger.error(f"Sell transaction failed: {str(e)}") diff --git a/src/trading/trader.py b/src/trading/trader.py index 38fe7e1..55cb9e2 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -9,6 +9,7 @@ import os from datetime import datetime import config +from core.priority_fee.manager import PriorityFeeManager from src.core.client import SolanaClient from src.core.curve import BondingCurveManager from src.core.pubkeys import PumpAddresses @@ -50,10 +51,20 @@ class PumpTrader: self.wallet = Wallet(private_key) self.curve_manager = BondingCurveManager(self.solana_client) + self.priority_fee_manager = PriorityFeeManager( + client=self.solana_client, + enable_dynamic_fee=config.ENABLE_DYNAMIC_PRIORITY_FEE, + enable_fixed_fee=config.ENABLE_FIXED_PRIORITY_FEE, + fixed_fee=config.FIXED_PRIORITY_FEE, + extra_fee=config.EXTRA_PRIORITY_FEE, + hard_cap=config.HARD_CAP_PRIOR_FEE, + ) + self.buyer = TokenBuyer( self.solana_client, self.wallet, self.curve_manager, + self.priority_fee_manager, buy_amount, buy_slippage, max_retries, @@ -63,6 +74,7 @@ class PumpTrader: self.solana_client, self.wallet, self.curve_manager, + self.priority_fee_manager, sell_slippage, max_retries, ) From a1d5966f2995e560f69a3f00dd3ce3c05208cce2 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 13 Mar 2025 21:01:26 +0000 Subject: [PATCH 17/65] refactor: tx building, getting dynamic prior fee --- config.py | 10 +++-- requirements.txt | 3 +- src/core/client.py | 36 ++++++++++++++++- src/core/priority_fee/dynamic_fee.py | 59 ++++++++++++++++++++------- src/core/priority_fee/fixed_fee.py | 4 +- src/core/priority_fee/manager.py | 26 +++++++----- src/trading/base.py | 19 +++++++++ src/trading/buyer.py | 60 ++++++++++++---------------- src/trading/seller.py | 44 +++++++++----------- 9 files changed, 170 insertions(+), 91 deletions(-) diff --git a/config.py b/config.py index 32dc0b3..3f075d8 100644 --- a/config.py +++ b/config.py @@ -3,18 +3,20 @@ Configuration for the pump.fun trading bot. """ # Trading parameters -BUY_AMOUNT: float = 0.000001 # Amount of SOL to spend when buying +BUY_AMOUNT: float = 0.000_001 # Amount of SOL to spend when buying BUY_SLIPPAGE: float = 0.4 # 40% slippage tolerance for buying SELL_SLIPPAGE: float = 0.4 # 40% slippage tolerance for selling # Configuration for priority fee settings ENABLE_DYNAMIC_PRIORITY_FEE: bool = True # Enable dynamic priority fee calculation -ENABLE_FIXED_PRIORITY_FEE: bool = True # Enable fixed priority fee -FIXED_PRIORITY_FEE: int = 200000 # Fixed priority fee in lamports (0 means no fee) +ENABLE_FIXED_PRIORITY_FEE: bool = False # Enable fixed priority fee +FIXED_PRIORITY_FEE: int = 50_000 # Fixed priority fee in microlamports EXTRA_PRIORITY_FEE: float = ( 0.1 # Percentage increase applied to priority fee (0.1 = 10%) ) -HARD_CAP_PRIOR_FEE: int = 1000000 # Maximum allowed priority fee in lamports (hard cap) +HARD_CAP_PRIOR_FEE: int = ( + 200_000 # Maximum allowed priority fee in microlamports (hard cap) +) # Retries and timeouts MAX_RETRIES: int = 2 diff --git a/requirements.txt b/requirements.txt index e18fee3..c84165e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ construct-typing>=0.5.2 solana==0.36.6 solders>=0.26.0 websockets>=15.0 -python-dotenv>=1.0.1 \ No newline at end of file +python-dotenv>=1.0.1 +aiohttp==3.11.13 \ No newline at end of file diff --git a/src/core/client.py b/src/core/client.py index 8a2da66..a6d4b3b 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -3,8 +3,10 @@ Solana client abstraction for blockchain operations. """ import asyncio +import json from typing import Any +import aiohttp from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts @@ -107,17 +109,21 @@ class SolanaClient: instructions: List of instructions to include in the transaction. skip_preflight: Whether to skip preflight checks. max_retries: Maximum number of retry attempts. - priority_fee: Optional priority fee in lamports. + priority_fee: Optional priority fee in microlamports. Returns: Transaction signature. """ client = await self.get_client() + logger.info( + f"Priority fee in microlamports: {priority_fee if priority_fee else 0}" + ) + # Add priority fee instructions if applicable if priority_fee is not None: fee_instructions = [ - set_compute_unit_limit(200_000), # Default compute unit limit + set_compute_unit_limit(100_000), # Default compute unit limit set_compute_unit_price(priority_fee), ] instructions = fee_instructions + instructions @@ -166,3 +172,29 @@ class SolanaClient: except Exception as e: logger.error(f"Failed to confirm transaction {signature}: {str(e)}") return False + + async def post_rpc(self, body: dict[str, Any]) -> dict[str, Any] | None: + """ + Send a raw RPC request to the Solana node. + + Args: + body: JSON-RPC request body. + + Returns: + Optional[Dict[str, Any]]: Parsed JSON response, or None if the request fails. + """ + try: + async with aiohttp.ClientSession() as session: + async with session.post( + self.rpc_endpoint, + json=body, + timeout=aiohttp.ClientTimeout(10), # 10-second timeout + ) as response: + response.raise_for_status() + return await response.json() + except aiohttp.ClientError as e: + logger.error(f"RPC request failed: {str(e)}", exc_info=True) + return None + except json.JSONDecodeError as e: + logger.error(f"Failed to decode RPC response: {str(e)}", exc_info=True) + return None diff --git a/src/core/priority_fee/dynamic_fee.py b/src/core/priority_fee/dynamic_fee.py index 02804ab..b8a1ff8 100644 --- a/src/core/priority_fee/dynamic_fee.py +++ b/src/core/priority_fee/dynamic_fee.py @@ -1,13 +1,16 @@ +import statistics + +from solders.pubkey import Pubkey + +from core.priority_fee import PriorityFeePlugin from src.core.client import SolanaClient from src.utils.logger import get_logger -from . import PriorityFeePlugin - logger = get_logger(__name__) class DynamicPriorityFee(PriorityFeePlugin): - """Default dynamic priority fee plugin using getRecentPriorityFee.""" + """Dynamic priority fee plugin using getRecentPrioritizationFees.""" def __init__(self, client: SolanaClient): """ @@ -18,21 +21,49 @@ class DynamicPriorityFee(PriorityFeePlugin): """ self.client = client - async def get_priority_fee(self) -> int | None: + async def get_priority_fee( + self, accounts: list[Pubkey] | None = None + ) -> int | None: """ - Fetch the recent priority fee from the Solana network. + Fetch the recent priority fee using getRecentPrioritizationFees. + + Args: + accounts: List of accounts to consider for the fee calculation. + If None, the fee is calculated without specific account constraints. Returns: - Optional[int]: Recent priority fee in lamports, or None if the request fails. + Optional[int]: Median priority fee in microlamports, or None if the request fails. """ try: - client = await self.client.get_client() - response = await client.get_recent_prioritization_fees() - if response and response.value: - return response.value[ - 0 - ].prioritization_fee # Use the first fee from the list - return None + body = { + "jsonrpc": "2.0", + "id": 1, + "method": "getRecentPrioritizationFees", + "params": [[str(account) for account in accounts]] if accounts else [], + } + + response = await self.client.post_rpc(body) + if not response or "result" not in response: + logger.error( + "Failed to fetch recent prioritization fees: invalid response" + ) + return None + + fees = [fee["prioritizationFee"] for fee in response["result"]] + if not fees: + logger.warning("No prioritization fees found in the response") + return None + + # Get the 70th percentile of fees for faster processing + # It means you're paying a fee that's higher than 70% of other transactions + # Higher percentile = faster transactions but more expensive + # Lower percentile = cheaper but slower transactions + prior_fee = int(statistics.quantiles(fees, n=10)[-3]) # 70th percentile + + return prior_fee + except Exception as e: - logger.error(f"Failed to fetch recent priority fee: {str(e)}") + logger.error( + f"Failed to fetch recent priority fee: {str(e)}", exc_info=True + ) return None diff --git a/src/core/priority_fee/fixed_fee.py b/src/core/priority_fee/fixed_fee.py index 48cfc27..64102bc 100644 --- a/src/core/priority_fee/fixed_fee.py +++ b/src/core/priority_fee/fixed_fee.py @@ -9,7 +9,7 @@ class FixedPriorityFee(PriorityFeePlugin): Initialize the fixed fee plugin. Args: - fixed_fee: Fixed priority fee in lamports. + fixed_fee: Fixed priority fee in microlamports. """ self.fixed_fee = fixed_fee @@ -18,7 +18,7 @@ class FixedPriorityFee(PriorityFeePlugin): Return the fixed priority fee. Returns: - Optional[int]: Fixed priority fee in lamports, or None if fixed_fee is 0. + Optional[int]: Fixed priority fee in microlamports, or None if fixed_fee is 0. """ if self.fixed_fee == 0: return None diff --git a/src/core/priority_fee/manager.py b/src/core/priority_fee/manager.py index 641d05d..db5b12d 100644 --- a/src/core/priority_fee/manager.py +++ b/src/core/priority_fee/manager.py @@ -1,3 +1,5 @@ +from solders.pubkey import Pubkey + from src.core.client import SolanaClient from src.core.priority_fee.dynamic_fee import DynamicPriorityFee from src.core.priority_fee.fixed_fee import FixedPriorityFee @@ -25,9 +27,9 @@ class PriorityFeeManager: client: Solana RPC client for dynamic fee calculation. enable_dynamic_fee: Whether to enable dynamic fee calculation. enable_fixed_fee: Whether to enable fixed fee. - fixed_fee: Fixed priority fee in lamports. + fixed_fee: Fixed priority fee in microlamports. extra_fee: Percentage increase to apply to the base fee. - hard_cap: Maximum allowed priority fee in lamports. + hard_cap: Maximum allowed priority fee in microlamports. """ self.client = client self.enable_dynamic_fee = enable_dynamic_fee @@ -40,14 +42,20 @@ class PriorityFeeManager: self.dynamic_fee_plugin = DynamicPriorityFee(client) self.fixed_fee_plugin = FixedPriorityFee(fixed_fee) - async def calculate_priority_fee(self) -> int | None: + async def calculate_priority_fee( + self, accounts: list[Pubkey] | None = None + ) -> int | None: """ Calculate the priority fee based on the configuration. + Args: + accounts: List of accounts to consider for dynamic fee calculation. + If None, the fee is calculated without specific account constraints. + Returns: - Optional[int]: Calculated priority fee in lamports, or None if no fee should be applied. + Optional[int]: Calculated priority fee in microlamports, or None if no fee should be applied. """ - base_fee = await self._get_base_fee() + base_fee = await self._get_base_fee(accounts) if base_fee is None: return None @@ -63,16 +71,16 @@ class PriorityFeeManager: return final_fee - async def _get_base_fee(self) -> int | None: + async def _get_base_fee(self, accounts: list[Pubkey] | None = None) -> int | None: """ Determine the base fee based on the configuration. Returns: - Optional[int]: Base fee in lamports, or None if no fee should be applied. + Optional[int]: Base fee in microlamports, or None if no fee should be applied. """ # Prefer dynamic fee if both are enabled if self.enable_dynamic_fee: - dynamic_fee = await self.dynamic_fee_plugin.get_priority_fee() + dynamic_fee = await self.dynamic_fee_plugin.get_priority_fee(accounts) if dynamic_fee is not None: return dynamic_fee @@ -80,5 +88,5 @@ class PriorityFeeManager: if self.enable_fixed_fee: return await self.fixed_fee_plugin.get_priority_fee() - # No fee if both are disabled or return None + # No priority fee if both are disabled return None diff --git a/src/trading/base.py b/src/trading/base.py index 7144e7f..2359590 100644 --- a/src/trading/base.py +++ b/src/trading/base.py @@ -8,6 +8,8 @@ from typing import Any from solders.pubkey import Pubkey +from core.pubkeys import PumpAddresses + @dataclass class TokenInfo: @@ -80,3 +82,20 @@ class Trader(ABC): TradeResult with operation outcome """ pass + + def _get_relevant_accounts(self, token_info: TokenInfo) -> list[Pubkey]: + """ + Get the list of accounts relevant for calculating the priority fee. + + Args: + token_info: Token information for the buy/sell operation. + + Returns: + list[Pubkey]: List of relevant accounts. + """ + return [ + token_info.mint, # Token mint address + token_info.bonding_curve, # Bonding curve address + PumpAddresses.PROGRAM, # Pump.fun program address + PumpAddresses.FEE, # Pump.fun fee account + ] diff --git a/src/trading/buyer.py b/src/trading/buyer.py index c1f6a69..c38dfe7 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -69,16 +69,13 @@ class TokenBuyer(Trader): TradeResult with buy outcome """ try: - # Extract token info - mint = token_info.mint - bonding_curve = token_info.bonding_curve - associated_bonding_curve = token_info.associated_bonding_curve - # Convert amount to lamports amount_lamports = int(self.amount * LAMPORTS_PER_SOL) # Fetch token price - curve_state = await self.curve_manager.get_curve_state(bonding_curve) + curve_state = await self.curve_manager.get_curve_state( + token_info.bonding_curve + ) token_price_sol = curve_state.calculate_price() token_amount = self.amount / token_price_sol @@ -92,14 +89,16 @@ class TokenBuyer(Trader): f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)" ) - associated_token_account = self.wallet.get_associated_token_address(mint) + associated_token_account = self.wallet.get_associated_token_address( + token_info.mint + ) - await self._ensure_associated_token_account(mint, associated_token_account) + await self._ensure_associated_token_account( + token_info.mint, associated_token_account + ) tx_signature = await self._send_buy_transaction( - mint, - bonding_curve, - associated_bonding_curve, + token_info, associated_token_account, token_amount, max_amount_lamports, @@ -128,7 +127,7 @@ class TokenBuyer(Trader): async def _ensure_associated_token_account( self, mint: Pubkey, associated_token_account: Pubkey ) -> None: - """Ensure associated token account exists. + """Ensure associated token account exists, else create it. Args: mint: Token mint @@ -147,18 +146,14 @@ class TokenBuyer(Trader): payer=self.wallet.pubkey, owner=self.wallet.pubkey, mint=mint ) - # recent_blockhash: Hash = await self.client.get_latest_blockhash() - # create_ata_msg = Message([create_ata_ix], self.wallet.keypair.pubkey()) - # create_ata_tx = Transaction( - # [self.wallet.keypair], create_ata_msg, recent_blockhash - # ) - tx_sig = await self.client.build_and_send_transaction( [create_ata_ix], self.wallet.keypair, skip_preflight=True, max_retries=self.max_retries, - priority_fee=await self.priority_fee_manager.calculate_priority_fee(), # Get priority fee from manager + priority_fee=await self.priority_fee_manager.calculate_priority_fee( + [mint, SystemAddresses.PROGRAM, SystemAddresses.TOKEN_PROGRAM] + ), ) await self.client.confirm_transaction(tx_sig) @@ -176,9 +171,7 @@ class TokenBuyer(Trader): async def _send_buy_transaction( self, - mint: Pubkey, - bonding_curve: Pubkey, - associated_bonding_curve: Pubkey, + token_info: TokenInfo, associated_token_account: Pubkey, token_amount: float, max_amount_lamports: int, @@ -186,9 +179,7 @@ class TokenBuyer(Trader): """Send buy transaction. Args: - mint: Token mint - bonding_curve: Bonding curve address - associated_bonding_curve: Associated bonding curve address + token_info: Token information associated_token_account: User's token account token_amount: Amount of tokens to buy max_amount_lamports: Maximum SOL to spend in lamports @@ -204,10 +195,14 @@ class TokenBuyer(Trader): pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False ), AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), AccountMeta( - pubkey=associated_bonding_curve, is_signer=False, is_writable=True + pubkey=token_info.bonding_curve, is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=token_info.associated_bonding_curve, + is_signer=False, + is_writable=True, ), AccountMeta( pubkey=associated_token_account, is_signer=False, is_writable=True @@ -239,18 +234,15 @@ class TokenBuyer(Trader): ) buy_ix = Instruction(PumpAddresses.PROGRAM, data, accounts) - # Prepare buy transaction data - # recent_blockhash: Hash = await self.client.get_latest_blockhash() - # buy_message = Message([buy_ix], self.wallet.keypair.pubkey()) - # buy_tx = Transaction([self.wallet.keypair], buy_message, recent_blockhash) - try: return await self.client.build_and_send_transaction( [buy_ix], self.wallet.keypair, skip_preflight=True, max_retries=self.max_retries, - priority_fee=await self.priority_fee_manager.calculate_priority_fee(), # Get priority fee from manager + priority_fee=await self.priority_fee_manager.calculate_priority_fee( + self._get_relevant_accounts(token_info) + ), ) except Exception as e: logger.error(f"Buy transaction failed: {str(e)}") diff --git a/src/trading/seller.py b/src/trading/seller.py index 244221d..14da81d 100644 --- a/src/trading/seller.py +++ b/src/trading/seller.py @@ -65,13 +65,10 @@ class TokenSeller(Trader): TradeResult with sell outcome """ try: - # Extract token info - mint = token_info.mint - bonding_curve = token_info.bonding_curve - associated_bonding_curve = token_info.associated_bonding_curve - # Get associated token account - associated_token_account = self.wallet.get_associated_token_address(mint) + associated_token_account = self.wallet.get_associated_token_address( + token_info.mint + ) # Get token balance token_balance = await self.client.get_token_account_balance( @@ -86,7 +83,9 @@ class TokenSeller(Trader): return TradeResult(success=False, error_message="No tokens to sell") # Fetch token price - curve_state = await self.curve_manager.get_curve_state(bonding_curve) + curve_state = await self.curve_manager.get_curve_state( + token_info.bonding_curve + ) token_price_sol = curve_state.calculate_price() logger.info(f"Price per Token: {token_price_sol:.8f} SOL") @@ -106,9 +105,7 @@ class TokenSeller(Trader): ) tx_signature = await self._send_sell_transaction( - mint, - bonding_curve, - associated_bonding_curve, + token_info, associated_token_account, amount, min_sol_output, @@ -136,9 +133,7 @@ class TokenSeller(Trader): async def _send_sell_transaction( self, - mint: Pubkey, - bonding_curve: Pubkey, - associated_bonding_curve: Pubkey, + token_info: TokenInfo, associated_token_account: Pubkey, token_amount: int, min_sol_output: int, @@ -146,9 +141,7 @@ class TokenSeller(Trader): """Send sell transaction. Args: - mint: Token mint - bonding_curve: Bonding curve address - associated_bonding_curve: Associated bonding curve address + mint: Token information associated_token_account: User's token account token_amount: Amount of tokens to sell in raw units min_sol_output: Minimum SOL to receive in lamports @@ -165,10 +158,14 @@ class TokenSeller(Trader): pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False ), AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True), - AccountMeta(pubkey=mint, is_signer=False, is_writable=False), - AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), AccountMeta( - pubkey=associated_bonding_curve, is_signer=False, is_writable=True + pubkey=token_info.bonding_curve, is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=token_info.associated_bonding_curve, + is_signer=False, + is_writable=True, ), AccountMeta( pubkey=associated_token_account, is_signer=False, is_writable=True @@ -201,18 +198,15 @@ class TokenSeller(Trader): ) sell_ix = Instruction(PumpAddresses.PROGRAM, data, accounts) - # Prepare sell transaction data - # recent_blockhash: Hash = await self.client.get_latest_blockhash() - # sell_message = Message([sell_ix], self.wallet.keypair.pubkey()) - # sell_tx = Transaction([self.wallet.keypair], sell_message, recent_blockhash) - try: return await self.client.build_and_send_transaction( [sell_ix], self.wallet.keypair, skip_preflight=True, max_retries=self.max_retries, - priority_fee=await self.priority_fee_manager.calculate_priority_fee(), # Get priority fee from manager + priority_fee=await self.priority_fee_manager.calculate_priority_fee( + self._get_relevant_accounts(token_info) + ), ) except Exception as e: logger.error(f"Sell transaction failed: {str(e)}") From f8d05ae825ab7239651381de51ced4f472922b18 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Fri, 14 Mar 2025 18:25:56 +0000 Subject: [PATCH 18/65] chore(config): update comments and remove default nodes --- cli.py | 8 ++------ config.py | 42 ++++++++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/cli.py b/cli.py index c5228d8..7f0c2dc 100644 --- a/cli.py +++ b/cli.py @@ -62,12 +62,8 @@ async def main() -> None: args = parse_args() # Get configuration values, preferring command line args over config.py - rpc_endpoint: str | None = ( - os.environ.get("SOLANA_NODE_RPC_ENDPOINT") or config.PUBLIC_RPC_ENDPOINT - ) - wss_endpoint: str | None = ( - os.environ.get("SOLANA_NODE_WSS_ENDPOINT") or config.PUBLIC_WSS_ENDPOINT - ) + rpc_endpoint: str | None = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") + wss_endpoint: str | None = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") private_key: str | None = os.environ.get("SOLANA_PRIVATE_KEY") # Validate configuration values diff --git a/config.py b/config.py index 3f075d8..4e499be 100644 --- a/config.py +++ b/config.py @@ -3,37 +3,51 @@ Configuration for the pump.fun trading bot. """ # Trading parameters -BUY_AMOUNT: float = 0.000_001 # Amount of SOL to spend when buying +BUY_AMOUNT: int | float = 0.000_001 # Amount of SOL to spend when buying BUY_SLIPPAGE: float = 0.4 # 40% slippage tolerance for buying SELL_SLIPPAGE: float = 0.4 # 40% slippage tolerance for selling + # Configuration for priority fee settings -ENABLE_DYNAMIC_PRIORITY_FEE: bool = True # Enable dynamic priority fee calculation -ENABLE_FIXED_PRIORITY_FEE: bool = False # Enable fixed priority fee -FIXED_PRIORITY_FEE: int = 50_000 # Fixed priority fee in microlamports +ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Enable dynamic priority fee calculation +ENABLE_FIXED_PRIORITY_FEE: bool = True # Enable fixed priority fee +FIXED_PRIORITY_FEE: int = 2_000 # Fixed priority fee in microlamports EXTRA_PRIORITY_FEE: float = ( - 0.1 # Percentage increase applied to priority fee (0.1 = 10%) + 0.0 # Percentage increase applied to priority fee (0.1 = 10%) ) HARD_CAP_PRIOR_FEE: int = ( 200_000 # Maximum allowed priority fee in microlamports (hard cap) ) + # Retries and timeouts -MAX_RETRIES: int = 2 -WAIT_TIME_AFTER_BUY: int = 15 -WAIT_TIME_BEFORE_NEW_TOKEN: int = 30 -WAIT_TIME_AFTER_CREATION: int = 15 +MAX_RETRIES: int = 10 # Number of retries for transaction sending +# TODO: waiting times will be replaced with retries to shorten delays +WAIT_TIME_AFTER_CREATION: int | float = ( + 15 # Time to wait after token creation (in seconds) + # Too short a delay may cause the RPC node to be unaware of the bonding curve account +) +WAIT_TIME_AFTER_BUY: int | float = ( + 15 # Time to wait after a buy transaction is confirmed (in seconds) + # Acts as a simple holding period + # Too short delay may cause the RPC node to be unaware of account balance +) +WAIT_TIME_BEFORE_NEW_TOKEN: int | float = ( + 5 # Time to wait after a sell transaction is confirmed (in seconds) + # Provides a pause between completed trades, can be set to 0 +) + # Maximum age (in seconds) for a token to be considered "fresh" and eligible for processing. # This threshold is checked before processing starts - tokens older than this are skipped # since they likely contain outdated information from the websocket stream -MAX_TOKEN_AGE: float = 0.1 +MAX_TOKEN_AGE: int | float = 0.1 -# Node provier configuration -# You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes + +# Node provider configuration +# Tested with Chainstack nodes (https://console.chainstack.com), but you can use any node provider +# You can get a trader node https://docs.chainstack.com/docs/solana-trader-nodes MAX_RPS: int = 25 # TODO: not implemented. Max RPS to avoid rate limit errors -PUBLIC_RPC_ENDPOINT = "https://api.mainnet-beta.solana.com" -PUBLIC_WSS_ENDPOINT = "wss://api.mainnet-beta.solana.com" def validate_priority_fee_config() -> None: From 396e9bce1dded5a723941bcbc9843c2ea2362026 Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Sat, 15 Mar 2025 09:23:15 +0100 Subject: [PATCH 19/65] docs: update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a74b95c..7b9446b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Overall, it'll be a gradual development & rollout: * ✅ Lib updates * Error handling * Ability to set RPS in the config to match your provider's and plan RPS (preferably [Chainstack](https://console.chainstack.com/) 🤩) - * Ability to set dynamic priority fees + * ✅ Ability to set dynamic priority fees * Stage 2: Bonding curve and migration management * Integrate `logsSubscribe` instead of `blockSubscribe` for sniping minted tokens into the main bot (currently it's separate in the learning examples section that you can integrate yourself) * Keep both `logsSubscribe` & `blockSubscribe` in the main bot — so that you can try out/choose which one works best for you — plus the Solana node architecture and provders change, so it's useful to have both From 684b1c4b616a8030071b6a6f8651e81ea546ca0e Mon Sep 17 00:00:00 2001 From: smypmsa Date: Mon, 17 Mar 2025 15:47:28 +0000 Subject: [PATCH 20/65] fix: pyproject.toml, module paths --- pyproject.toml | 29 +++++++++++++++++++++++++++- requirements.txt | 9 --------- setup.py | 8 -------- src/__init__.py | 0 cli.py => src/cli.py | 10 +++++++--- config.py => src/config.py | 0 src/core/client.py | 2 +- src/core/curve.py | 6 +++--- src/core/priority_fee/dynamic_fee.py | 4 ++-- src/core/priority_fee/manager.py | 8 ++++---- src/trading/buyer.py | 12 ++++++------ src/trading/seller.py | 12 ++++++------ src/trading/trader.py | 20 +++++++++---------- tests/test_listener.py | 8 +++++--- 14 files changed, 72 insertions(+), 56 deletions(-) delete mode 100644 requirements.txt delete mode 100644 setup.py create mode 100644 src/__init__.py rename cli.py => src/cli.py (95%) rename config.py => src/config.py (100%) diff --git a/pyproject.toml b/pyproject.toml index 15adee2..951a7bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,30 @@ +[project] +name = "pump_bot" +version = "2.0" +description = "Trade tokens on pump.fun" +readme = "README.md" +requires-python = ">=3.9" + +dependencies = [ + "base58>=2.1.1", + "borsh-construct>=0.1.0", + "construct>=2.10.67", + "construct-typing>=0.5.2", + "solana==0.36.6", + "solders>=0.26.0", + "websockets>=15.0", + "python-dotenv>=1.0.1", + "aiohttp>=3.11.13", +] + +[project.optional-dependencies] +dev = [ + "ruff>=0.10.0" +] + +[project.scripts] +pump_bot = "cli:sync_main" + [tool.ruff] line-length = 88 target-version = "py311" @@ -8,4 +35,4 @@ ignore = ["E501"] [tool.ruff.format] quote-style = "double" -indent-style = "space" \ No newline at end of file +indent-style = "space" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c84165e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -base58>=2.1.1 -borsh-construct>=0.1.0 -construct>=2.10.67 -construct-typing>=0.5.2 -solana==0.36.6 -solders>=0.26.0 -websockets>=15.0 -python-dotenv>=1.0.1 -aiohttp==3.11.13 \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 28ea38b..0000000 --- a/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -from setuptools import find_packages, setup - -setup( - name="pump_bot", - version="0.1", - package_dir={"": "src"}, - packages=find_packages(where="src"), -) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli.py b/src/cli.py similarity index 95% rename from cli.py rename to src/cli.py index 7f0c2dc..fe35bdd 100644 --- a/cli.py +++ b/src/cli.py @@ -8,9 +8,9 @@ import asyncio import os import sys -import config -from src.trading.trader import PumpTrader -from src.utils.logger import get_logger, setup_file_logging +import config as config +from trading.trader import PumpTrader +from utils.logger import get_logger, setup_file_logging logger = get_logger(__name__) @@ -122,5 +122,9 @@ async def main() -> None: pass +def sync_main(): + asyncio.run(main()) + + if __name__ == "__main__": asyncio.run(main()) diff --git a/config.py b/src/config.py similarity index 100% rename from config.py rename to src/config.py diff --git a/src/core/client.py b/src/core/client.py index a6d4b3b..7542613 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -18,7 +18,7 @@ from solders.message import Message from solders.pubkey import Pubkey from solders.transaction import Transaction -from src.utils.logger import get_logger +from utils.logger import get_logger logger = get_logger(__name__) diff --git a/src/core/curve.py b/src/core/curve.py index c332170..2ffaa5c 100644 --- a/src/core/curve.py +++ b/src/core/curve.py @@ -8,9 +8,9 @@ from typing import Final from construct import Flag, Int64ul, Struct from solders.pubkey import Pubkey -from src.core.client import SolanaClient -from src.core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS -from src.utils.logger import get_logger +from core.client import SolanaClient +from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS +from utils.logger import get_logger logger = get_logger(__name__) diff --git a/src/core/priority_fee/dynamic_fee.py b/src/core/priority_fee/dynamic_fee.py index b8a1ff8..b04235d 100644 --- a/src/core/priority_fee/dynamic_fee.py +++ b/src/core/priority_fee/dynamic_fee.py @@ -2,9 +2,9 @@ import statistics from solders.pubkey import Pubkey +from core.client import SolanaClient from core.priority_fee import PriorityFeePlugin -from src.core.client import SolanaClient -from src.utils.logger import get_logger +from utils.logger import get_logger logger = get_logger(__name__) diff --git a/src/core/priority_fee/manager.py b/src/core/priority_fee/manager.py index db5b12d..8497cbb 100644 --- a/src/core/priority_fee/manager.py +++ b/src/core/priority_fee/manager.py @@ -1,9 +1,9 @@ from solders.pubkey import Pubkey -from src.core.client import SolanaClient -from src.core.priority_fee.dynamic_fee import DynamicPriorityFee -from src.core.priority_fee.fixed_fee import FixedPriorityFee -from src.utils.logger import get_logger +from core.client import SolanaClient +from core.priority_fee.dynamic_fee import DynamicPriorityFee +from core.priority_fee.fixed_fee import FixedPriorityFee +from utils.logger import get_logger logger = get_logger(__name__) diff --git a/src/trading/buyer.py b/src/trading/buyer.py index c38dfe7..858dde1 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -9,18 +9,18 @@ from solders.instruction import AccountMeta, Instruction from solders.pubkey import Pubkey from spl.token.instructions import create_associated_token_account +from core.client import SolanaClient +from core.curve import BondingCurveManager from core.priority_fee.manager import PriorityFeeManager -from src.core.client import SolanaClient -from src.core.curve import BondingCurveManager -from src.core.pubkeys import ( +from core.pubkeys import ( LAMPORTS_PER_SOL, TOKEN_DECIMALS, PumpAddresses, SystemAddresses, ) -from src.core.wallet import Wallet -from src.trading.base import TokenInfo, Trader, TradeResult -from src.utils.logger import get_logger +from core.wallet import Wallet +from trading.base import TokenInfo, Trader, TradeResult +from utils.logger import get_logger logger = get_logger(__name__) diff --git a/src/trading/seller.py b/src/trading/seller.py index 14da81d..48dc7b1 100644 --- a/src/trading/seller.py +++ b/src/trading/seller.py @@ -8,18 +8,18 @@ from typing import Final from solders.instruction import AccountMeta, Instruction from solders.pubkey import Pubkey +from core.client import SolanaClient +from core.curve import BondingCurveManager from core.priority_fee.manager import PriorityFeeManager -from src.core.client import SolanaClient -from src.core.curve import BondingCurveManager -from src.core.pubkeys import ( +from core.pubkeys import ( LAMPORTS_PER_SOL, TOKEN_DECIMALS, PumpAddresses, SystemAddresses, ) -from src.core.wallet import Wallet -from src.trading.base import TokenInfo, Trader, TradeResult -from src.utils.logger import get_logger +from core.wallet import Wallet +from trading.base import TokenInfo, Trader, TradeResult +from utils.logger import get_logger logger = get_logger(__name__) diff --git a/src/trading/trader.py b/src/trading/trader.py index 55cb9e2..e9def64 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -8,17 +8,17 @@ import json import os from datetime import datetime -import config +import config as config +from core.client import SolanaClient +from core.curve import BondingCurveManager from core.priority_fee.manager import PriorityFeeManager -from src.core.client import SolanaClient -from src.core.curve import BondingCurveManager -from src.core.pubkeys import PumpAddresses -from src.core.wallet import Wallet -from src.monitoring.listener import PumpTokenListener -from src.trading.base import TokenInfo, TradeResult -from src.trading.buyer import TokenBuyer -from src.trading.seller import TokenSeller -from src.utils.logger import get_logger +from core.pubkeys import PumpAddresses +from core.wallet import Wallet +from monitoring.listener import PumpTokenListener +from trading.base import TokenInfo, TradeResult +from trading.buyer import TokenBuyer +from trading.seller import TokenSeller +from utils.logger import get_logger logger = get_logger(__name__) diff --git a/tests/test_listener.py b/tests/test_listener.py index 13a40d8..09a5063 100644 --- a/tests/test_listener.py +++ b/tests/test_listener.py @@ -6,8 +6,11 @@ Tests websocket monitoring for new pump.fun tokens import asyncio import logging import os +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent / "src")) -import config from core.pubkeys import PumpAddresses from monitoring.listener import PumpTokenListener from trading.base import TokenInfo @@ -45,8 +48,7 @@ async def test_pump_token_listener( ): """Test the token listener functionality""" wss_endpoint = os.environ.get( - "SOLANA_NODE_WSS_ENDPOINT", config.PUBLIC_WSS_ENDPOINT - ) + "SOLANA_NODE_WSS_ENDPOINT") logger.info(f"Connecting to WebSocket: {wss_endpoint}") listener = PumpTokenListener(wss_endpoint, PumpAddresses.PROGRAM) callback = TestTokenCallback() From d3985d6bde665b558a7160760ffa54758b4c8058 Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:27:43 +0100 Subject: [PATCH 21/65] docs: added uv instructions --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b9446b..45bc907 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,70 @@ For near-instantaneous transaction propagation, you can use the [Chainstack Sola [Sign up with Chainstack](https://console.chainstack.com). -Make sure you have the required packages installed `pip install -r requirements.txt`. -Make sure you have your endpoints set up in `config.py`. +## 🚀 Getting started + +### Prerequisites +- Install [uv](https://github.com/astral-sh/uv), a fast Python package manager. + +> If Python is already installed, `uv` will detect and use it automatically. + +### Installation + +#### 1️⃣ Install Python (if needed) +```bash +uv python install +``` +> **Why?** `uv` will fetch and install the required Python version for your system. + +#### 2️⃣ Clone the repository +```bash +git clone https://github.com/chainstacklabs/pump-fun-bot.git +cd pump-fun-bot +``` + +#### 3️⃣ Set up a virtual environment +```bash +# Create virtual environment +uv venv + +# Activate (Unix/macOS) +source .venv/bin/activate + +# Activate (Windows) +.venv\Scripts\activate +``` +> Virtual environments help keep dependencies isolated and prevent conflicts. + +#### 4️⃣ Install dependencies +```bash +uv pip install -e . +``` +> **Why `-e` (editable mode)?** Lets you modify the code without reinstalling the package—useful for development! + +#### 5️⃣ Configure the bot +```bash +# Copy example config +cp .env.example .env # Unix/macOS + +# Windows +copy .env.example .env +``` +Edit the `.env` file and add your **Solana RPC endpoints** and **private key**. + +### Running the bot + +#### Option 1: Run as installed package +```bash +pump_bot --help +``` + +#### Option 2: Run directly +```bash +python -m src.cli --help +``` + +> **You're all set! 🎉** Now you can start using the bot. Check `--help` for available commands. 🚀 ## Note on limits From 669f8e8807d750c3a82621d181a4ec20b24bae36 Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:29:00 +0100 Subject: [PATCH 22/65] docs: minor fix in readme --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 45bc907..9c501bb 100644 --- a/README.md +++ b/README.md @@ -107,13 +107,11 @@ Edit the `.env` file and add your **Solana RPC endpoints** and **private key**. ### Running the bot -#### Option 1: Run as installed package ```bash +# Option 1: run as installed package pump_bot --help -``` -#### Option 2: Run directly -```bash +# Option 2: run directly python -m src.cli --help ``` From dacb3d377bfee7058b6758be9d039053d8067ed8 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 18 Mar 2025 16:03:46 +0000 Subject: [PATCH 23/65] feat: add logsSubscribe --- src/cli.py | 7 +- src/config.py | 4 + src/monitoring/base_listener.py | 29 +++ .../{events.py => block_event_processor.py} | 0 .../{listener.py => block_listener.py} | 7 +- src/monitoring/logs_event_processor.py | 152 ++++++++++++++++ src/monitoring/logs_listener.py | 167 ++++++++++++++++++ src/trading/trader.py | 15 +- tests/compare_listeners.py | 137 ++++++++++++++ tests/test_block_listener.py | 97 ++++++++++ ...test_listener.py => test_logs_listener.py} | 25 +-- 11 files changed, 617 insertions(+), 23 deletions(-) create mode 100644 src/monitoring/base_listener.py rename src/monitoring/{events.py => block_event_processor.py} (100%) rename src/monitoring/{listener.py => block_listener.py} (96%) create mode 100644 src/monitoring/logs_event_processor.py create mode 100644 src/monitoring/logs_listener.py create mode 100644 tests/compare_listeners.py create mode 100644 tests/test_block_listener.py rename tests/{test_listener.py => test_logs_listener.py} (78%) diff --git a/src/cli.py b/src/cli.py index fe35bdd..983f880 100644 --- a/src/cli.py +++ b/src/cli.py @@ -88,12 +88,6 @@ async def main() -> None: args.sell_slippage if args.sell_slippage is not None else config.SELL_SLIPPAGE ) - # Not implemented parameters - enable_dynamic_prior__fee = ( - config.ENABLE_DYNAMIC_PRIORITY_FEE - ) # TODO: to be implemented - prior_fee_multiplier = config.EXTRA_PRIORITY_FEE # TODO: to be implemented - trader: PumpTrader = PumpTrader( rpc_endpoint=rpc_endpoint, # type: ignore wss_endpoint=wss_endpoint, # type: ignore @@ -102,6 +96,7 @@ async def main() -> None: buy_slippage=buy_slippage, sell_slippage=sell_slippage, max_retries=config.MAX_RETRIES, + listener_type=config.LISTENER_TYPE, ) try: diff --git a/src/config.py b/src/config.py index 4e499be..a2b8d15 100644 --- a/src/config.py +++ b/src/config.py @@ -20,6 +20,10 @@ HARD_CAP_PRIOR_FEE: int = ( ) +# Listener configuration +LISTENER_TYPE = "block" # Options: "block" or "logs" + + # Retries and timeouts MAX_RETRIES: int = 10 # Number of retries for transaction sending # TODO: waiting times will be replaced with retries to shorten delays diff --git a/src/monitoring/base_listener.py b/src/monitoring/base_listener.py new file mode 100644 index 0000000..0a5e191 --- /dev/null +++ b/src/monitoring/base_listener.py @@ -0,0 +1,29 @@ +""" +Base class for WebSocket token listeners. +""" + +from abc import ABC, abstractmethod +from collections.abc import Awaitable, Callable + +from trading.base import TokenInfo + + +class BaseTokenListener(ABC): + """Base abstract class for token listeners.""" + + @abstractmethod + 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 + """ + pass diff --git a/src/monitoring/events.py b/src/monitoring/block_event_processor.py similarity index 100% rename from src/monitoring/events.py rename to src/monitoring/block_event_processor.py diff --git a/src/monitoring/listener.py b/src/monitoring/block_listener.py similarity index 96% rename from src/monitoring/listener.py rename to src/monitoring/block_listener.py index 7af8647..846b761 100644 --- a/src/monitoring/listener.py +++ b/src/monitoring/block_listener.py @@ -9,15 +9,16 @@ from collections.abc import Awaitable, Callable import websockets from solders.pubkey import Pubkey -from monitoring.events import PumpEventProcessor +from monitoring.base_listener import BaseTokenListener +from monitoring.block_event_processor import PumpEventProcessor from trading.base import TokenInfo from utils.logger import get_logger logger = get_logger(__name__) -class PumpTokenListener: - """WebSocket listener for pump.fun token creation events.""" +class BlockListener(BaseTokenListener): + """WebSocket listener for pump.fun token creation events using blockSubscribe.""" def __init__(self, wss_endpoint: str, pump_program: Pubkey): """Initialize token listener. diff --git a/src/monitoring/logs_event_processor.py b/src/monitoring/logs_event_processor.py new file mode 100644 index 0000000..79b70f0 --- /dev/null +++ b/src/monitoring/logs_event_processor.py @@ -0,0 +1,152 @@ +""" +Event processing for pump.fun tokens using logsSubscribe data. +""" + +import base64 +import struct +from typing import Final + +import base58 +from solders.pubkey import Pubkey + +from core.pubkeys import SystemAddresses +from trading.base import TokenInfo +from utils.logger import get_logger + +logger = get_logger(__name__) + + +class LogsEventProcessor: + """Processes events from pump.fun program logs.""" + + # Discriminator for create instruction to avoid non-create transactions + CREATE_DISCRIMINATOR: Final[int] = 8530921459188068891 + + def __init__(self, pump_program: Pubkey): + """Initialize event processor. + + Args: + pump_program: Pump.fun program address + """ + self.pump_program = pump_program + + def process_program_logs(self, logs: list[str], signature: str) -> TokenInfo | None: + """Process program logs and extract token info. + + Args: + logs: List of log strings from the notification + signature: Transaction signature + + Returns: + TokenInfo if a token creation is found, None otherwise + """ + # 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 + + # Find and process program data + for log in logs: + if "Program data:" in log: + try: + encoded_data = log.split(": ")[1] + decoded_data = base64.b64decode(encoded_data) + print(signature) + 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"]) + associated_curve = self._find_associated_bonding_curve( + mint, bonding_curve + ) + + return TokenInfo( + name=parsed_data["name"], + symbol=parsed_data["symbol"], + uri=parsed_data["uri"], + mint=mint, + bonding_curve=bonding_curve, + associated_bonding_curve=associated_curve, + user=Pubkey.from_string(parsed_data["user"]), + ) + except Exception as e: + logger.error(f"Failed to process log data: {e}") + + return None + + def _parse_create_instruction(self, data: bytes) -> dict | None: + """Parse the create instruction data. + + Args: + data: Raw instruction data + + Returns: + Dictionary of parsed data or None if parsing fails + """ + if len(data) < 8: + return None + + # Check for the correct instruction discriminator + discriminator = struct.unpack(" Pubkey: + """ + Find the associated bonding curve for a given mint and bonding curve. + This uses the standard ATA derivation. + + Args: + mint: Token mint address + bonding_curve: Bonding curve address + + Returns: + Associated bonding curve address + """ + derived_address, _ = Pubkey.find_program_address( + [ + bytes(bonding_curve), + bytes(SystemAddresses.TOKEN_PROGRAM), + bytes(mint), + ], + SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, + ) + return derived_address diff --git a/src/monitoring/logs_listener.py b/src/monitoring/logs_listener.py new file mode 100644 index 0000000..4af072e --- /dev/null +++ b/src/monitoring/logs_listener.py @@ -0,0 +1,167 @@ +""" +WebSocket monitoring for pump.fun tokens using logsSubscribe. +""" + +import asyncio +import json +from collections.abc import Awaitable, Callable + +import websockets +from solders.pubkey import Pubkey + +from monitoring.base_listener import BaseTokenListener +from monitoring.logs_event_processor import LogsEventProcessor +from trading.base import TokenInfo +from utils.logger import get_logger + +logger = get_logger(__name__) + + +class LogsListener(BaseTokenListener): + """WebSocket listener for pump.fun token creation events using logsSubscribe.""" + + 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 = LogsEventProcessor(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 using logsSubscribe. + + 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_logs(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})" + ) + + 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_logs(self, websocket) -> None: + """Subscribe to logs mentioning the pump.fun program. + + Args: + websocket: Active WebSocket connection + """ + subscription_message = json.dumps( + { + "jsonrpc": "2.0", + "id": 1, + "method": "logsSubscribe", + "params": [ + {"mentions": [str(self.pump_program)]}, + {"commitment": "processed"}, + ], + } + ) + + await websocket.send(subscription_message) + logger.info(f"Subscribed to logs mentioning program: {self.pump_program}") + + # Wait for subscription confirmation + response = await websocket.recv() + response_data = json.loads(response) + if "result" in response_data: + logger.info(f"Subscription confirmed with ID: {response_data['result']}") + else: + logger.warning(f"Unexpected subscription response: {response}") + + 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) + try: + pong_waiter = await websocket.ping() + await asyncio.wait_for(pong_waiter, timeout=10) + except asyncio.TimeoutError: + logger.warning("Ping timeout - server not responding") + # Force reconnection + await websocket.close() + return + except asyncio.CancelledError: + pass + except Exception as e: + logger.error(f"Ping error: {str(e)}") + + async def _wait_for_token_creation(self, websocket) -> TokenInfo | None: + try: + response = await asyncio.wait_for(websocket.recv(), timeout=30) + data = json.loads(response) + + if "method" not in data or data["method"] != "logsNotification": + return None + + log_data = data["params"]["result"]["value"] + logs = log_data.get("logs", []) + signature = log_data.get("signature", "unknown") + + # Use the processor to extract token info + return self.event_processor.process_program_logs(logs, signature) + + 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 diff --git a/src/trading/trader.py b/src/trading/trader.py index e9def64..6204217 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -14,7 +14,8 @@ from core.curve import BondingCurveManager from core.priority_fee.manager import PriorityFeeManager from core.pubkeys import PumpAddresses from core.wallet import Wallet -from monitoring.listener import PumpTokenListener +from monitoring.block_listener import BlockListener +from monitoring.logs_listener import LogsListener from trading.base import TokenInfo, TradeResult from trading.buyer import TokenBuyer from trading.seller import TokenSeller @@ -35,6 +36,7 @@ class PumpTrader: buy_slippage: float, sell_slippage: float, max_retries: int = 5, + listener_type: str = "block", # Add this parameter ): """Initialize the pump trader. @@ -46,6 +48,7 @@ class PumpTrader: buy_slippage: Slippage tolerance for buys sell_slippage: Slippage tolerance for sells max_retries: Maximum number of retry attempts + listener_type: Type of listener to use ('block' or 'logs') """ self.solana_client = SolanaClient(rpc_endpoint) self.wallet = Wallet(private_key) @@ -79,7 +82,13 @@ class PumpTrader: max_retries, ) - self.token_listener = PumpTokenListener(wss_endpoint, PumpAddresses.PROGRAM) + # Initialize the appropriate listener type + if listener_type.lower() == "logs": + self.token_listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM) + logger.info("Using logsSubscribe listener for token monitoring") + else: + self.token_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM) + logger.info("Using blockSubscribe listener for token monitoring") self.buy_amount = buy_amount self.buy_slippage = buy_slippage @@ -92,7 +101,7 @@ class PumpTrader: self.processing = False self.processed_tokens: set[str] = set() self.token_timestamps: dict[str, float] = {} - + async def start( self, match_string: str | None = None, diff --git a/tests/compare_listeners.py b/tests/compare_listeners.py new file mode 100644 index 0000000..809b7c7 --- /dev/null +++ b/tests/compare_listeners.py @@ -0,0 +1,137 @@ +""" +Test script to compare BlockListener and LogsListener +Runs both listeners simultaneously to compare their performance +""" + +import asyncio +import logging +import os +import sys +import time +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent / "src")) + +from core.pubkeys import PumpAddresses +from monitoring.block_listener import BlockListener +from monitoring.logs_listener import LogsListener +from trading.base import TokenInfo + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("listener-comparison") + + +class TimingTokenCallback: + def __init__(self, name: str): + self.name = name + self.detected_tokens = [] + self.detection_times = {} + + async def on_token_created(self, token_info: TokenInfo) -> None: + """Process detected token with timing information""" + token_key = str(token_info.mint) + detection_time = time.time() + + self.detected_tokens.append(token_info) + self.detection_times[token_key] = detection_time + + logger.info(f"[{self.name}] Detected: {token_info.name} ({token_info.symbol})") + print(f"\n{'=' * 50}") + print(f"[{self.name}] NEW TOKEN: {token_info.name}") + print(f"Symbol: {token_info.symbol}") + print(f"Mint: {token_info.mint}") + print(f"Detection time: {detection_time}") + print(f"{'=' * 50}\n") + + +async def run_comparison(test_duration: int = 300): + """Run both listeners and compare their performance""" + wss_endpoint = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") + if not wss_endpoint: + logger.error("SOLANA_NODE_WSS_ENDPOINT environment variable is not set") + return + + logger.info(f"Connecting to WebSocket: {wss_endpoint}") + + block_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM) + logs_listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM) + + block_callback = TimingTokenCallback("BlockListener") + logs_callback = TimingTokenCallback("LogsListener") + + logger.info("Starting both listeners...") + block_task = asyncio.create_task( + block_listener.listen_for_tokens(block_callback.on_token_created) + ) + logs_task = asyncio.create_task( + logs_listener.listen_for_tokens(logs_callback.on_token_created) + ) + + logger.info(f"Comparison running for {test_duration} seconds...") + try: + await asyncio.sleep(test_duration) + except KeyboardInterrupt: + logger.info("Test interrupted by user") + finally: + block_task.cancel() + logs_task.cancel() + try: + await asyncio.gather(block_task, logs_task, return_exceptions=True) + except asyncio.CancelledError: + pass + + logger.info(f"BlockListener detected {len(block_callback.detected_tokens)} tokens") + logger.info(f"LogsListener detected {len(logs_callback.detected_tokens)} tokens") + + # Find tokens detected by both listeners + block_mints = {str(token.mint) for token in block_callback.detected_tokens} + logs_mints = {str(token.mint) for token in logs_callback.detected_tokens} + common_mints = block_mints.intersection(logs_mints) + + logger.info(f"Tokens detected by both listeners: {len(common_mints)}") + + # Compare detection times for common tokens + if common_mints: + logger.info("\nPerformance comparison for tokens detected by both listeners:") + logger.info("Token Mint | BlockListener Time | LogsListener Time | Difference (ms)") + logger.info("-" * 80) + + for mint in common_mints: + block_time = block_callback.detection_times.get(mint) + logs_time = logs_callback.detection_times.get(mint) + + if block_time and logs_time: + diff_ms = abs(block_time - logs_time) * 1000 # Convert to milliseconds + faster = "BlockListener" if block_time < logs_time else "LogsListener" + + logger.info(f"{mint[:10]}... | {block_time:.6f} | {logs_time:.6f} | {diff_ms:.2f}ms ({faster} faster)") + + # Report tokens only detected by one listener + block_only = block_mints - logs_mints + logs_only = logs_mints - block_mints + + if block_only: + logger.info(f"\nTokens only detected by BlockListener: {len(block_only)}") + for mint in block_only: + logger.info(f" - {mint}") + + if logs_only: + logger.info(f"\nTokens only detected by LogsListener: {len(logs_only)}") + for mint in logs_only: + logger.info(f" - {mint}") + + +if __name__ == "__main__": + test_duration = 30 # seconds + + if len(sys.argv) > 1: + try: + test_duration = int(sys.argv[1]) + except ValueError: + logger.error(f"Invalid test duration: {sys.argv[1]}. Using default of {test_duration} seconds.") + + logger.info("Starting listener comparison test") + logger.info(f"Will run for {test_duration} seconds") + asyncio.run(run_comparison(test_duration)) diff --git a/tests/test_block_listener.py b/tests/test_block_listener.py new file mode 100644 index 0000000..9648f21 --- /dev/null +++ b/tests/test_block_listener.py @@ -0,0 +1,97 @@ +""" +Test script for BlockListener +Tests websocket monitoring for new pump.fun tokens using blockSubscribe +""" + +import asyncio +import logging +import os +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent / "src")) + +from core.pubkeys import PumpAddresses +from monitoring.block_listener import BlockListener +from trading.base import TokenInfo + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("block-listener-test") + + +class TestTokenCallback: + def __init__(self): + self.detected_tokens = [] + + async def on_token_created(self, token_info: TokenInfo) -> None: + """Process detected token""" + logger.info(f"New token detected: {token_info.name} ({token_info.symbol})") + logger.info(f"Mint: {token_info.mint}") + self.detected_tokens.append(token_info) + print(f"\n{'=' * 50}") + print(f"NEW TOKEN: {token_info.name}") + print(f"Symbol: {token_info.symbol}") + print(f"Mint: {token_info.mint}") + print(f"URI: {token_info.uri}") + print(f"Creator: {token_info.user}") + print(f"Bonding Curve: {token_info.bonding_curve}") + print(f"Associated Bonding Curve: {token_info.associated_bonding_curve}") + print(f"{'=' * 50}\n") + + +async def test_block_listener( + match_string: str | None = None, + creator_address: str | None = None, + test_duration: int = 60, +): + """Test the block listener functionality""" + wss_endpoint = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") + if not wss_endpoint: + logger.error("SOLANA_NODE_WSS_ENDPOINT environment variable is not set") + return [] + + logger.info(f"Connecting to WebSocket: {wss_endpoint}") + listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM) + callback = TestTokenCallback() + + if match_string: + logger.info(f"Filtering tokens matching: {match_string}") + if creator_address: + logger.info(f"Filtering tokens by creator: {creator_address}") + + listen_task = asyncio.create_task( + listener.listen_for_tokens( + callback.on_token_created, + match_string=match_string, + creator_address=creator_address, + ) + ) + + logger.info(f"Listening for {test_duration} seconds...") + try: + await asyncio.sleep(test_duration) + except KeyboardInterrupt: + logger.info("Test interrupted by user") + finally: + listen_task.cancel() + try: + await listen_task + except asyncio.CancelledError: + pass + + logger.info(f"Detected {len(callback.detected_tokens)} tokens") + for token in callback.detected_tokens: + logger.info(f" - {token.name} ({token.symbol}): {token.mint}") + + return callback.detected_tokens + + +if __name__ == "__main__": + match_string = None # Update if you want to filter tokens by name/symbol + creator_address = None # Update if you want to filter tokens by creator address + test_duration = 15 + + logger.info("Starting block listener test (using blockSubscribe)") + asyncio.run(test_block_listener(match_string, creator_address, test_duration)) diff --git a/tests/test_listener.py b/tests/test_logs_listener.py similarity index 78% rename from tests/test_listener.py rename to tests/test_logs_listener.py index 09a5063..8dcc05f 100644 --- a/tests/test_listener.py +++ b/tests/test_logs_listener.py @@ -1,6 +1,6 @@ """ -Test script for PumpTokenListener -Tests websocket monitoring for new pump.fun tokens +Test script for LogsListener +Tests websocket monitoring for new pump.fun tokens using logsSubscribe """ import asyncio @@ -12,13 +12,13 @@ from pathlib import Path sys.path.append(str(Path(__file__).parent.parent / "src")) from core.pubkeys import PumpAddresses -from monitoring.listener import PumpTokenListener +from monitoring.logs_listener import LogsListener from trading.base import TokenInfo logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) -logger = logging.getLogger("token-listener-test") +logger = logging.getLogger("logs-listener-test") class TestTokenCallback: @@ -41,16 +41,19 @@ class TestTokenCallback: print(f"{'=' * 50}\n") -async def test_pump_token_listener( +async def test_logs_listener( match_string: str | None = None, creator_address: str | None = None, test_duration: int = 60, ): - """Test the token listener functionality""" - wss_endpoint = os.environ.get( - "SOLANA_NODE_WSS_ENDPOINT") + """Test the logs listener functionality""" + wss_endpoint = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") + if not wss_endpoint: + logger.error("SOLANA_NODE_WSS_ENDPOINT environment variable is not set") + return [] + logger.info(f"Connecting to WebSocket: {wss_endpoint}") - listener = PumpTokenListener(wss_endpoint, PumpAddresses.PROGRAM) + listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM) callback = TestTokenCallback() if match_string: @@ -90,5 +93,5 @@ if __name__ == "__main__": creator_address = None # Update if you want to filter tokens by creator address test_duration = 15 - logger.info("Starting token listener test") - asyncio.run(test_pump_token_listener(match_string, creator_address, test_duration)) + logger.info("Starting logs listener test (using logsSubscribe)") + asyncio.run(test_logs_listener(match_string, creator_address, test_duration)) From 3cc0e38b37f168703cd3ffddc2c4fd090b6106ae Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 18 Mar 2025 16:08:01 +0000 Subject: [PATCH 24/65] fix: removed print --- src/monitoring/logs_event_processor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/monitoring/logs_event_processor.py b/src/monitoring/logs_event_processor.py index 79b70f0..9aff061 100644 --- a/src/monitoring/logs_event_processor.py +++ b/src/monitoring/logs_event_processor.py @@ -54,7 +54,6 @@ class LogsEventProcessor: try: encoded_data = log.split(": ")[1] decoded_data = base64.b64decode(encoded_data) - print(signature) parsed_data = self._parse_create_instruction(decoded_data) if parsed_data and "name" in parsed_data: From e6236a9a86454d13be8138a36b45d2d1b201ab81 Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Wed, 19 Mar 2025 08:13:07 +0100 Subject: [PATCH 25/65] docs: update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9c501bb..38e4048 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ Overall, it'll be a gradual development & rollout: * Ability to set RPS in the config to match your provider's and plan RPS (preferably [Chainstack](https://console.chainstack.com/) 🤩) * ✅ Ability to set dynamic priority fees * Stage 2: Bonding curve and migration management - * Integrate `logsSubscribe` instead of `blockSubscribe` for sniping minted tokens into the main bot (currently it's separate in the learning examples section that you can integrate yourself) - * Keep both `logsSubscribe` & `blockSubscribe` in the main bot — so that you can try out/choose which one works best for you — plus the Solana node architecture and provders change, so it's useful to have both + * ✅ Integrate `logsSubscribe` instead of `blockSubscribe` for sniping minted tokens into the main bot (currently it's separate in the learning examples section that you can integrate yourself) + * ✅ Keep both `logsSubscribe` & `blockSubscribe` in the main bot — so that you can try out/choose which one works best for you — plus the Solana node architecture and provders change, so it's useful to have both * Do retries instead of cooldown and/or keep the cooldown * Checking a bonding curve status progress. As in to predict how soon a token will start the migration process. * Script to close the associated bonding curve account if the rest of the flow txs fails From ca91c879c3427b04c4e24a4da963790b90980bc3 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Wed, 19 Mar 2025 16:01:46 +0000 Subject: [PATCH 26/65] fix: align learning examples with refactored modules --- .../blockSubscribe_extract_transactions.py | 15 +++--- .../check_boding_curve_status.py | 7 ++- .../compute_associated_bonding_curve.py | 13 +++--- .../decode_from_blockSubscribe.py | 25 +++++----- .../decode_from_getAccountInfo.py | 2 +- .../decode_from_getTransaction.py | 17 ++++--- learning-examples/fetch_price.py | 3 +- .../listen_create_from_blocksubscribe.py | 20 ++++---- learning-examples/listen_new_direct.py | 15 ++++-- .../listen_new_direct_full_details.py | 25 +++++----- .../listen_to_raydium_migration.py | 19 +++++--- learning-examples/manual_buy.py | 8 ++-- learning-examples/manual_sell.py | 10 ++-- pyproject.toml | 46 ++++++++++++++++++- 14 files changed, 141 insertions(+), 84 deletions(-) diff --git a/learning-examples/blockSubscribe_extract_transactions.py b/learning-examples/blockSubscribe_extract_transactions.py index 474f94b..671fb49 100644 --- a/learning-examples/blockSubscribe_extract_transactions.py +++ b/learning-examples/blockSubscribe_extract_transactions.py @@ -7,7 +7,10 @@ import sys import websockets sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from config import PUMP_PROGRAM, WSS_ENDPOINT + +from core.pubkeys import PumpAddresses + +WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") async def save_transaction(tx_data, tx_signature): @@ -27,7 +30,7 @@ async def listen_for_transactions(): "id": 1, "method": "blockSubscribe", "params": [ - {"mentionsAccountOrProgram": str(PUMP_PROGRAM)}, + {"mentionsAccountOrProgram": str(PumpAddresses.PROGRAM)}, { "commitment": "confirmed", "encoding": "base64", @@ -36,10 +39,10 @@ async def listen_for_transactions(): "maxSupportedTransactionVersion": 0, }, ], - } + }, ) await websocket.send(subscription_message) - print(f"Subscribed to blocks mentioning program: {PUMP_PROGRAM}") + print(f"Subscribed to blocks mentioning program: {PumpAddresses.PROGRAM}") while True: try: @@ -71,9 +74,9 @@ async def listen_for_transactions(): continue await save_transaction(tx, tx_signature) elif "result" in data: - print(f"Subscription confirmed") + print("Subscription confirmed") except Exception as e: - print(f"An error occurred: {str(e)}") + print(f"An error occurred: {e!s}") if __name__ == "__main__": diff --git a/learning-examples/check_boding_curve_status.py b/learning-examples/check_boding_curve_status.py index 86a97ca..dfc1cb3 100644 --- a/learning-examples/check_boding_curve_status.py +++ b/learning-examples/check_boding_curve_status.py @@ -10,11 +10,14 @@ from solana.rpc.async_api import AsyncClient from solders.pubkey import Pubkey sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from config import PUMP_PROGRAM, RPC_ENDPOINT + +from core.pubkeys import PumpAddresses # Constants EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" None: # Get the associated bonding curve address bonding_curve_address, bump = get_associated_bonding_curve_address( - mint, PUMP_PROGRAM + mint, PumpAddresses.PROGRAM ) print("\nToken Status:") diff --git a/learning-examples/compute_associated_bonding_curve.py b/learning-examples/compute_associated_bonding_curve.py index d354eb1..12343ec 100644 --- a/learning-examples/compute_associated_bonding_curve.py +++ b/learning-examples/compute_associated_bonding_curve.py @@ -4,7 +4,8 @@ import sys from solders.pubkey import Pubkey sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from config import PUMP_PROGRAM + +from core.pubkeys import PumpAddresses, SystemAddresses def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> tuple[Pubkey, int]: @@ -19,16 +20,14 @@ def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey Find the associated bonding curve for a given mint and bonding curve. This uses the standard ATA derivation. """ - from config import SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM as ATA_PROGRAM_ID - from config import SYSTEM_TOKEN_PROGRAM as TOKEN_PROGRAM_ID derived_address, _ = Pubkey.find_program_address( [ bytes(bonding_curve), - bytes(TOKEN_PROGRAM_ID), + bytes(SystemAddresses.TOKEN_PROGRAM), bytes(mint), ], - ATA_PROGRAM_ID, + SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, ) return derived_address @@ -39,7 +38,9 @@ def main(): try: mint = Pubkey.from_string(mint_address) - bonding_curve_address, bump = get_bonding_curve_address(mint, PUMP_PROGRAM) + bonding_curve_address, bump = get_bonding_curve_address( + mint, PumpAddresses.PROGRAM + ) # Calculate the associated bonding curve associated_bonding_curve = find_associated_bonding_curve( diff --git a/learning-examples/decode_from_blockSubscribe.py b/learning-examples/decode_from_blockSubscribe.py index 4124568..1bebc96 100644 --- a/learning-examples/decode_from_blockSubscribe.py +++ b/learning-examples/decode_from_blockSubscribe.py @@ -4,18 +4,16 @@ import json import struct import sys -from solana.transaction import Transaction -from solders.pubkey import Pubkey -from solders.transaction import VersionedTransaction +from solders.transaction import Transaction, VersionedTransaction def load_idl(file_path): - with open(file_path, "r") as f: + with open(file_path) as f: return json.load(f) def load_transaction(file_path): - with open(file_path, "r") as f: + with open(file_path) as f: data = json.load(f) return data @@ -67,8 +65,8 @@ def decode_transaction(tx_data, idl): print("Versioned transaction detected") else: # Use legacy deserialization for older transactions - transaction = Transaction.deserialize(tx_data_decoded) - instructions = transaction.instructions + transaction = Transaction.from_bytes(tx_data_decoded) + instructions = transaction.message.instructions account_keys = transaction.message.account_keys print("Legacy transaction detected") @@ -137,12 +135,15 @@ def decode_transaction(tx_data, idl): return decoded_instructions -if len(sys.argv) != 2: - print("Usage: python decode_fromBlock.py ") - sys.exit(1) +tx_file_path = "" -tx_file_path = sys.argv[1] -idl = load_idl("../idl/pump_fun_idl.json") +if len(sys.argv) != 2: + tx_file_path = "learning-examples/blockSubscribe-transactions/raw_create_tx_from_blockSubscribe.json" + print(f"No path provided, using the path: {tx_file_path}") +else: + tx_file_path = sys.argv[1] + +idl = load_idl("idl/pump_fun_idl.json") tx_data = load_transaction(tx_file_path) decoded_instructions = decode_transaction(tx_data, idl) diff --git a/learning-examples/decode_from_getAccountInfo.py b/learning-examples/decode_from_getAccountInfo.py index 1a62ac3..c342607 100644 --- a/learning-examples/decode_from_getAccountInfo.py +++ b/learning-examples/decode_from_getAccountInfo.py @@ -41,7 +41,7 @@ def decode_bonding_curve_data(raw_data: str) -> BondingCurveState: # Load the JSON data -with open("raw_bondingCurve_from_getAccountInfo.json", "r") as file: +with open("learning-examples/raw_bondingCurve_from_getAccountInfo.json", "r") as file: json_data = json.load(file) # Extract the base64 encoded data diff --git a/learning-examples/decode_from_getTransaction.py b/learning-examples/decode_from_getTransaction.py index 5be9b6d..630d8f3 100644 --- a/learning-examples/decode_from_getTransaction.py +++ b/learning-examples/decode_from_getTransaction.py @@ -1,24 +1,23 @@ -import base64 import json import struct import sys import base58 -from solana.transaction import Transaction -from solders.pubkey import Pubkey + +tx_file_path = "" if len(sys.argv) != 2: - print("Usage: python decode_getTransaction.py ") - sys.exit(1) - -tx_file_path = sys.argv[1] + tx_file_path = "learning-examples/raw_buy_tx_from_getTransaction.json" + print(f"No path provided, using the path: {tx_file_path}") +else: + tx_file_path = sys.argv[1] # Load the IDL -with open("../idl/pump_fun_idl.json", "r") as f: +with open("idl/pump_fun_idl.json") as f: idl = json.load(f) # Load the transaction log -with open(tx_file_path, "r") as f: +with open(tx_file_path) as f: tx_log = json.load(f) # Extract the transaction data diff --git a/learning-examples/fetch_price.py b/learning-examples/fetch_price.py index e2eb460..ac8a1b9 100644 --- a/learning-examples/fetch_price.py +++ b/learning-examples/fetch_price.py @@ -9,7 +9,6 @@ from solana.rpc.async_api import AsyncClient from solders.pubkey import Pubkey sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from config import RPC_ENDPOINT LAMPORTS_PER_SOL: Final[int] = 1_000_000_000 TOKEN_DECIMALS: Final[int] = 6 @@ -18,6 +17,8 @@ CURVE_ADDRESS: Final[str] = "6GXfUqrmPM4VdN1NoDZsE155jzRegJngZRjMkGyby7do" # Here and later all the discriminators are precalculated. See learning-examples/discriminator.py EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" Pubkey: @@ -28,16 +23,16 @@ def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey derived_address, _ = Pubkey.find_program_address( [ bytes(bonding_curve), - bytes(TOKEN_PROGRAM_ID), + bytes(SystemAddresses.TOKEN_PROGRAM), bytes(mint), ], - ATA_PROGRAM_ID, + SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, ) return derived_address # Load the IDL JSON file -with open("../idl/pump_fun_idl.json", "r") as f: +with open("idl/pump_fun_idl.json") as f: idl = json.load(f) # Extract the "create" instruction definition @@ -102,13 +97,15 @@ async def listen_for_new_tokens(): "id": 1, "method": "logsSubscribe", "params": [ - {"mentions": [str(PUMP_PROGRAM)]}, + {"mentions": [str(PumpAddresses.PROGRAM)]}, {"commitment": "processed"}, ], } ) await websocket.send(subscription_message) - print(f"Listening for new token creations from program: {PUMP_PROGRAM}") + print( + f"Listening for new token creations from program: {PumpAddresses.PROGRAM}" + ) # Wait for subscription confirmation response = await websocket.recv() @@ -165,7 +162,7 @@ async def listen_for_new_tokens(): ) except Exception as e: print(f"Failed to decode: {log}") - print(f"Error: {str(e)}") + print(f"Error: {e!s}") except Exception as e: print(f"An error occurred while processing message: {e}") diff --git a/learning-examples/listen_to_raydium_migration.py b/learning-examples/listen_to_raydium_migration.py index b57493e..4d3e823 100644 --- a/learning-examples/listen_to_raydium_migration.py +++ b/learning-examples/listen_to_raydium_migration.py @@ -1,14 +1,15 @@ import asyncio -import base64 import json import os import sys import websockets -from solders.pubkey import Pubkey sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from config import PUMP_LIQUIDITY_MIGRATOR, WSS_ENDPOINT + +from core.pubkeys import PumpAddresses + +WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") def process_initialize2_transaction(data): @@ -32,7 +33,7 @@ def process_initialize2_transaction(data): print(f"\nError: Not enough account keys (found {len(account_keys)})") except Exception as e: - print(f"\nError: {str(e)}") + print(f"\nError: {e!s}") async def listen_for_events(): @@ -45,7 +46,11 @@ async def listen_for_events(): "id": 1, "method": "blockSubscribe", "params": [ - {"mentionsAccountOrProgram": str(PUMP_LIQUIDITY_MIGRATOR)}, + { + "mentionsAccountOrProgram": str( + PumpAddresses.LIQUIDITY_MIGRATOR + ) + }, { "commitment": "confirmed", "encoding": "json", @@ -93,13 +98,13 @@ async def listen_for_events(): process_initialize2_transaction(tx) break - except asyncio.TimeoutError: + except TimeoutError: print("\nChecking connection...") print("Connection alive") continue except Exception as e: - print(f"\nConnection error: {str(e)}") + print(f"\nConnection error: {e!s}") print("Retrying in 5 seconds...") await asyncio.sleep(5) diff --git a/learning-examples/manual_buy.py b/learning-examples/manual_buy.py index 91d8616..cb00e29 100644 --- a/learning-examples/manual_buy.py +++ b/learning-examples/manual_buy.py @@ -12,10 +12,10 @@ from construct import Flag, Int64ul, Struct from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts -from solana.transaction import Message 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.system_program import TransferParams, transfer from solders.transaction import Transaction, VersionedTransaction @@ -42,8 +42,8 @@ SOL = Pubkey.from_string("So11111111111111111111111111111111111111112") LAMPORTS_PER_SOL = 1_000_000_000 # RPC ENDPOINTS -RPC_ENDPOINT = "ENTER_YOUR_CHAINSTACK_HTTP_ENDPOINT" -RPC_WEBSOCKET = "ENTER_YOUR_CHAINSTACK_WS_ENDPOINT" +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") +RPC_WEBSOCKET = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") class BondingCurveState: @@ -92,7 +92,7 @@ async def buy_token( slippage: float = 0.25, max_retries=5, ): - private_key = base58.b58decode("ENTER_PRIVATE_KEY") + private_key = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) payer = Keypair.from_bytes(private_key) async with AsyncClient(RPC_ENDPOINT) as client: diff --git a/learning-examples/manual_sell.py b/learning-examples/manual_sell.py index e63eb51..ab4aa61 100644 --- a/learning-examples/manual_sell.py +++ b/learning-examples/manual_sell.py @@ -1,19 +1,17 @@ import asyncio -import base64 -import json +import os import struct import base58 -import spl.token.instructions as spl_token from construct import Flag, Int64ul, Struct from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import TxOpts -from solana.transaction import Transaction from solders.instruction import AccountMeta, Instruction from solders.keypair import Keypair from solders.pubkey import Pubkey from solders.system_program import TransferParams, transfer +from solders.transaction import Transaction from spl.token.instructions import get_associated_token_address # Here and later all the discriminators are precalculated. See learning-examples/discriminator.py @@ -39,7 +37,7 @@ UNIT_PRICE = 10_000_000 UNIT_BUDGET = 100_000 # RPC endpoint -RPC_ENDPOINT = "SOLANA_NODE_RPC_ENDPOINT" +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") class BondingCurveState: @@ -94,7 +92,7 @@ async def sell_token( slippage: float = 0.25, max_retries=5, ): - private_key = base58.b58decode("SOLANA_PRIVATE_KEY") + private_key = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) payer = Keypair.from_bytes(private_key) async with AsyncClient(RPC_ENDPOINT) as client: diff --git a/pyproject.toml b/pyproject.toml index 951a7bd..e28e445 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,13 +26,57 @@ dev = [ pump_bot = "cli:sync_main" [tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + line-length = 88 +indent-width = 4 target-version = "py311" [tool.ruff.lint] -select = ["E", "F", "I", "UP", "N", "B", "A", "C4", "T10", "ARG", "PTH"] +select = [ + "E", "F", "I", "UP", "N", "B", "A", "C4", "T10", "ARG", "PTH", + "ANN", # type annotations + "S", # security best practices + "BLE", # blind except statements + "FBT", # boolean trap parameters + "C90", # complexity metrics + "TRY", # exception handling best practices + "SLF", # private member access + "TCH", # type checking issues + "RUF", # Ruff-specific rules + "ERA", # eradicate commented-out code + "PL", # pylint conventions +] ignore = ["E501"] [tool.ruff.format] quote-style = "double" indent-style = "space" +line-ending = "auto" \ No newline at end of file From 9268d7aaa9425070b62aad121d1bfcb05cb93a30 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Wed, 19 Mar 2025 21:09:26 +0000 Subject: [PATCH 27/65] fix: remove unused imports, update sell tx in learning --- learning-examples/manual_buy.py | 7 +++---- learning-examples/manual_sell.py | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/learning-examples/manual_buy.py b/learning-examples/manual_buy.py index cb00e29..53b6b7e 100644 --- a/learning-examples/manual_buy.py +++ b/learning-examples/manual_buy.py @@ -17,7 +17,6 @@ from solders.instruction import AccountMeta, Instruction from solders.keypair import Keypair from solders.message import Message from solders.pubkey import Pubkey -from solders.system_program import TransferParams, transfer from solders.transaction import Transaction, VersionedTransaction from spl.token.instructions import get_associated_token_address @@ -148,7 +147,7 @@ async def buy_token( break except Exception as e: print( - f"Attempt {ata_attempt + 1} to create associated token account failed: {str(e)}" + f"Attempt {ata_attempt + 1} to create associated token account failed: {e!s}" ) if ata_attempt < max_retries - 1: wait_time = 2**ata_attempt @@ -235,7 +234,7 @@ async def buy_token( def load_idl(file_path): - with open(file_path, "r") as f: + with open(file_path) as f: return json.load(f) @@ -370,7 +369,7 @@ async def main(): token_price_sol = calculate_pump_curve_price(curve_state) # Amount of SOL to spend (adjust as needed) - amount = 0.00001 # 0.00001 SOL + amount = 0.000_001 # 0.00001 SOL slippage = 0.3 # 30% slippage tolerance print(f"Bonding curve address: {bonding_curve}") diff --git a/learning-examples/manual_sell.py b/learning-examples/manual_sell.py index ab4aa61..333cd5c 100644 --- a/learning-examples/manual_sell.py +++ b/learning-examples/manual_sell.py @@ -7,10 +7,11 @@ from construct import 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.system_program import TransferParams, transfer from solders.transaction import Transaction from spl.token.instructions import get_associated_token_address @@ -170,14 +171,13 @@ async def sell_token( ) sell_ix = Instruction(PUMP_PROGRAM, data, accounts) - recent_blockhash = await client.get_latest_blockhash() - transaction = Transaction() - transaction.add(sell_ix) - transaction.recent_blockhash = recent_blockhash.value.blockhash - + msg = Message([set_compute_unit_price(1_000), sell_ix], payer.pubkey()) tx = await client.send_transaction( - transaction, - payer, + Transaction( + [payer], + msg, + (await client.get_latest_blockhash()).value.blockhash, + ), opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed), ) @@ -188,7 +188,7 @@ async def sell_token( return # Success, exit the function except Exception as e: - print(f"Attempt {attempt + 1} failed: {str(e)}") + print(f"Attempt {attempt + 1} failed: {e!s}") if attempt < max_retries - 1: wait_time = 2**attempt # Exponential backoff print(f"Retrying in {wait_time} seconds...") @@ -199,10 +199,10 @@ async def sell_token( async def main(): # Replace these with the actual values for the token you want to sell - mint = Pubkey.from_string("EyLuyWV5N1GVSqLLeumFDWYkmmSkLED5s2xqu37Lpump") - bonding_curve = Pubkey.from_string("AGZLYVwmGL9cXCLN4C3ki9hXGix1kXYjr59B2H7jwRMQ") + mint = Pubkey.from_string("7aXYndxpkHRU9xg1hyAE6z3X3KYPc2LR3dJMzYTSpump") + bonding_curve = Pubkey.from_string("5UYdCZigGDyAh1doCW8FdnA7VwJTJePayTLCyZkAWMxg") associated_bonding_curve = Pubkey.from_string( - "74H2bo2hjNnWgf9oDHzsVsu3mcsoiYYWJ3s9dVnL5erV" + "BS2RUaPXjQpfzncizvmEfnARZG6SNp1imXnv5USA7PMA" ) slippage = 0.25 # 25% slippage tolerance From 1b9c3c5b4b1e169d48834297eefd66e2d92853bc Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:39:31 +0100 Subject: [PATCH 28/65] docs: sync readme with main --- README.md | 63 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 38e4048..402ae9c 100644 --- a/README.md +++ b/README.md @@ -12,41 +12,48 @@ Not everyone is a scammer though, sometimes there are helpful outside devs who c **>>END OF WARNING<<** -**>> FURTHER ROADMAP <<** +Development ongoing in the [refactored/main-v2](https://github.com/chainstacklabs/pump-fun-bot/commits/refactored/main-v2/) branch. -Hey guys, starting from the next week (**week of March 10**) we'll be rolling out updates to the bot v2 based on the feedback and the reported issues, including updating to the latest libs, better error handling etc. We are already actively working on it. +As of March 21, 2025, the bot from the **refactored/main-v2** branch is signficantly better over the **main** version, so the suggestion is to FAFO with v2. -That'll be in a separate branch. +Leave your feedback by opening **Issues**. -Overall, it'll be a gradual development & rollout: +A word warning: -* Stage 1: General updates & QoL - * ✅ Lib updates - * Error handling - * Ability to set RPS in the config to match your provider's and plan RPS (preferably [Chainstack](https://console.chainstack.com/) 🤩) - * ✅ Ability to set dynamic priority fees -* Stage 2: Bonding curve and migration management - * ✅ Integrate `logsSubscribe` instead of `blockSubscribe` for sniping minted tokens into the main bot (currently it's separate in the learning examples section that you can integrate yourself) - * ✅ Keep both `logsSubscribe` & `blockSubscribe` in the main bot — so that you can try out/choose which one works best for you — plus the Solana node architecture and provders change, so it's useful to have both - * Do retries instead of cooldown and/or keep the cooldown - * Checking a bonding curve status progress. As in to predict how soon a token will start the migration process. - * Script to close the associated bonding curve account if the rest of the flow txs fails - * Add listening to Raydium migration (and try and figure out the `logSubscribe` way for it as well) — still not sure if I can FAFO this one out, but had some progress in the past -* Stage 3: Trading experience - * Take profit, stop loss - * Sell when a specific market cap has been reached - * Copy trading - * Script for basic token analysis (market cap, creator investment, liquidity, token age) + being to go back with Solana archive nodes (e.g. accounts that consistently print token, average mint to raydium time for winning printing accounts and so on) -* Stage 4: Minting experience - * Ability to mint tokens? (there is a request and there was someone who minted 18k tokens) -* Bonus: Vector.fun - * There's a lot of pump.fun tokens on vector.fun but I didn't investigate yet if there's anything we can do with it. +**Not For Production (NFP)** -Note that the stage progression is from simple to more complex and we don't guarantee everything as move from Stage 1 to the rest. +This code is not intended for production use. Feel free to take the source, modify it to your needs, and most importantly, **learn from it**. -And we appreciate all your feedback and we'll keep you posted! +We assume no responsibility for the code or its usage. This is our public service contribution to the community and Web3. -**>> END OF ROADMPAP <<** +# Pump.fun bot development roadmap + +| Stage | Feature | Comments | Implementation status +|-------|---------|----------|---------------------| +| **Stage 1: General updates & QoL** | Lib updates | Updating to the latest libraries | ✅ | +| | Error handling | Improving error handling | WIP | +| | Configurable RPS | Ability to set RPS in the config to match your provider's and plan RPS (preferably [Chainstack](https://console.chainstack.com/) 🤩) | WIP | +| | Dynamic priority fees | Ability to set dynamic priority fees | ✅ | +| **Stage 2: Bonding curve and migration management** | `logsSubscribe` integration | Integrate `logsSubscribe` instead of `blockSubscribe` for sniping minted tokens into the main bot | ✅ | +| | Dual subscription methods | Keep both `logsSubscribe` & `blockSubscribe` in the main bot for flexibility and adapting to Solana node architecture changes | ✅ | +| | Transaction retries | Do retries instead of cooldown and/or keep the cooldown | WIP | +| | Bonding curve status tracking | Checking a bonding curve status progress. Predict how soon a token will start the migration process | WIP | +| | Account closure script | Script to close the associated bonding curve account if the rest of the flow txs fails | WIP | +| | PumpSwap migration listening | pump_fun migrated to their own DEX — [PumpSwap](https://x.com/pumpdotfun/status/1902762309950292010), so we need to FAFO with that instead of Raydium (and attempt `logSubscribe` implementation) | FAFO | +| **Stage 3: Trading experience** | Take profit/stop loss | Implement take profit, stop loss exit strategies | FAFO | +| | Market cap-based selling | Sell when a specific market cap has been reached | Not started | +| | Copy trading | Enable copy trading functionality | Not started | +| | Token analysis script | Script for basic token analysis (market cap, creator investment, liquidity, token age) | Not started | +| | Archive node integration | Use Solana archive nodes for historical analysis (accounts that consistently print tokens, average mint to raydium time) | Not started | +| | Geyser implementation | Leverage Solana Geyser for real-time data stream processing | Not started | +| **Stage 4: Minting experience** | Token minting | Ability to mint tokens (based on user request - someone minted 18k tokens) | FAFO | + + +## Development Timeline +- Development begins: Week of March 10, 2025 +- Implementation approach: Gradual rollout in separate branch +- Priority: Stages progress from simple to complex features +- Completion guarantee: Full completion of Stage 1, other stages dependent on feedback and feasibility For the full walkthrough, see [Solana: Creating a trading and sniping pump.fun bot](https://docs.chainstack.com/docs/solana-creating-a-pumpfun-bot). From 6b2ae66c73cd7f3204373c33ae71cc7ad97492b0 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 25 Mar 2025 15:58:50 +0000 Subject: [PATCH 29/65] feat: add script to close ATA and reclaim rent --- learning-examples/cleanup_accounts.py | 87 +++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 learning-examples/cleanup_accounts.py diff --git a/learning-examples/cleanup_accounts.py b/learning-examples/cleanup_accounts.py new file mode 100644 index 0000000..d75d4fb --- /dev/null +++ b/learning-examples/cleanup_accounts.py @@ -0,0 +1,87 @@ +import asyncio +import os + +from dotenv import load_dotenv +from solders.pubkey import Pubkey +from spl.token.instructions import BurnParams, CloseAccountParams, burn, close_account + +from core.client import SolanaClient +from core.pubkeys import SystemAddresses +from core.wallet import Wallet +from utils.logger import get_logger + +load_dotenv() +logger = get_logger(__name__) + +RPC_ENDPOINT = os.getenv("SOLANA_NODE_RPC_ENDPOINT") +PRIVATE_KEY = os.getenv("SOLANA_PRIVATE_KEY") + +# Update this address to MINT address of a token you want to close +MINT_ADDRESS = Pubkey.from_string("9WHpYbqG6LJvfCYfMjvGbyo1wHXgroCrixPb33s2pump") + + +async def close_account_if_exists(client: SolanaClient, wallet: Wallet, account: Pubkey, mint: Pubkey): + """Safely close a token account if it exists and reclaim rent.""" + try: + solana_client = await client.get_client() + info = await solana_client.get_account_info(account) + + # WARNING: This will permanently burn all tokens in the account before closing it + # Closing account is impossible if balance is positive + balance = await client.get_token_account_balance(account) + if balance > 0: + logger.info(f"Burning {balance} tokens from account {account}...") + burn_ix = burn( + BurnParams( + account=account, + mint=mint, + owner=wallet.pubkey, + amount=balance, + program_id=SystemAddresses.TOKEN_PROGRAM, + ) + ) + await client.build_and_send_transaction([burn_ix], wallet.keypair) + logger.info(f"Burned tokens from {account}") + + # If account exists, attempt to close it + if info.value: + logger.info(f"Closing account: {account}") + close_params = CloseAccountParams( + account=account, + dest=wallet.pubkey, + owner=wallet.pubkey, + program_id=SystemAddresses.TOKEN_PROGRAM, + ) + ix = close_account(close_params) + + tx_sig = await client.build_and_send_transaction( + [ix], + wallet.keypair, + skip_preflight=True, + ) + await client.confirm_transaction(tx_sig) + logger.info(f"Closed successfully: {account}") + else: + logger.info(f"Account does not exist or already closed: {account}") + + except Exception as e: + logger.error(f"Error while processing account {account}: {e}") + + +async def main(): + try: + client = SolanaClient(RPC_ENDPOINT) + wallet = Wallet(PRIVATE_KEY) + + # Get user's ATA for the token + ata = wallet.get_associated_token_address(MINT_ADDRESS) + await close_account_if_exists(client, wallet, ata, MINT_ADDRESS) + + except Exception as e: + logger.error(f"Unexpected error: {e}") + finally: + await client.close() + + +if __name__ == "__main__": + asyncio.run(main()) From 075e5ae191553137480801904c43982aa456cd15 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 25 Mar 2025 16:25:12 +0000 Subject: [PATCH 30/65] chore: scaffold cleanup module and config for ATA closing feature --- src/cleanup/__init__.py | 0 src/cleanup/manager.py | 0 src/cleanup/modes.py | 0 src/config.py | 15 +++++++++++++++ 4 files changed, 15 insertions(+) create mode 100644 src/cleanup/__init__.py create mode 100644 src/cleanup/manager.py create mode 100644 src/cleanup/modes.py diff --git a/src/cleanup/__init__.py b/src/cleanup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cleanup/manager.py b/src/cleanup/manager.py new file mode 100644 index 0000000..e69de29 diff --git a/src/cleanup/modes.py b/src/cleanup/modes.py new file mode 100644 index 0000000..e69de29 diff --git a/src/config.py b/src/config.py index a2b8d15..21b26e5 100644 --- a/src/config.py +++ b/src/config.py @@ -48,6 +48,21 @@ WAIT_TIME_BEFORE_NEW_TOKEN: int | float = ( MAX_TOKEN_AGE: int | float = 0.1 +# Cleanup configuration + +# CLEANUP_MODE controls when to attempt closing token accounts (ATA) +# Options: +# - "disabled": no cleanup at all +# - "on_fail": close ATA only if a buy transaction fails +# - "after_sell": close ATA after selling, but only if token balance is zero +# - "post_session": clean up all empty ATA accounts at the end of trading session +CLEANUP_MODE: str = "disabled" + +# If True, cleanup transactions will skip priority fees (cheaper, slower confirmation) +# Set to False if you want faster confirmations (e.g. when racing for SOL reclaim) +CLEANUP_WITHOUT_PRIORITY_FEE: bool = True + + # Node provider configuration # Tested with Chainstack nodes (https://console.chainstack.com), but you can use any node provider # You can get a trader node https://docs.chainstack.com/docs/solana-trader-nodes From 6d9d0a106d1fdcb0a8b3b5534161bfafd8f076b3 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 27 Mar 2025 14:59:15 +0000 Subject: [PATCH 31/65] fix: minor formatting fixes in cli --- src/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli.py b/src/cli.py index 983f880..90a4620 100644 --- a/src/cli.py +++ b/src/cli.py @@ -8,7 +8,7 @@ import asyncio import os import sys -import config as config +import config from trading.trader import PumpTrader from utils.logger import get_logger, setup_file_logging @@ -109,7 +109,7 @@ async def main() -> None: except KeyboardInterrupt: logger.info("Trading stopped by user") except Exception as e: - logger.error(f"Trading stopped due to error: {str(e)}") + logger.error(f"Trading stopped due to error: {e!s}") finally: try: await trader.solana_client.close() @@ -117,7 +117,7 @@ async def main() -> None: pass -def sync_main(): +def sync_main() -> None: asyncio.run(main()) From 735ab11d4d711d9dd7a2063e75ec772399d37696 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 27 Mar 2025 15:19:52 +0000 Subject: [PATCH 32/65] feat: added cleanup manager and modes --- src/cleanup/manager.py | 83 ++++++++++++++++++++++++++++++++++++++++++ src/cleanup/modes.py | 39 ++++++++++++++++++++ src/trading/trader.py | 8 ++-- 3 files changed, 126 insertions(+), 4 deletions(-) diff --git a/src/cleanup/manager.py b/src/cleanup/manager.py index e69de29..eb00e53 100644 --- a/src/cleanup/manager.py +++ b/src/cleanup/manager.py @@ -0,0 +1,83 @@ +from solders.pubkey import Pubkey +from spl.token.instructions import BurnParams, CloseAccountParams, burn, close_account + +from config import CLEANUP_WITHOUT_PRIORITY_FEE +from core.client import SolanaClient +from core.pubkeys import SystemAddresses +from core.wallet import Wallet +from utils.logger import get_logger + +logger = get_logger(__name__) + + +class AccountCleanupManager: + """Handles safe cleanup of token accounts (ATA) after trading sessions.""" + + def __init__( + self, + client: SolanaClient, + wallet: Wallet, + ): + """ + Args: + client: Solana RPC client + wallet: Wallet for signing transactions + """ + self.client = client + self.wallet = wallet + self.use_priority_fee = not CLEANUP_WITHOUT_PRIORITY_FEE + + async def cleanup_ata(self, mint: Pubkey) -> None: + """ + Attempt to burn any remaining tokens and close the ATA. + Skips if account doesn't exist or is already empty/closed. + """ + ata = self.wallet.get_associated_token_address(mint) + solana_client = await self.client.get_client() + + try: + info = await solana_client.get_account_info(ata) + if not info.value: + logger.info(f"ATA {ata} does not exist or already closed.") + return + + balance = await self.client.get_token_account_balance(ata) + if balance > 0: + logger.info(f"⚠️ Burning {balance} tokens from ATA {ata} (mint: {mint})...") + burn_ix = burn( + BurnParams( + account=ata, + mint=mint, + owner=self.wallet.pubkey, + amount=balance, + program_id=SystemAddresses.TOKEN_PROGRAM, + ) + ) + await self.client.build_and_send_transaction( + [burn_ix], + self.wallet.keypair, + skip_preflight=True, + priority_fee=None if not self.use_priority_fee else 0, + ) + logger.info(f"✅ Burned successfully from ATA {ata}") + + logger.info(f"Closing ATA: {ata}") + close_ix = close_account( + CloseAccountParams( + account=ata, + dest=self.wallet.pubkey, + owner=self.wallet.pubkey, + program_id=SystemAddresses.TOKEN_PROGRAM, + ) + ) + tx_sig = await self.client.build_and_send_transaction( + [close_ix], + self.wallet.keypair, + skip_preflight=True, + priority_fee=None if not self.use_priority_fee else 0, + ) + await self.client.confirm_transaction(tx_sig) + logger.info(f"✅ Closed successfully: {ata}") + + except Exception as e: + logger.warning(f"⚠️ Cleanup failed for ATA {ata}: {e!s}") diff --git a/src/cleanup/modes.py b/src/cleanup/modes.py index e69de29..26cf079 100644 --- a/src/cleanup/modes.py +++ b/src/cleanup/modes.py @@ -0,0 +1,39 @@ +from cleanup.manager import AccountCleanupManager +from config import CLEANUP_MODE, CLEANUP_WITHOUT_PRIORITY_FEE +from utils.logger import get_logger + +logger = get_logger(__name__) + + +def should_cleanup_after_failure() -> bool: + return CLEANUP_MODE == "on_fail" + + +def should_cleanup_after_sell() -> bool: + return CLEANUP_MODE == "after_sell" + + +def should_cleanup_post_session() -> bool: + return CLEANUP_MODE == "post_session" + + +async def handle_cleanup_after_failure(client, wallet, mint): + if should_cleanup_after_failure(): + logger.info("[Cleanup] Triggered by failed buy transaction.") + manager = AccountCleanupManager(client, wallet, use_priority_fee=not CLEANUP_WITHOUT_PRIORITY_FEE) + await manager.cleanup_ata(mint) + + +async def handle_cleanup_after_sell(client, wallet, mint): + if should_cleanup_after_sell(): + logger.info("[Cleanup] Triggered after token sell.") + manager = AccountCleanupManager(client, wallet, use_priority_fee=not CLEANUP_WITHOUT_PRIORITY_FEE) + await manager.cleanup_ata(mint) + + +async def handle_cleanup_post_session(client, wallet, mints): + if should_cleanup_post_session(): + logger.info("[Cleanup] Triggered post trading session.") + manager = AccountCleanupManager(client, wallet, use_priority_fee=not CLEANUP_WITHOUT_PRIORITY_FEE) + for mint in mints: + await manager.cleanup_ata(mint) diff --git a/src/trading/trader.py b/src/trading/trader.py index 6204217..470b125 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -8,7 +8,7 @@ import json import os from datetime import datetime -import config as config +import config from core.client import SolanaClient from core.curve import BondingCurveManager from core.priority_fee.manager import PriorityFeeManager @@ -137,7 +137,7 @@ class PumpTrader: ) except Exception as e: - logger.error(f"Trading stopped due to error: {str(e)}") + logger.error(f"Trading stopped due to error: {e!s}") processor_task.cancel() await self.solana_client.close() @@ -153,7 +153,7 @@ class PumpTrader: self.token_timestamps[token_key] = asyncio.get_event_loop().time() await self.token_queue.put(token_info) - # logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})") + logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})") async def _process_token_queue(self, marry_mode: bool, yolo_mode: bool) -> None: """Continuously process tokens from the queue, only if they're fresh.""" @@ -254,7 +254,7 @@ class PumpTrader: await asyncio.sleep(config.WAIT_TIME_BEFORE_NEW_TOKEN) except Exception as e: - logger.error(f"Error handling token {token_info.symbol}: {str(e)}") + logger.error(f"Error handling token {token_info.symbol}: {e!s}") async def _save_token_info(self, token_info: TokenInfo) -> None: """Save token information to a file. From 5c2d044569e1d6efba1086ac86f8c60731fcaa9c Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 27 Mar 2025 21:11:48 +0000 Subject: [PATCH 33/65] feat: cleanup manager integrated into trader, enhanced config --- src/cleanup/manager.py | 33 ++++++--- src/cleanup/modes.py | 16 ++--- src/config.py | 150 +++++++++++++++++++++++------------------ src/trading/trader.py | 21 ++++++ 4 files changed, 135 insertions(+), 85 deletions(-) diff --git a/src/cleanup/manager.py b/src/cleanup/manager.py index eb00e53..286e009 100644 --- a/src/cleanup/manager.py +++ b/src/cleanup/manager.py @@ -1,8 +1,9 @@ from solders.pubkey import Pubkey from spl.token.instructions import BurnParams, CloseAccountParams, burn, close_account -from config import CLEANUP_WITHOUT_PRIORITY_FEE +from config import CLEANUP_FORCE_CLOSE_WITH_BURN from core.client import SolanaClient +from core.priority_fee.manager import PriorityFeeManager from core.pubkeys import SystemAddresses from core.wallet import Wallet from utils.logger import get_logger @@ -12,11 +13,12 @@ logger = get_logger(__name__) class AccountCleanupManager: """Handles safe cleanup of token accounts (ATA) after trading sessions.""" - def __init__( self, client: SolanaClient, wallet: Wallet, + priority_fee_manager: PriorityFeeManager, + use_priority_fee: bool = False, ): """ Args: @@ -25,7 +27,8 @@ class AccountCleanupManager: """ self.client = client self.wallet = wallet - self.use_priority_fee = not CLEANUP_WITHOUT_PRIORITY_FEE + self.priority_fee_manager = priority_fee_manager + self.use_priority_fee = use_priority_fee async def cleanup_ata(self, mint: Pubkey) -> None: """ @@ -35,6 +38,12 @@ class AccountCleanupManager: ata = self.wallet.get_associated_token_address(mint) solana_client = await self.client.get_client() + priority_fee = ( + await self.priority_fee_manager.calculate_priority_fee([ata]) + if self.use_priority_fee + else None + ) + try: info = await solana_client.get_account_info(ata) if not info.value: @@ -42,8 +51,8 @@ class AccountCleanupManager: return balance = await self.client.get_token_account_balance(ata) - if balance > 0: - logger.info(f"⚠️ Burning {balance} tokens from ATA {ata} (mint: {mint})...") + if balance > 0 and CLEANUP_FORCE_CLOSE_WITH_BURN: + logger.info(f"Burning {balance} tokens from ATA {ata} (mint: {mint})...") burn_ix = burn( BurnParams( account=ata, @@ -57,9 +66,15 @@ class AccountCleanupManager: [burn_ix], self.wallet.keypair, skip_preflight=True, - priority_fee=None if not self.use_priority_fee else 0, + priority_fee=priority_fee, ) logger.info(f"✅ Burned successfully from ATA {ata}") + elif balance > 0: + logger.info( + f"Skipping ATA {ata} with non-zero balance ({balance} tokens) " + f"because CLEANUP_FORCE_CLOSE_WITH_BURN is disabled." + ) + return logger.info(f"Closing ATA: {ata}") close_ix = close_account( @@ -74,10 +89,10 @@ class AccountCleanupManager: [close_ix], self.wallet.keypair, skip_preflight=True, - priority_fee=None if not self.use_priority_fee else 0, + priority_fee=priority_fee, ) await self.client.confirm_transaction(tx_sig) - logger.info(f"✅ Closed successfully: {ata}") + logger.info(f"Closed successfully: {ata}") except Exception as e: - logger.warning(f"⚠️ Cleanup failed for ATA {ata}: {e!s}") + logger.warning(f"Cleanup failed for ATA {ata}: {e!s}") diff --git a/src/cleanup/modes.py b/src/cleanup/modes.py index 26cf079..8c11794 100644 --- a/src/cleanup/modes.py +++ b/src/cleanup/modes.py @@ -1,5 +1,5 @@ from cleanup.manager import AccountCleanupManager -from config import CLEANUP_MODE, CLEANUP_WITHOUT_PRIORITY_FEE +from config import CLEANUP_MODE, CLEANUP_WITH_PRIORITY_FEE from utils.logger import get_logger logger = get_logger(__name__) @@ -17,23 +17,21 @@ def should_cleanup_post_session() -> bool: return CLEANUP_MODE == "post_session" -async def handle_cleanup_after_failure(client, wallet, mint): +async def handle_cleanup_after_failure(client, wallet, mint, priority_fee_manager): if should_cleanup_after_failure(): logger.info("[Cleanup] Triggered by failed buy transaction.") - manager = AccountCleanupManager(client, wallet, use_priority_fee=not CLEANUP_WITHOUT_PRIORITY_FEE) + manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE) await manager.cleanup_ata(mint) - -async def handle_cleanup_after_sell(client, wallet, mint): +async def handle_cleanup_after_sell(client, wallet, mint, priority_fee_manager): if should_cleanup_after_sell(): logger.info("[Cleanup] Triggered after token sell.") - manager = AccountCleanupManager(client, wallet, use_priority_fee=not CLEANUP_WITHOUT_PRIORITY_FEE) + manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE) await manager.cleanup_ata(mint) - -async def handle_cleanup_post_session(client, wallet, mints): +async def handle_cleanup_post_session(client, wallet, mints, priority_fee_manager): if should_cleanup_post_session(): logger.info("[Cleanup] Triggered post trading session.") - manager = AccountCleanupManager(client, wallet, use_priority_fee=not CLEANUP_WITHOUT_PRIORITY_FEE) + manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE) for mint in mints: await manager.cleanup_ata(mint) diff --git a/src/config.py b/src/config.py index 21b26e5..0a87f04 100644 --- a/src/config.py +++ b/src/config.py @@ -1,87 +1,103 @@ """ Configuration for the pump.fun trading bot. + +This file defines comprehensive parameters and settings for the trading bot. +Carefully review and adjust values to match your trading strategy and risk tolerance. """ # Trading parameters -BUY_AMOUNT: int | float = 0.000_001 # Amount of SOL to spend when buying -BUY_SLIPPAGE: float = 0.4 # 40% slippage tolerance for buying -SELL_SLIPPAGE: float = 0.4 # 40% slippage tolerance for selling +# Control trade execution: amount of SOL per trade and acceptable price deviation +BUY_AMOUNT: int | float = 0.000_001 # Minimal SOL amount to prevent dust transactions +BUY_SLIPPAGE: float = 0.4 # Maximum acceptable price deviation (0.4 = 40%) +SELL_SLIPPAGE: float = 0.4 # Consistent slippage tolerance to maintain trading strategy -# Configuration for priority fee settings -ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Enable dynamic priority fee calculation -ENABLE_FIXED_PRIORITY_FEE: bool = True # Enable fixed priority fee -FIXED_PRIORITY_FEE: int = 2_000 # Fixed priority fee in microlamports -EXTRA_PRIORITY_FEE: float = ( - 0.0 # Percentage increase applied to priority fee (0.1 = 10%) -) -HARD_CAP_PRIOR_FEE: int = ( - 200_000 # Maximum allowed priority fee in microlamports (hard cap) -) +# Priority fee configuration +# Manage transaction speed and cost on the Solana network +ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Adaptive fee calculation +ENABLE_FIXED_PRIORITY_FEE: bool = True # Use consistent, predictable fee +FIXED_PRIORITY_FEE: int = 2_000 # Base fee in microlamports +EXTRA_PRIORITY_FEE: float = 0.0 # Percentage increase on base priority fee (0.1 = 10%) +HARD_CAP_PRIOR_FEE: int = 200_000 # Maximum allowable fee to prevent excessive spending in microlamports # Listener configuration -LISTENER_TYPE = "block" # Options: "block" or "logs" +# Choose method for detecting new tokens on the network +# "logs": Recommended for more stable token detection +# "blocks": Unstable method, potentially less reliable +LISTENER_TYPE = "logs" -# Retries and timeouts -MAX_RETRIES: int = 10 # Number of retries for transaction sending -# TODO: waiting times will be replaced with retries to shorten delays -WAIT_TIME_AFTER_CREATION: int | float = ( - 15 # Time to wait after token creation (in seconds) - # Too short a delay may cause the RPC node to be unaware of the bonding curve account -) -WAIT_TIME_AFTER_BUY: int | float = ( - 15 # Time to wait after a buy transaction is confirmed (in seconds) - # Acts as a simple holding period - # Too short delay may cause the RPC node to be unaware of account balance -) -WAIT_TIME_BEFORE_NEW_TOKEN: int | float = ( - 5 # Time to wait after a sell transaction is confirmed (in seconds) - # Provides a pause between completed trades, can be set to 0 -) +# Retry and timeout settings +# Control bot resilience and transaction handling +MAX_RETRIES: int = 10 # Number of attempts for transaction submission + +# Waiting periods in seconds between actions (TODO: to be replaced with retry mechanism) +WAIT_TIME_AFTER_CREATION: int | float = 15 # Seconds to wait after token creation +WAIT_TIME_AFTER_BUY: int | float = 15 # Holding period after buy transaction +WAIT_TIME_BEFORE_NEW_TOKEN: int | float = 15 # Pause between token trades -# Maximum age (in seconds) for a token to be considered "fresh" and eligible for processing. -# This threshold is checked before processing starts - tokens older than this are skipped -# since they likely contain outdated information from the websocket stream -MAX_TOKEN_AGE: int | float = 0.1 +# Token and account management +# Control token processing and account cleanup strategies +MAX_TOKEN_AGE: int | float = 0.1 # Maximum token age in seconds for processing + +# Cleanup mode determines when to manage token accounts. Options: +# "disabled": No cleanup will occur. +# "on_fail": Only clean up if a buy transaction fails. +# "after_sell": Clean up after selling, but only if the balance is zero. +# "post_session": Clean up all empty accounts after a trading session ends. +CLEANUP_MODE: str = "after_sell" +CLEANUP_FORCE_CLOSE_WITH_BURN: bool = True # Burn remaining tokens before closing account, else skip ATA with non-zero balances +CLEANUP_WITH_PRIORITY_FEE: bool = False # Use priority fees for cleanup transactions -# Cleanup configuration - -# CLEANUP_MODE controls when to attempt closing token accounts (ATA) -# Options: -# - "disabled": no cleanup at all -# - "on_fail": close ATA only if a buy transaction fails -# - "after_sell": close ATA after selling, but only if token balance is zero -# - "post_session": clean up all empty ATA accounts at the end of trading session -CLEANUP_MODE: str = "disabled" - -# If True, cleanup transactions will skip priority fees (cheaper, slower confirmation) -# Set to False if you want faster confirmations (e.g. when racing for SOL reclaim) -CLEANUP_WITHOUT_PRIORITY_FEE: bool = True +# Node provider configuration (TODO: to be implemented) +# Manage RPC node interaction to prevent rate limiting +MAX_RPS: int = 25 # Maximum requests per second -# Node provider configuration -# Tested with Chainstack nodes (https://console.chainstack.com), but you can use any node provider -# You can get a trader node https://docs.chainstack.com/docs/solana-trader-nodes -MAX_RPS: int = 25 # TODO: not implemented. Max RPS to avoid rate limit errors +def validate_configuration() -> None: + """ + Comprehensive validation of bot configuration. + + Checks: + - Type correctness + - Value ranges + - Logical consistency of settings + """ + # Configuration validation checks + config_checks = [ + # (value, type, min_value, max_value, error_message) + (BUY_AMOUNT, (int, float), 0, float('inf'), "BUY_AMOUNT must be a positive number"), + (BUY_SLIPPAGE, float, 0, 1, "BUY_SLIPPAGE must be between 0 and 1"), + (SELL_SLIPPAGE, float, 0, 1, "SELL_SLIPPAGE must be between 0 and 1"), + (FIXED_PRIORITY_FEE, int, 0, float('inf'), "FIXED_PRIORITY_FEE must be a non-negative integer"), + (EXTRA_PRIORITY_FEE, float, 0, 1, "EXTRA_PRIORITY_FEE must be between 0 and 1"), + (HARD_CAP_PRIOR_FEE, int, 0, float('inf'), "HARD_CAP_PRIOR_FEE must be a non-negative integer"), + (MAX_RETRIES, int, 0, 100, "MAX_RETRIES must be between 0 and 100") + ] + + for value, expected_type, min_val, max_val, error_msg in config_checks: + if not isinstance(value, expected_type): + raise ValueError(f"Type error: {error_msg}") + + if isinstance(value, (int, float)) and not (min_val <= value <= max_val): + raise ValueError(f"Range error: {error_msg}") + + # Logical consistency checks + if ENABLE_DYNAMIC_PRIORITY_FEE and ENABLE_FIXED_PRIORITY_FEE: + raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously") + + # Validate listener type + if LISTENER_TYPE not in ["logs", "blocks"]: + raise ValueError("LISTENER_TYPE must be either 'logs' or 'blocks'") + + # Validate cleanup mode + valid_cleanup_modes = ["disabled", "on_fail", "after_sell", "post_session"] + if CLEANUP_MODE not in valid_cleanup_modes: + raise ValueError(f"CLEANUP_MODE must be one of {valid_cleanup_modes}") -def validate_priority_fee_config() -> None: - """Validate priority fee configuration values.""" - if not isinstance(ENABLE_DYNAMIC_PRIORITY_FEE, bool): - raise ValueError("ENABLE_DYNAMIC_PRIORITY_FEE must be a boolean") - if not isinstance(ENABLE_FIXED_PRIORITY_FEE, bool): - raise ValueError("ENABLE_FIXED_PRIORITY_FEE must be a boolean") - if not isinstance(FIXED_PRIORITY_FEE, int) or FIXED_PRIORITY_FEE < 0: - raise ValueError("FIXED_PRIORITY_FEE must be a non-negative integer") - if not isinstance(EXTRA_PRIORITY_FEE, float) or EXTRA_PRIORITY_FEE < 0: - raise ValueError("EXTRA_PRIORITY_FEE must be a non-negative float") - if not isinstance(HARD_CAP_PRIOR_FEE, int) or HARD_CAP_PRIOR_FEE < 0: - raise ValueError("HARD_CAP_PRIOR_FEE must be a non-negative integer") - - -# Validate config on import -validate_priority_fee_config() +# Validate configuration on import +validate_configuration() \ No newline at end of file diff --git a/src/trading/trader.py b/src/trading/trader.py index 470b125..c5cf832 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -8,7 +8,14 @@ import json import os from datetime import datetime +from solders.pubkey import Pubkey + import config +from cleanup.modes import ( + handle_cleanup_after_failure, + handle_cleanup_after_sell, + handle_cleanup_post_session, +) from core.client import SolanaClient from core.curve import BondingCurveManager from core.priority_fee.manager import PriorityFeeManager @@ -96,6 +103,8 @@ class PumpTrader: self.max_retries = max_retries self.max_token_age = config.MAX_TOKEN_AGE + self.traded_mints: set[Pubkey] = set() + # Token processing state self.token_queue = asyncio.Queue() self.processing = False @@ -141,6 +150,13 @@ class PumpTrader: processor_task.cancel() await self.solana_client.close() + finally: + processor_task.cancel() + if self.traded_mints: + # Close ATA if enabled + await handle_cleanup_post_session(self.solana_client, self.wallet, list(self.traded_mints), self.priority_fee_manager) + await self.solana_client.close() + async def _queue_token(self, token_info: TokenInfo) -> None: """Queue a token for processing if not already processed.""" token_key = str(token_info.mint) @@ -215,10 +231,13 @@ class PumpTrader: buy_result.amount, # type: ignore buy_result.tx_signature, ) + self.traded_mints.add(token_info.mint) else: logger.error( f"Failed to buy {token_info.symbol}: {buy_result.error_message}" ) + # Close ATA if enabled + await handle_cleanup_after_failure(self.solana_client, self.wallet, token_info.mint, self.priority_fee_manager) # Sell token if not in marry mode if not marry_mode and buy_result.success: @@ -239,6 +258,8 @@ class PumpTrader: sell_result.amount, # type: ignore sell_result.tx_signature, ) + # Close ATA if enabled + await handle_cleanup_after_sell(self.solana_client, self.wallet, token_info.mint, self.priority_fee_manager) else: logger.error( f"Failed to sell {token_info.symbol}: {sell_result.error_message}" From 2514be2b317c054d6fedef25de4561a17d3a5bc7 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 27 Mar 2025 21:13:12 +0000 Subject: [PATCH 34/65] chore: update default params --- src/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.py b/src/config.py index 0a87f04..9b978e6 100644 --- a/src/config.py +++ b/src/config.py @@ -47,8 +47,8 @@ MAX_TOKEN_AGE: int | float = 0.1 # Maximum token age in seconds for processing # "on_fail": Only clean up if a buy transaction fails. # "after_sell": Clean up after selling, but only if the balance is zero. # "post_session": Clean up all empty accounts after a trading session ends. -CLEANUP_MODE: str = "after_sell" -CLEANUP_FORCE_CLOSE_WITH_BURN: bool = True # Burn remaining tokens before closing account, else skip ATA with non-zero balances +CLEANUP_MODE: str = "disabled" +CLEANUP_FORCE_CLOSE_WITH_BURN: bool = False # Burn remaining tokens before closing account, else skip ATA with non-zero balances CLEANUP_WITH_PRIORITY_FEE: bool = False # Use priority fees for cleanup transactions From 7d64ff7245df9fc03a70d50cd1b212bc5b2f7a0e Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 27 Mar 2025 21:22:52 +0000 Subject: [PATCH 35/65] chore: fix typos in config --- src/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.py b/src/config.py index 9b978e6..e10a472 100644 --- a/src/config.py +++ b/src/config.py @@ -7,7 +7,7 @@ Carefully review and adjust values to match your trading strategy and risk toler # Trading parameters # Control trade execution: amount of SOL per trade and acceptable price deviation -BUY_AMOUNT: int | float = 0.000_001 # Minimal SOL amount to prevent dust transactions +BUY_AMOUNT: int | float = 0.000_001 # Amount of SOL to spend when buying BUY_SLIPPAGE: float = 0.4 # Maximum acceptable price deviation (0.4 = 40%) SELL_SLIPPAGE: float = 0.4 # Consistent slippage tolerance to maintain trading strategy @@ -17,7 +17,7 @@ SELL_SLIPPAGE: float = 0.4 # Consistent slippage tolerance to maintain trading ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Adaptive fee calculation ENABLE_FIXED_PRIORITY_FEE: bool = True # Use consistent, predictable fee FIXED_PRIORITY_FEE: int = 2_000 # Base fee in microlamports -EXTRA_PRIORITY_FEE: float = 0.0 # Percentage increase on base priority fee (0.1 = 10%) +EXTRA_PRIORITY_FEE: float = 0.0 # Percentage increase on priority fee (0.1 = 10%) HARD_CAP_PRIOR_FEE: int = 200_000 # Maximum allowable fee to prevent excessive spending in microlamports From 1f55adeeea1530d627eb58c9c79fca286b8fc9e8 Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Fri, 28 Mar 2025 09:30:16 +0100 Subject: [PATCH 36/65] docs: ATA closing done --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 402ae9c..4dba477 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ We assume no responsibility for the code or its usage. This is our public servic | | Dual subscription methods | Keep both `logsSubscribe` & `blockSubscribe` in the main bot for flexibility and adapting to Solana node architecture changes | ✅ | | | Transaction retries | Do retries instead of cooldown and/or keep the cooldown | WIP | | | Bonding curve status tracking | Checking a bonding curve status progress. Predict how soon a token will start the migration process | WIP | -| | Account closure script | Script to close the associated bonding curve account if the rest of the flow txs fails | WIP | +| | Account closure script | Script to close the associated bonding curve account if the rest of the flow txs fails | ✅ | | | PumpSwap migration listening | pump_fun migrated to their own DEX — [PumpSwap](https://x.com/pumpdotfun/status/1902762309950292010), so we need to FAFO with that instead of Raydium (and attempt `logSubscribe` implementation) | FAFO | | **Stage 3: Trading experience** | Take profit/stop loss | Implement take profit, stop loss exit strategies | FAFO | | | Market cap-based selling | Sell when a specific market cap has been reached | Not started | From 6b2211c1cad8812ff9cc93989c1e670803400520 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 29 Mar 2025 18:10:50 +0000 Subject: [PATCH 37/65] feat: add script to listen to migrations --- learning-examples/listen_to_logs_migration.py | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 learning-examples/listen_to_logs_migration.py diff --git a/learning-examples/listen_to_logs_migration.py b/learning-examples/listen_to_logs_migration.py new file mode 100644 index 0000000..d02a2bc --- /dev/null +++ b/learning-examples/listen_to_logs_migration.py @@ -0,0 +1,158 @@ +import asyncio +import base64 +import json +import os +import struct + +import base58 +import websockets +from solders.pubkey import Pubkey + +WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") +MIGRATION_PROGRAM_ID = Pubkey.from_string("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg") + + +def parse_migrate_instruction(data): + if len(data) < 8: + print(f"[ERROR] Data length too short: {len(data)} bytes") + return None + + offset = 8 + parsed_data = {} + + fields = [ + ("timestamp", "i64"), + ("index", "u16"), + ("creator", "publicKey"), + ("baseMint", "publicKey"), + ("quoteMint", "publicKey"), + ("baseMintDecimals", "u8"), + ("quoteMintDecimals", "u8"), + ("baseAmountIn", "u64"), + ("quoteAmountIn", "u64"), + ("poolBaseAmount", "u64"), + ("poolQuoteAmount", "u64"), + ("minimumLiquidity", "u64"), + ("initialLiquidity", "u64"), + ("lpTokenAmountOut", "u64"), + ("poolBump", "u8"), + ("pool", "publicKey"), + ("lpMint", "publicKey"), + ("userBaseTokenAccount", "publicKey"), + ("userQuoteTokenAccount", "publicKey"), + ] + + try: + for field_name, field_type in fields: + if field_type == "publicKey": + value = data[offset:offset + 32] + parsed_data[field_name] = base58.b58encode(value).decode("utf-8") + offset += 32 + elif field_type in {"u64", "i64"}: + value = struct.unpack(" Date: Sat, 29 Mar 2025 21:31:46 +0000 Subject: [PATCH 38/65] feat: add script to get pumpswap market data --- learning-examples/get_pumpswap_pools.py | 86 +++++++++++++++++++++++++ learning-examples/manual_sell.py | 2 +- 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 learning-examples/get_pumpswap_pools.py diff --git a/learning-examples/get_pumpswap_pools.py b/learning-examples/get_pumpswap_pools.py new file mode 100644 index 0000000..c8e9d00 --- /dev/null +++ b/learning-examples/get_pumpswap_pools.py @@ -0,0 +1,86 @@ +import asyncio +import os +import struct + +import base58 +from solana.rpc.async_api import AsyncClient +from solana.rpc.types import MemcmpOpts +from solders.pubkey import Pubkey + +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") +PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") +TOKEN_MINT = Pubkey.from_string("35ySx7Rt3RqeTp75QB81FgRvPT5yDY2m5BupsUYDpump") + + +async def get_market_address_by_base_mint(base_mint_address: Pubkey, amm_program_id: Pubkey): + async with AsyncClient(RPC_ENDPOINT) as client: + base_mint_bytes = bytes(base_mint_address) + + # Define the offset for base_mint field + offset = 43 + + # Create the filter to match the base_mint + filters = [ + MemcmpOpts(offset=offset, bytes=base_mint_bytes) + ] + + # Retrieve the accounts that match the filter + response = await client.get_program_accounts( + amm_program_id, # AMM program ID + encoding="jsonParsed", + filters=filters + ) + + pool_addresses = [account.pubkey for account in response.value] + return pool_addresses[0] + +async def get_market_data(market_address: Pubkey): + async with AsyncClient(RPC_ENDPOINT) as client: + response = await client.get_account_info_json_parsed(market_address) + data = response.value.data + parsed_data = {} + + offset = 8 + fields = [ + ("pool_bump", "u8"), + ("index", "u16"), + ("creator", "pubkey"), + ("base_mint", "pubkey"), + ("quote_mint", "pubkey"), + ("lp_mint", "pubkey"), + ("pool_base_token_account", "pubkey"), + ("pool_quote_token_account", "pubkey"), + ("lp_supply", "u64"), + ] + + for field_name, field_type in fields: + if field_type == "pubkey": + value = data[offset:offset + 32] + parsed_data[field_name] = base58.b58encode(value).decode("utf-8") + offset += 32 + elif field_type in {"u64", "i64"}: + value = struct.unpack(" Date: Sun, 30 Mar 2025 12:51:02 +0000 Subject: [PATCH 39/65] feat: add manual pumpswap sell --- learning-examples/manual_sell_pumpswap.py | 238 ++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 learning-examples/manual_sell_pumpswap.py diff --git a/learning-examples/manual_sell_pumpswap.py b/learning-examples/manual_sell_pumpswap.py new file mode 100644 index 0000000..9e3a21b --- /dev/null +++ b/learning-examples/manual_sell_pumpswap.py @@ -0,0 +1,238 @@ +import asyncio +import os +import struct + +import base58 +from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Confirmed +from solana.rpc.types import MemcmpOpts, TxOpts +from solders.compute_budget import set_compute_unit_limit, 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 VersionedTransaction +from spl.token.instructions import get_associated_token_address + +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") +TOKEN_MINT = Pubkey.from_string("35ySx7Rt3RqeTp75QB81FgRvPT5yDY2m5BupsUYDpump") +PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) +PAYER = Keypair.from_bytes(PRIVATE_KEY) +SLIPPAGE = 0.25 # Slippage tolerance + +TOKEN_DECIMALS = 6 +SELL_DISCRIMINATOR = struct.pack(" Pubkey: + base_mint_bytes = bytes(base_mint_address) + offset = 43 # This should be calculated based on the fields before the base_mint field + filters = [ + MemcmpOpts(offset=offset, bytes=base_mint_bytes) + ] + + response = await client.get_program_accounts( + amm_program_id, + encoding="jsonParsed", + filters=filters + ) + + market_address = [account.pubkey for account in response.value][0] + return market_address + +async def get_market_data(client: AsyncClient, market_address: Pubkey): + response = await client.get_account_info_json_parsed(market_address) + data = response.value.data + parsed_data = {} + + offset = 8 + fields = [ + ("pool_bump", "u8"), + ("index", "u16"), + ("creator", "pubkey"), + ("base_mint", "pubkey"), + ("quote_mint", "pubkey"), + ("lp_mint", "pubkey"), + ("pool_base_token_account", "pubkey"), + ("pool_quote_token_account", "pubkey"), + ("lp_supply", "u64"), + ] + + for field_name, field_type in fields: + if field_type == "pubkey": + value = data[offset:offset + 32] + parsed_data[field_name] = base58.b58encode(value).decode("utf-8") + offset += 32 + elif field_type in {"u64", "i64"}: + value = struct.unpack(" float: + base_balance_resp = await client.get_token_account_balance(pool_base_token_account) + quote_balance_resp = await client.get_token_account_balance(pool_quote_token_account) + + base_amount = float(base_balance_resp.value.ui_amount) + quote_amount = float(quote_balance_resp.value.ui_amount) + token_price = base_amount / quote_amount + + return token_price, base_amount, quote_amount + +async def calculate_sol_amount_from_token(client: AsyncClient, token_amount: float, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float: + _, base_amount, quote_amount = await get_token_price_from_pool(client, pool_base_token_account, pool_quote_token_account) + + k = base_amount * quote_amount + new_base_amount = base_amount + token_amount + new_quote_amount = k / new_base_amount + sol_amount = quote_amount - new_quote_amount + + sol_received = sol_amount * (1 - 0.25 / 100) + + return sol_received + +def create_ata_idempotent_ix(payer_pubkey, owner_pubkey): + """ + Create an instruction to create an Associated Token Account for WSOL in an idempotent way. + """ + associated_token_address = get_associated_token_address(owner_pubkey, SOL) + instruction_accounts = [ + AccountMeta(pubkey=payer_pubkey, is_signer=True, is_writable=True), + AccountMeta(pubkey=associated_token_address, is_signer=False, is_writable=True), + AccountMeta(pubkey=owner_pubkey, is_signer=False, is_writable=False), + AccountMeta(pubkey=SOL, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + ] + data = bytes([1]) + return Instruction(SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, data, instruction_accounts) + +async def sell_pump_swap(client: AsyncClient, pump_fun_amm_pool: Pubkey, payer: Keypair, + base_mint: Pubkey, user_base_token_account: Pubkey, + user_quote_token_account: Pubkey, pool_base_token_account: Pubkey, + pool_quote_token_account: Pubkey, slippage: float = 0.25): + + token_balance = await client.get_token_account_balance(user_quote_token_account) + token_balance_decimal = token_balance / 10**TOKEN_DECIMALS + print(f"Token balance: {token_balance_decimal}") + if token_balance == 0: + print("No tokens to sell.") + return + + token_price, base_amount, quote_amount = await get_token_price_from_pool(client, pool_base_token_account, pool_quote_token_account) + + k = base_amount * quote_amount + new_quote_amount = quote_amount + token_balance_decimal + new_base_amount = k / new_quote_amount + expected_sol_amount = base_amount - new_base_amount + + expected_sol_after_fee = expected_sol_amount * (1 - 0.25 / 100) + min_sol_output_float = expected_sol_after_fee * (1 - slippage) + min_sol_output = min(int(min_sol_output_float * 10**9), 18_446_744_073_709_551_615) + + print(f"Selling {token_balance_decimal} tokens") + print(f"Expected SOL: {expected_sol_after_fee:.9f}") + print(f"Min SOL with {slippage*100}% slippage: {min_sol_output/10**9:.9f}") + + print(f"Token balance to sell (raw): {token_balance}") + print(f"Min SOL output (raw): {min_sol_output}") + + data = SELL_DISCRIMINATOR + struct.pack(" Date: Sun, 30 Mar 2025 12:55:50 +0000 Subject: [PATCH 40/65] fix: use base64 for get_program_accounts --- learning-examples/get_pumpswap_pools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learning-examples/get_pumpswap_pools.py b/learning-examples/get_pumpswap_pools.py index c8e9d00..9019950 100644 --- a/learning-examples/get_pumpswap_pools.py +++ b/learning-examples/get_pumpswap_pools.py @@ -27,7 +27,7 @@ async def get_market_address_by_base_mint(base_mint_address: Pubkey, amm_program # Retrieve the accounts that match the filter response = await client.get_program_accounts( amm_program_id, # AMM program ID - encoding="jsonParsed", + encoding="base64", filters=filters ) From 925b4bfaee03b273f83f70c090988d97b09ac14b Mon Sep 17 00:00:00 2001 From: smypmsa Date: Mon, 31 Mar 2025 06:59:41 +0000 Subject: [PATCH 41/65] fix: add missed load_dotenv --- learning-examples/get_pumpswap_pools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/learning-examples/get_pumpswap_pools.py b/learning-examples/get_pumpswap_pools.py index 9019950..6a9606e 100644 --- a/learning-examples/get_pumpswap_pools.py +++ b/learning-examples/get_pumpswap_pools.py @@ -3,10 +3,13 @@ import os import struct import base58 +from dotenv import load_dotenv from solana.rpc.async_api import AsyncClient from solana.rpc.types import MemcmpOpts from solders.pubkey import Pubkey +load_dotenv() + RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") TOKEN_MINT = Pubkey.from_string("35ySx7Rt3RqeTp75QB81FgRvPT5yDY2m5BupsUYDpump") From 4b224879ddfec1d31732623bc3fb73a987bb409d Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Mon, 31 Mar 2025 10:27:47 +0200 Subject: [PATCH 42/65] docs: migration listening done --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4dba477..59043a0 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ We assume no responsibility for the code or its usage. This is our public servic | | Transaction retries | Do retries instead of cooldown and/or keep the cooldown | WIP | | | Bonding curve status tracking | Checking a bonding curve status progress. Predict how soon a token will start the migration process | WIP | | | Account closure script | Script to close the associated bonding curve account if the rest of the flow txs fails | ✅ | -| | PumpSwap migration listening | pump_fun migrated to their own DEX — [PumpSwap](https://x.com/pumpdotfun/status/1902762309950292010), so we need to FAFO with that instead of Raydium (and attempt `logSubscribe` implementation) | FAFO | +| | PumpSwap migration listening | pump_fun migrated to their own DEX — [PumpSwap](https://x.com/pumpdotfun/status/1902762309950292010), so we need to FAFO with that instead of Raydium (and attempt `logSubscribe` implementation) | ✅ | | **Stage 3: Trading experience** | Take profit/stop loss | Implement take profit, stop loss exit strategies | FAFO | | | Market cap-based selling | Sell when a specific market cap has been reached | Not started | | | Copy trading | Enable copy trading functionality | Not started | From 355cd6bdb15afbc644a61e0b8a6b0ca845a3e1d4 Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Mon, 31 Mar 2025 10:37:45 +0200 Subject: [PATCH 43/65] docs: add review & optimize json, jsonParsed, base64 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 59043a0..18c3b60 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ We assume no responsibility for the code or its usage. This is our public servic | | Error handling | Improving error handling | WIP | | | Configurable RPS | Ability to set RPS in the config to match your provider's and plan RPS (preferably [Chainstack](https://console.chainstack.com/) 🤩) | WIP | | | Dynamic priority fees | Ability to set dynamic priority fees | ✅ | +| | Review & optimize `json`, `jsonParsed`, `base64` | Improve speed and traffic for calls, not just `getBlock`. [Helpful overview](https://docs.chainstack.com/docs/solana-optimize-your-getblock-performance#json-jsonparsed-base58-base64).| WIP | | **Stage 2: Bonding curve and migration management** | `logsSubscribe` integration | Integrate `logsSubscribe` instead of `blockSubscribe` for sniping minted tokens into the main bot | ✅ | | | Dual subscription methods | Keep both `logsSubscribe` & `blockSubscribe` in the main bot for flexibility and adapting to Solana node architecture changes | ✅ | | | Transaction retries | Do retries instead of cooldown and/or keep the cooldown | WIP | From 7bf78830f0e0089b6ec4ad9ed8f5d4cd22eed1f8 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Mon, 31 Mar 2025 15:14:07 +0000 Subject: [PATCH 44/65] fix: fixes in initial implementation --- learning-examples/manual_sell_pumpswap.py | 78 ++++++++++------------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/learning-examples/manual_sell_pumpswap.py b/learning-examples/manual_sell_pumpswap.py index 9e3a21b..8a84a86 100644 --- a/learning-examples/manual_sell_pumpswap.py +++ b/learning-examples/manual_sell_pumpswap.py @@ -3,6 +3,7 @@ import os import struct import base58 +from dotenv import load_dotenv from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Confirmed from solana.rpc.types import MemcmpOpts, TxOpts @@ -14,6 +15,8 @@ from solders.pubkey import Pubkey from solders.transaction import VersionedTransaction from spl.token.instructions import get_associated_token_address +load_dotenv() + RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") TOKEN_MINT = Pubkey.from_string("35ySx7Rt3RqeTp75QB81FgRvPT5yDY2m5BupsUYDpump") PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) @@ -32,6 +35,9 @@ SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss62 SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111") SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR") +LAMPORTS_PER_SOL = 1_000_000_000 +COMPUTE_UNIT_PRICE = 10_000 +COMPUTE_UNIT_BUDGET = 100_000 async def get_market_address_by_base_mint(client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey) -> Pubkey: @@ -88,29 +94,17 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey): return parsed_data -async def get_token_price_from_pool(client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float: +async def calculate_token_pool_price(client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float: base_balance_resp = await client.get_token_account_balance(pool_base_token_account) quote_balance_resp = await client.get_token_account_balance(pool_quote_token_account) base_amount = float(base_balance_resp.value.ui_amount) quote_amount = float(quote_balance_resp.value.ui_amount) - token_price = base_amount / quote_amount + token_price = quote_amount / base_amount - return token_price, base_amount, quote_amount + return token_price -async def calculate_sol_amount_from_token(client: AsyncClient, token_amount: float, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float: - _, base_amount, quote_amount = await get_token_price_from_pool(client, pool_base_token_account, pool_quote_token_account) - - k = base_amount * quote_amount - new_base_amount = base_amount + token_amount - new_quote_amount = k / new_base_amount - sol_amount = quote_amount - new_quote_amount - - sol_received = sol_amount * (1 - 0.25 / 100) - - return sol_received - -def create_ata_idempotent_ix(payer_pubkey, owner_pubkey): +def create_ata_idempotent_ix(payer_pubkey, owner_pubkey) -> Instruction: """ Create an instruction to create an Associated Token Account for WSOL in an idempotent way. """ @@ -131,32 +125,26 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_pool: Pubkey, payer: user_quote_token_account: Pubkey, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey, slippage: float = 0.25): - token_balance = await client.get_token_account_balance(user_quote_token_account) + # Get token balance + token_balance = int((await client.get_token_account_balance(user_base_token_account)).value.amount) token_balance_decimal = token_balance / 10**TOKEN_DECIMALS print(f"Token balance: {token_balance_decimal}") if token_balance == 0: print("No tokens to sell.") return + + # Calculate token price + token_price_sol = await calculate_token_pool_price(client, pool_base_token_account, pool_quote_token_account) + print(f"Price per Token: {token_price_sol:.20f} SOL") + + # Calculate minimum SOL output + amount = token_balance + min_sol_output = float(token_balance_decimal) * float(token_price_sol) + slippage_factor = 1 - slippage + min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL) - token_price, base_amount, quote_amount = await get_token_price_from_pool(client, pool_base_token_account, pool_quote_token_account) - - k = base_amount * quote_amount - new_quote_amount = quote_amount + token_balance_decimal - new_base_amount = k / new_quote_amount - expected_sol_amount = base_amount - new_base_amount - - expected_sol_after_fee = expected_sol_amount * (1 - 0.25 / 100) - min_sol_output_float = expected_sol_after_fee * (1 - slippage) - min_sol_output = min(int(min_sol_output_float * 10**9), 18_446_744_073_709_551_615) - print(f"Selling {token_balance_decimal} tokens") - print(f"Expected SOL: {expected_sol_after_fee:.9f}") - print(f"Min SOL with {slippage*100}% slippage: {min_sol_output/10**9:.9f}") - - print(f"Token balance to sell (raw): {token_balance}") - print(f"Min SOL output (raw): {min_sol_output}") - - data = SELL_DISCRIMINATOR + struct.pack(" Date: Tue, 1 Apr 2025 07:19:27 +0000 Subject: [PATCH 45/65] fix: final fixes in instructions, docstrings, comments --- learning-examples/manual_sell_pumpswap.py | 120 +++++++++++++++++----- 1 file changed, 96 insertions(+), 24 deletions(-) diff --git a/learning-examples/manual_sell_pumpswap.py b/learning-examples/manual_sell_pumpswap.py index 8a84a86..a10344e 100644 --- a/learning-examples/manual_sell_pumpswap.py +++ b/learning-examples/manual_sell_pumpswap.py @@ -17,15 +17,17 @@ from spl.token.instructions import get_associated_token_address load_dotenv() +# Configuration constants RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") TOKEN_MINT = Pubkey.from_string("35ySx7Rt3RqeTp75QB81FgRvPT5yDY2m5BupsUYDpump") PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) PAYER = Keypair.from_bytes(PRIVATE_KEY) -SLIPPAGE = 0.25 # Slippage tolerance +SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept TOKEN_DECIMALS = 6 -SELL_DISCRIMINATOR = struct.pack(" Pubkey: + """Find the market address for a given token mint. + + Searches for the AMM pool that contains the specified token as its base token. + + Args: + client: Solana RPC client instance + base_mint_address: Address of the token mint you want to find the market for + amm_program_id: Address of the AMM program + + Returns: + The Pubkey of the market (AMM pool) for the token + """ base_mint_bytes = bytes(base_mint_address) - offset = 43 # This should be calculated based on the fields before the base_mint field + offset = 43 # Offset where the base_mint field is stored in the account data structure filters = [ MemcmpOpts(offset=offset, bytes=base_mint_bytes) ] @@ -56,12 +70,25 @@ async def get_market_address_by_base_mint(client: AsyncClient, base_mint_address market_address = [account.pubkey for account in response.value][0] return market_address -async def get_market_data(client: AsyncClient, market_address: Pubkey): +async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: + """Fetch and parse market data from the blockchain. + + Retrieves and deserializes the data stored in the market account. + + Args: + client: Solana RPC client instance + market_address: Address of the market (AMM pool) to fetch data for + + Returns: + Dictionary containing the parsed market data + """ response = await client.get_account_info_json_parsed(market_address) data = response.value.data - parsed_data = {} + parsed_data: dict = {} + # Start after the 8-byte discriminator offset = 8 + # Define the structure of the market account data fields = [ ("pool_bump", "u8"), ("index", "u16"), @@ -95,49 +122,93 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey): return parsed_data async def calculate_token_pool_price(client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float: + """Calculate the price of tokens in the pool. + + Fetches the balance of tokens in the pool and calculates the price ratio. + + Args: + client: Solana RPC client instance + pool_base_token_account: Address of the pool's base token account (your token) + pool_quote_token_account: Address of the pool's quote token account (SOL) + + Returns: + The price of the base token in terms of the quote token (usually SOL) + """ base_balance_resp = await client.get_token_account_balance(pool_base_token_account) quote_balance_resp = await client.get_token_account_balance(pool_quote_token_account) + # Extract the UI amounts (human-readable with decimals) base_amount = float(base_balance_resp.value.ui_amount) quote_amount = float(quote_balance_resp.value.ui_amount) + token_price = quote_amount / base_amount return token_price -def create_ata_idempotent_ix(payer_pubkey, owner_pubkey) -> Instruction: +def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction: + """Create an instruction to create an Associated Token Account (ATA) if it doesn't exist. + + This creates an instruction that will create an Associated Token Account for SOL + if it doesn't already exist. + + Args: + payer_pubkey: The public key of the account that will pay for the creation + + Returns: + An instruction to create the ATA """ - Create an instruction to create an Associated Token Account for WSOL in an idempotent way. - """ - associated_token_address = get_associated_token_address(owner_pubkey, SOL) + associated_token_address = get_associated_token_address(payer_pubkey, SOL) + instruction_accounts = [ AccountMeta(pubkey=payer_pubkey, is_signer=True, is_writable=True), AccountMeta(pubkey=associated_token_address, is_signer=False, is_writable=True), - AccountMeta(pubkey=owner_pubkey, is_signer=False, is_writable=False), + AccountMeta(pubkey=payer_pubkey, is_signer=True, is_writable=True), AccountMeta(pubkey=SOL, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), ] + + # The data for creating an ATA idempotently is just a single byte with value 1 + # Check the details here: + # https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs data = bytes([1]) return Instruction(SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, data, instruction_accounts) -async def sell_pump_swap(client: AsyncClient, pump_fun_amm_pool: Pubkey, payer: Keypair, +async def sell_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer: Keypair, base_mint: Pubkey, user_base_token_account: Pubkey, user_quote_token_account: Pubkey, pool_base_token_account: Pubkey, - pool_quote_token_account: Pubkey, slippage: float = 0.25): + pool_quote_token_account: Pubkey, slippage: float = 0.25) -> str | None: + """Sell tokens on the PUMP AMM. + This function sells all tokens in the user's token account with the specified slippage tolerance. + + Args: + client: Solana RPC client instance + pump_fun_amm_market: Address of the AMM market + payer: Keypair of the transaction signer and token seller + base_mint: Address of the token mint being sold + user_base_token_account: Address of the user's token account for the token being sold + user_quote_token_account: Address of the user's SOL token account + pool_base_token_account: Address of the pool's token account for the token being sold + pool_quote_token_account: Address of the pool's SOL token account + slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%) + + Returns: + Transaction signature if successful, None otherwise + """ # Get token balance token_balance = int((await client.get_token_account_balance(user_base_token_account)).value.amount) token_balance_decimal = token_balance / 10**TOKEN_DECIMALS print(f"Token balance: {token_balance_decimal}") if token_balance == 0: print("No tokens to sell.") - return + return None # Calculate token price token_price_sol = await calculate_token_pool_price(client, pool_base_token_account, pool_quote_token_account) print(f"Price per Token: {token_price_sol:.20f} SOL") - # Calculate minimum SOL output + # Calculate minimum SOL output with slippage protection amount = token_balance min_sol_output = float(token_balance_decimal) * float(token_price_sol) slippage_factor = 1 - slippage @@ -146,10 +217,11 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_pool: Pubkey, payer: print(f"Selling {token_balance_decimal} tokens") print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") + # Define all accounts needed for the sell instruction accounts = [ - AccountMeta(pubkey=pump_fun_amm_pool, is_signer=False, is_writable=False), + AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=False), AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), - AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=True), AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False), AccountMeta(pubkey=SOL, is_signer=False, is_writable=False), AccountMeta(pubkey=user_base_token_account, is_signer=False, is_writable=True), @@ -173,20 +245,19 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_pool: Pubkey, payer: create_ata_ix = create_ata_idempotent_ix( payer_pubkey=payer.pubkey(), - owner_pubkey=payer.pubkey() ) sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts) blockhash_resp = await client.get_latest_blockhash() recent_blockhash = blockhash_resp.value.blockhash - + msg = Message.new_with_blockhash( [compute_limit_ix, compute_price_ix, create_ata_ix, sell_ix], payer.pubkey(), recent_blockhash ) - + tx_sell = VersionedTransaction( message=msg, keypairs=[payer] @@ -210,13 +281,14 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_pool: Pubkey, payer: async def main(): + """Main function to execute the token selling process.""" async with AsyncClient(RPC_ENDPOINT) as client: market_address = await get_market_address_by_base_mint(client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID) market_data = await get_market_data(client, market_address) - + await sell_pump_swap( client, - PUMP_AMM_PROGRAM_ID, + market_address, PAYER, TOKEN_MINT, get_associated_token_address(PAYER.pubkey(), TOKEN_MINT), @@ -227,4 +299,4 @@ async def main(): ) if __name__ == "__main__": - asyncio.run(main()) + asyncio.run(main()) \ No newline at end of file From 1064247e6d93f71b8358cd076b6d981488374f69 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 1 Apr 2025 14:37:13 +0000 Subject: [PATCH 46/65] chore: comments on base64 encoding --- src/core/client.py | 10 +++++----- src/monitoring/block_listener.py | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/client.py b/src/core/client.py index 7542613..b1b74b4 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -64,7 +64,7 @@ class SolanaClient: ValueError: If account doesn't exist or has no data """ client = await self.get_client() - response = await client.get_account_info(pubkey) + response = await client.get_account_info(pubkey, encoding="base64") # base64 encoding for account data by default if not response.value: raise ValueError(f"Account {pubkey} not found") return response.value @@ -149,7 +149,7 @@ class SolanaClient: wait_time = 2**attempt logger.warning( - f"Transaction attempt {attempt + 1} failed: {str(e)}, retrying in {wait_time}s" + f"Transaction attempt {attempt + 1} failed: {e!s}, retrying in {wait_time}s" ) await asyncio.sleep(wait_time) @@ -170,7 +170,7 @@ class SolanaClient: await client.confirm_transaction(signature, commitment=commitment) return True except Exception as e: - logger.error(f"Failed to confirm transaction {signature}: {str(e)}") + logger.error(f"Failed to confirm transaction {signature}: {e!s}") return False async def post_rpc(self, body: dict[str, Any]) -> dict[str, Any] | None: @@ -193,8 +193,8 @@ class SolanaClient: response.raise_for_status() return await response.json() except aiohttp.ClientError as e: - logger.error(f"RPC request failed: {str(e)}", exc_info=True) + logger.error(f"RPC request failed: {e!s}", exc_info=True) return None except json.JSONDecodeError as e: - logger.error(f"Failed to decode RPC response: {str(e)}", exc_info=True) + logger.error(f"Failed to decode RPC response: {e!s}", exc_info=True) return None diff --git a/src/monitoring/block_listener.py b/src/monitoring/block_listener.py index 846b761..5c3c5db 100644 --- a/src/monitoring/block_listener.py +++ b/src/monitoring/block_listener.py @@ -86,7 +86,7 @@ class BlockListener(BaseTokenListener): ping_task.cancel() except Exception as e: - logger.error(f"WebSocket connection error: {str(e)}") + logger.error(f"WebSocket connection error: {e!s}") logger.info("Reconnecting in 5 seconds...") await asyncio.sleep(5) @@ -105,7 +105,7 @@ class BlockListener(BaseTokenListener): {"mentionsAccountOrProgram": str(self.pump_program)}, { "commitment": "confirmed", - "encoding": "base64", + "encoding": "base64", # base64 is faster than other encoding options "showRewards": False, "transactionDetails": "full", "maxSupportedTransactionVersion": 0, @@ -129,7 +129,7 @@ class BlockListener(BaseTokenListener): try: pong_waiter = await websocket.ping() await asyncio.wait_for(pong_waiter, timeout=10) - except asyncio.TimeoutError: + except TimeoutError: logger.warning("Ping timeout - server not responding") # Force reconnection await websocket.close() @@ -137,7 +137,7 @@ class BlockListener(BaseTokenListener): except asyncio.CancelledError: pass except Exception as e: - logger.error(f"Ping error: {str(e)}") + logger.error(f"Ping error: {e!s}") async def _wait_for_token_creation(self, websocket) -> TokenInfo | None: """Wait for token creation event. @@ -176,12 +176,12 @@ class BlockListener(BaseTokenListener): if token_info: return token_info - except asyncio.TimeoutError: + except 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)}") + logger.error(f"Error processing WebSocket message: {e!s}") return None From c5a0258aa796780ebeb3c3c19aa2d221b13678a3 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 1 Apr 2025 14:57:01 +0000 Subject: [PATCH 47/65] fix: use base64 explicitly --- learning-examples/check_boding_curve_status.py | 2 +- learning-examples/cleanup_accounts.py | 2 +- learning-examples/fetch_price.py | 2 +- learning-examples/get_pumpswap_pools.py | 2 +- learning-examples/manual_buy.py | 4 ++-- learning-examples/manual_sell.py | 2 +- learning-examples/manual_sell_pumpswap.py | 6 +++--- src/cleanup/manager.py | 2 +- src/trading/buyer.py | 8 ++++---- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/learning-examples/check_boding_curve_status.py b/learning-examples/check_boding_curve_status.py index dfc1cb3..d66fafa 100644 --- a/learning-examples/check_boding_curve_status.py +++ b/learning-examples/check_boding_curve_status.py @@ -46,7 +46,7 @@ def get_associated_bonding_curve_address( async def get_bonding_curve_state( conn: AsyncClient, curve_address: Pubkey ) -> BondingCurveState: - response = await conn.get_account_info(curve_address) + 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") diff --git a/learning-examples/cleanup_accounts.py b/learning-examples/cleanup_accounts.py index d75d4fb..8809253 100644 --- a/learning-examples/cleanup_accounts.py +++ b/learning-examples/cleanup_accounts.py @@ -24,7 +24,7 @@ async def close_account_if_exists(client: SolanaClient, wallet: Wallet, account: """Safely close a token account if it exists and reclaim rent.""" try: solana_client = await client.get_client() - info = await solana_client.get_account_info(account) + info = await solana_client.get_account_info(account, encoding="base64") # base64 encoding for account data by deafult # WARNING: This will permanently burn all tokens in the account before closing it # Closing account is impossible if balance is positive diff --git a/learning-examples/fetch_price.py b/learning-examples/fetch_price.py index ac8a1b9..f332cb1 100644 --- a/learning-examples/fetch_price.py +++ b/learning-examples/fetch_price.py @@ -38,7 +38,7 @@ class BondingCurveState: async def get_bonding_curve_state( conn: AsyncClient, curve_address: Pubkey ) -> BondingCurveState: - response = await conn.get_account_info(curve_address) + 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") diff --git a/learning-examples/get_pumpswap_pools.py b/learning-examples/get_pumpswap_pools.py index 6a9606e..8b511d1 100644 --- a/learning-examples/get_pumpswap_pools.py +++ b/learning-examples/get_pumpswap_pools.py @@ -39,7 +39,7 @@ async def get_market_address_by_base_mint(base_mint_address: Pubkey, amm_program async def get_market_data(market_address: Pubkey): async with AsyncClient(RPC_ENDPOINT) as client: - response = await client.get_account_info_json_parsed(market_address) + response = await client.get_account_info(market_address, encoding="base64") data = response.value.data parsed_data = {} diff --git a/learning-examples/manual_buy.py b/learning-examples/manual_buy.py index 53b6b7e..a4850b8 100644 --- a/learning-examples/manual_buy.py +++ b/learning-examples/manual_buy.py @@ -63,7 +63,7 @@ class BondingCurveState: async def get_pump_curve_state( conn: AsyncClient, curve_address: Pubkey ) -> BondingCurveState: - response = await conn.get_account_info(curve_address) + 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") @@ -109,7 +109,7 @@ async def buy_token( # Create associated token account with retries for ata_attempt in range(max_retries): try: - account_info = await client.get_account_info(associated_token_account) + account_info = await client.get_account_info(associated_token_account, encoding="base64") if account_info.value is None: print( f"Creating associated token account (Attempt {ata_attempt + 1})..." diff --git a/learning-examples/manual_sell.py b/learning-examples/manual_sell.py index 45570db..c943801 100644 --- a/learning-examples/manual_sell.py +++ b/learning-examples/manual_sell.py @@ -59,7 +59,7 @@ class BondingCurveState: async def get_pump_curve_state( conn: AsyncClient, curve_address: Pubkey ) -> BondingCurveState: - response = await conn.get_account_info(curve_address) + 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") diff --git a/learning-examples/manual_sell_pumpswap.py b/learning-examples/manual_sell_pumpswap.py index a10344e..566cc56 100644 --- a/learning-examples/manual_sell_pumpswap.py +++ b/learning-examples/manual_sell_pumpswap.py @@ -19,7 +19,7 @@ load_dotenv() # Configuration constants RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") -TOKEN_MINT = Pubkey.from_string("35ySx7Rt3RqeTp75QB81FgRvPT5yDY2m5BupsUYDpump") +TOKEN_MINT = Pubkey.from_string("8XNrSusWrzYpizYXJb5yeB66AWX1cfQ86SBJFqVTpump") PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY")) PAYER = Keypair.from_bytes(PRIVATE_KEY) SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept @@ -63,7 +63,7 @@ async def get_market_address_by_base_mint(client: AsyncClient, base_mint_address response = await client.get_program_accounts( amm_program_id, - encoding="jsonParsed", + encoding="base64", filters=filters ) @@ -82,7 +82,7 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: Returns: Dictionary containing the parsed market data """ - response = await client.get_account_info_json_parsed(market_address) + response = await client.get_account_info(market_address, encoding="base64") data = response.value.data parsed_data: dict = {} diff --git a/src/cleanup/manager.py b/src/cleanup/manager.py index 286e009..d6c1851 100644 --- a/src/cleanup/manager.py +++ b/src/cleanup/manager.py @@ -45,7 +45,7 @@ class AccountCleanupManager: ) try: - info = await solana_client.get_account_info(ata) + info = await solana_client.get_account_info(ata, encoding="base64") if not info.value: logger.info(f"ATA {ata} does not exist or already closed.") return diff --git a/src/trading/buyer.py b/src/trading/buyer.py index 858dde1..92b40ef 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -121,7 +121,7 @@ class TokenBuyer(Trader): ) except Exception as e: - logger.error(f"Buy operation failed: {str(e)}") + logger.error(f"Buy operation failed: {e!s}") return TradeResult(success=False, error_message=str(e)) async def _ensure_associated_token_account( @@ -136,7 +136,7 @@ class TokenBuyer(Trader): try: solana_client = await self.client.get_client() account_info = await solana_client.get_account_info( - associated_token_account + associated_token_account, encoding="base64" ) if account_info.value is None: @@ -166,7 +166,7 @@ class TokenBuyer(Trader): ) except Exception as e: - logger.error(f"Error creating associated token account: {str(e)}") + logger.error(f"Error creating associated token account: {e!s}") raise async def _send_buy_transaction( @@ -245,5 +245,5 @@ class TokenBuyer(Trader): ), ) except Exception as e: - logger.error(f"Buy transaction failed: {str(e)}") + logger.error(f"Buy transaction failed: {e!s}") raise From 68c8251ccf2a1ae64c2cff6b925059608c73ff11 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Wed, 2 Apr 2025 15:36:58 +0000 Subject: [PATCH 48/65] feat: add track bonding curve progress script --- .../track_bonding_curve_progress.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 learning-examples/track_bonding_curve_progress.py diff --git a/learning-examples/track_bonding_curve_progress.py b/learning-examples/track_bonding_curve_progress.py new file mode 100644 index 0000000..11933c1 --- /dev/null +++ b/learning-examples/track_bonding_curve_progress.py @@ -0,0 +1,96 @@ +""" +Track bonding curve progress for a pump.fun token by mint address. +""" + +import asyncio +import os +import struct + +from dotenv import load_dotenv +from solana.rpc.async_api import AsyncClient +from solders.pubkey import Pubkey + +# Import pump.fun program address +from core.pubkeys import PumpAddresses + +load_dotenv() + +RPC_URL = os.getenv("SOLANA_NODE_RPC_ENDPOINT") +TOKEN_MINT = "xWrzYY4c1LnbSkLrd2LDUg9vw7YtVyJhGmw7MABpump" + +LAMPORTS_PER_SOL = 1_000_000_000 +TOKEN_DECIMALS = 6 +EXPECTED_DISCRIMINATOR = struct.pack(" Pubkey: + """Derive the bonding curve PDA address from mint.""" + return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], program_id)[0] + + +async def get_account_data(client: AsyncClient, pubkey: Pubkey) -> bytes: + """Fetch raw account data for a given public key.""" + resp = await client.get_account_info(pubkey, encoding="base64") + if not resp.value or not resp.value.data: + raise ValueError(f"Account {pubkey} not found or has no data") + + return resp.value.data + + +def parse_curve_state(data: bytes) -> dict: + """Decode bonding curve account data.""" + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid discriminator for bonding curve") + + fields = struct.unpack_from(" Date: Wed, 2 Apr 2025 22:43:16 +0200 Subject: [PATCH 49/65] docs: done reviewing base64 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 18c3b60..ae9317e 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ We assume no responsibility for the code or its usage. This is our public servic | | Error handling | Improving error handling | WIP | | | Configurable RPS | Ability to set RPS in the config to match your provider's and plan RPS (preferably [Chainstack](https://console.chainstack.com/) 🤩) | WIP | | | Dynamic priority fees | Ability to set dynamic priority fees | ✅ | -| | Review & optimize `json`, `jsonParsed`, `base64` | Improve speed and traffic for calls, not just `getBlock`. [Helpful overview](https://docs.chainstack.com/docs/solana-optimize-your-getblock-performance#json-jsonparsed-base58-base64).| WIP | +| | Review & optimize `json`, `jsonParsed`, `base64` | Improve speed and traffic for calls, not just `getBlock`. [Helpful overview](https://docs.chainstack.com/docs/solana-optimize-your-getblock-performance#json-jsonparsed-base58-base64).| ✅ | | **Stage 2: Bonding curve and migration management** | `logsSubscribe` integration | Integrate `logsSubscribe` instead of `blockSubscribe` for sniping minted tokens into the main bot | ✅ | | | Dual subscription methods | Keep both `logsSubscribe` & `blockSubscribe` in the main bot for flexibility and adapting to Solana node architecture changes | ✅ | | | Transaction retries | Do retries instead of cooldown and/or keep the cooldown | WIP | From ddb331f9e99689db92836eb385c87043132a2f2e Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 3 Apr 2025 14:49:01 +0000 Subject: [PATCH 50/65] feat: use idempotent ata instr, merge burn and close instr --- src/cleanup/manager.py | 32 +++++++++++----------- src/trading/buyer.py | 61 +++++++----------------------------------- 2 files changed, 27 insertions(+), 66 deletions(-) diff --git a/src/cleanup/manager.py b/src/cleanup/manager.py index d6c1851..9bcb8dc 100644 --- a/src/cleanup/manager.py +++ b/src/cleanup/manager.py @@ -51,6 +51,8 @@ class AccountCleanupManager: return balance = await self.client.get_token_account_balance(ata) + instructions = [] + if balance > 0 and CLEANUP_FORCE_CLOSE_WITH_BURN: logger.info(f"Burning {balance} tokens from ATA {ata} (mint: {mint})...") burn_ix = burn( @@ -62,13 +64,8 @@ class AccountCleanupManager: program_id=SystemAddresses.TOKEN_PROGRAM, ) ) - await self.client.build_and_send_transaction( - [burn_ix], - self.wallet.keypair, - skip_preflight=True, - priority_fee=priority_fee, - ) - logger.info(f"✅ Burned successfully from ATA {ata}") + instructions.append(burn_ix) + elif balance > 0: logger.info( f"Skipping ATA {ata} with non-zero balance ({balance} tokens) " @@ -76,6 +73,7 @@ class AccountCleanupManager: ) return + # Include close account instruction logger.info(f"Closing ATA: {ata}") close_ix = close_account( CloseAccountParams( @@ -85,14 +83,18 @@ class AccountCleanupManager: program_id=SystemAddresses.TOKEN_PROGRAM, ) ) - tx_sig = await self.client.build_and_send_transaction( - [close_ix], - self.wallet.keypair, - skip_preflight=True, - priority_fee=priority_fee, - ) - await self.client.confirm_transaction(tx_sig) - logger.info(f"Closed successfully: {ata}") + instructions.append(close_ix) + + # Send both burn and close instructions in the same transaction + if instructions: + tx_sig = await self.client.build_and_send_transaction( + instructions, + self.wallet.keypair, + skip_preflight=True, + priority_fee=priority_fee, + ) + await self.client.confirm_transaction(tx_sig) + logger.info(f"Closed successfully: {ata}") except Exception as e: logger.warning(f"Cleanup failed for ATA {ata}: {e!s}") diff --git a/src/trading/buyer.py b/src/trading/buyer.py index 92b40ef..e3aadfa 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -7,7 +7,7 @@ from typing import Final from solders.instruction import AccountMeta, Instruction from solders.pubkey import Pubkey -from spl.token.instructions import create_associated_token_account +from spl.token.instructions import create_idempotent_associated_token_account from core.client import SolanaClient from core.curve import BondingCurveManager @@ -93,10 +93,6 @@ class TokenBuyer(Trader): token_info.mint ) - await self._ensure_associated_token_account( - token_info.mint, associated_token_account - ) - tx_signature = await self._send_buy_transaction( token_info, associated_token_account, @@ -124,51 +120,6 @@ class TokenBuyer(Trader): logger.error(f"Buy operation failed: {e!s}") return TradeResult(success=False, error_message=str(e)) - async def _ensure_associated_token_account( - self, mint: Pubkey, associated_token_account: Pubkey - ) -> None: - """Ensure associated token account exists, else create it. - - Args: - mint: Token mint - associated_token_account: Associated token account address - """ - try: - solana_client = await self.client.get_client() - account_info = await solana_client.get_account_info( - associated_token_account, encoding="base64" - ) - - if account_info.value is None: - logger.info(f"Creating associated token account for {mint}...") - - create_ata_ix = create_associated_token_account( - payer=self.wallet.pubkey, owner=self.wallet.pubkey, mint=mint - ) - - tx_sig = await self.client.build_and_send_transaction( - [create_ata_ix], - self.wallet.keypair, - skip_preflight=True, - max_retries=self.max_retries, - priority_fee=await self.priority_fee_manager.calculate_priority_fee( - [mint, SystemAddresses.PROGRAM, SystemAddresses.TOKEN_PROGRAM] - ), - ) - - await self.client.confirm_transaction(tx_sig) - logger.info( - f"Associated token account created: {associated_token_account}" - ) - else: - logger.info( - f"Associated token account already exists: {associated_token_account}" - ) - - except Exception as e: - logger.error(f"Error creating associated token account: {e!s}") - raise - async def _send_buy_transaction( self, token_info: TokenInfo, @@ -225,6 +176,14 @@ class TokenBuyer(Trader): ), ] + # Prepare idempotent create ATA instruction: it will not fail if ATA already exists + idempotent_ata_ix = create_idempotent_associated_token_account( + self.wallet.pubkey, + self.wallet.pubkey, + token_info.mint, + SystemAddresses.TOKEN_PROGRAM + ) + # Prepare buy instruction data token_amount_raw = int(token_amount * 10**TOKEN_DECIMALS) data = ( @@ -236,7 +195,7 @@ class TokenBuyer(Trader): try: return await self.client.build_and_send_transaction( - [buy_ix], + [idempotent_ata_ix, buy_ix], self.wallet.keypair, skip_preflight=True, max_retries=self.max_retries, From 4eacc5a4f7d479dffe14c5cbac94ef51a814c57f Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 3 Apr 2025 21:57:36 +0000 Subject: [PATCH 51/65] feat: add EXTREME FAST mode for token buying --- src/config.py | 8 ++++++++ src/trading/buyer.py | 22 ++++++++++++++++------ src/trading/trader.py | 11 +++++++---- trades/trades.log | 4 ++++ 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/config.py b/src/config.py index e10a472..b90c11e 100644 --- a/src/config.py +++ b/src/config.py @@ -12,6 +12,14 @@ BUY_SLIPPAGE: float = 0.4 # Maximum acceptable price deviation (0.4 = 40%) SELL_SLIPPAGE: float = 0.4 # Consistent slippage tolerance to maintain trading strategy +# EXTREME FAST Mode configuration +# When enabled, skips waiting for the bonding curve to stabilize and RPC price check. +# The bot buys the specified number of tokens directly, making the process faster but less precise. +EXTREME_FAST_MODE: bool = False +# Amount of tokens to buy in EXTREME FAST mode. No price calculation is done; the bot buys exactly this amount. +EXTREME_FAST_TOKEN_AMOUNT: int = 30 + + # Priority fee configuration # Manage transaction speed and cost on the Solana network ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Adaptive fee calculation diff --git a/src/trading/buyer.py b/src/trading/buyer.py index 92b40ef..157e744 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -40,6 +40,8 @@ class TokenBuyer(Trader): amount: float, slippage: float = 0.01, max_retries: int = 5, + extreme_fast_token_amount: int = 0, + extreme_fast_mode: bool = False, ): """Initialize token buyer. @@ -50,6 +52,8 @@ class TokenBuyer(Trader): amount: Amount of SOL to spend slippage: Slippage tolerance (0.01 = 1%) max_retries: Maximum number of retry attempts + extreme_fast_token_amount: Amount of token to buy if extreme fast mode is enabled + extreme_fast_mode: If enabled, avoid fetching associated bonding curve state """ self.client = client self.wallet = wallet @@ -58,6 +62,8 @@ class TokenBuyer(Trader): self.amount = amount self.slippage = slippage self.max_retries = max_retries + self.extreme_fast_mode = extreme_fast_mode + self.extreme_fast_token_amount = extreme_fast_token_amount async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult: """Execute buy operation. @@ -72,12 +78,16 @@ class TokenBuyer(Trader): # Convert amount to lamports amount_lamports = int(self.amount * LAMPORTS_PER_SOL) - # Fetch token price - curve_state = await self.curve_manager.get_curve_state( - token_info.bonding_curve - ) - token_price_sol = curve_state.calculate_price() - token_amount = self.amount / token_price_sol + if self.extreme_fast_mode: + # Skip the wait and directly calculate the amount + token_amount = self.extreme_fast_token_amount + token_price_sol = self.amount / token_amount + logger.info(f"EXTREME FAST Mode: Buying {token_amount} tokens.") + else: + # Regular behavior with RPC call + curve_state = await self.curve_manager.get_curve_state(token_info.bonding_curve) + token_price_sol = curve_state.calculate_price() + token_amount = self.amount / token_price_sol # Calculate maximum SOL to spend with slippage max_amount_lamports = int(amount_lamports * (1 + self.slippage)) diff --git a/src/trading/trader.py b/src/trading/trader.py index c5cf832..ea306e1 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -78,6 +78,8 @@ class PumpTrader: buy_amount, buy_slippage, max_retries, + config.EXTREME_FAST_TOKEN_AMOUNT, + config.EXTREME_FAST_MODE ) self.seller = TokenSeller( @@ -212,10 +214,11 @@ class PumpTrader: try: await self._save_token_info(token_info) - logger.info( - f"Waiting for {config.WAIT_TIME_AFTER_CREATION} seconds for the bonding curve to stabilize..." - ) - await asyncio.sleep(config.WAIT_TIME_AFTER_CREATION) + if not config.EXTREME_FAST_MODE: + logger.info( + f"Waiting for {config.WAIT_TIME_AFTER_CREATION} seconds for the bonding curve to stabilize..." + ) + await asyncio.sleep(config.WAIT_TIME_AFTER_CREATION) logger.info( f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol}..." diff --git a/trades/trades.log b/trades/trades.log index 3a4a9aa..d44d1ed 100644 --- a/trades/trades.log +++ b/trades/trades.log @@ -6,3 +6,7 @@ {"timestamp": "2025-03-06T22:04:48.843998", "action": "sell", "token_address": "5jch2xH14RBWZgKwmxEUJwC4howFNBCBqbnZ5uguGujj", "symbol": "$MOONWEN", "price": 2.8056120698010214e-08, "amount": 35.033737, "tx_hash": "RLx8hFVd5DVZdUg5vN41Fs8Yz66yTEUEvegasuHwYQ7DTJhPcCdggoNBtipb93VPVdtQLZydxc9ZKRZ1uzK6KzH"} {"timestamp": "2025-03-06T22:05:11.444317", "action": "buy", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.383038210726932e-08, "amount": 29.559228649242023, "tx_hash": "3E4RT5kvRezWJmQKJ9wmcAiDcYZefohxFVNdEZPweE9vhq215rSmqBn4Dv4ypH5KUUuNJzKdhjyrGU2b47woWr9e"} {"timestamp": "2025-03-06T22:05:25.789876", "action": "sell", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.3830384158620706e-08, "amount": 29.559228, "tx_hash": "3oyJVzsRvnQV5qBsEp4hSk97eFWzUjzfWWa16GMUXtMd7JBmbQkTkmvs7N8nagYcJvqfFhzhCKZEG6onxjYYQbwX"} +{"timestamp": "2025-04-03T21:55:46.849123", "action": "buy", "token_address": "4rHSLfYLkFmHTcyCF8thnUmyuX4MBrHdjdftZXM7pump", "symbol": "GCT", "price": 3.3333333333333334e-08, "amount": 30, "tx_hash": "35UncvxypdvuKC4gD3jXnmmtwfBfEhFc2Y2JLfszzvHqsZumcjp2CMkXH4HD7iexgkPEpG1je73bwnFWbNQjxNUS"} +{"timestamp": "2025-04-03T21:56:05.816803", "action": "sell", "token_address": "4rHSLfYLkFmHTcyCF8thnUmyuX4MBrHdjdftZXM7pump", "symbol": "GCT", "price": 3.1716731316027684e-08, "amount": 30.0, "tx_hash": "2NoMsScE5tJu3gzvrSn1uarnVZ4DcgXVLsc7BEwJ1ndU7T69xPbgyk53YW4MVCEfrSvdoaCup6VyM9pnf6YR6pGc"} +{"timestamp": "2025-04-03T21:56:49.686823", "action": "buy", "token_address": "Hx4wR3Z7tEjfVip8jHFFsF3d4LwNmXgjBRkNR9qSUarF", "symbol": "Pablo", "price": 5.067234074416521e-08, "amount": 19.734632055953472, "tx_hash": "52hQYXPjtP1Rr945UW5peef6qbxThUubyBbEbcnGCCJ146nxx6bZXj3mCpWtpqpmMe82oNkEFwMms26JqhAaJMXL"} +{"timestamp": "2025-04-03T21:57:09.656288", "action": "sell", "token_address": "Hx4wR3Z7tEjfVip8jHFFsF3d4LwNmXgjBRkNR9qSUarF", "symbol": "Pablo", "price": 2.8113188162567704e-08, "amount": 19.734632, "tx_hash": "xzpCFN8PgtPvXyRfQjkhHkPQcdzTNb6JXtCsQixtsMQabiBt9yNMSb7JLxRCT295rbVZuuocRNxYe6AqAiRnE91"} From 9b9a86a506d1522d818e9cf520b2bb49c4347453 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Fri, 4 Apr 2025 21:31:00 +0000 Subject: [PATCH 52/65] feat: add blockhash caching --- src/core/client.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/core/client.py b/src/core/client.py index b1b74b4..ac64487 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -34,6 +34,28 @@ class SolanaClient: """ self.rpc_endpoint = rpc_endpoint self._client = None + self._cached_blockhash: Hash | None = None + self._blockhash_lock = asyncio.Lock() + + asyncio.create_task(self.start_blockhash_updater()) + + async def start_blockhash_updater(self, interval: float = 5.0): + """Start background task to update recent blockhash.""" + while True: + try: + blockhash = await self.get_latest_blockhash() + async with self._blockhash_lock: + self._cached_blockhash = blockhash + except Exception as e: + logger.warning(f"Blockhash fetch failed: {e!s}") + await asyncio.sleep(interval) + + async def get_cached_blockhash(self) -> Hash: + """Return the most recently cached blockhash.""" + async with self._blockhash_lock: + if self._cached_blockhash is None: + raise RuntimeError("No cached blockhash available yet") + return self._cached_blockhash async def get_client(self) -> AsyncClient: """Get or create the AsyncClient instance. @@ -128,7 +150,7 @@ class SolanaClient: ] instructions = fee_instructions + instructions - recent_blockhash = await self.get_latest_blockhash() + recent_blockhash = await self.get_cached_blockhash() message = Message(instructions, signer_keypair.pubkey()) transaction = Transaction([signer_keypair], message, recent_blockhash) From 7cfd89d1ae60371340b31a754b50e4572f852035 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Fri, 4 Apr 2025 21:38:36 +0000 Subject: [PATCH 53/65] fix: gracefully cancel blockhash updater task --- src/core/client.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/client.py b/src/core/client.py index ac64487..a39140b 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -36,8 +36,7 @@ class SolanaClient: self._client = None self._cached_blockhash: Hash | None = None self._blockhash_lock = asyncio.Lock() - - asyncio.create_task(self.start_blockhash_updater()) + self._blockhash_updater_task = asyncio.create_task(self.start_blockhash_updater()) async def start_blockhash_updater(self, interval: float = 5.0): """Start background task to update recent blockhash.""" @@ -68,7 +67,14 @@ class SolanaClient: return self._client async def close(self): - """Close the client connection if open.""" + """Close the client connection and stop the blockhash updater.""" + if self._blockhash_updater_task: + self._blockhash_updater_task.cancel() + try: + await self._blockhash_updater_task + except asyncio.CancelledError: + pass + if self._client: await self._client.close() self._client = None From 18ee63b2b9216fe04972309f20a662efe0ea1eae Mon Sep 17 00:00:00 2001 From: smypmsa Date: Wed, 9 Apr 2025 11:56:44 +0000 Subject: [PATCH 54/65] feat: add get graduating tokens script --- learning-examples/get_graduating_tokens.py | 84 ++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 learning-examples/get_graduating_tokens.py diff --git a/learning-examples/get_graduating_tokens.py b/learning-examples/get_graduating_tokens.py new file mode 100644 index 0000000..1695c13 --- /dev/null +++ b/learning-examples/get_graduating_tokens.py @@ -0,0 +1,84 @@ +import asyncio +import os +import struct + +from dotenv import load_dotenv +from solana.rpc.async_api import AsyncClient +from solana.rpc.types import MemcmpOpts +from solders.pubkey import Pubkey + +load_dotenv() + +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT1") +PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") +# Known 8-byte discriminator for bonding curve accounts +# This ensures we're only fetching bonding curve accounts +BONDING_CURVE_DISCRIMINATOR_BYTES = bytes.fromhex("17b7f83760d8ac60") + + +async def get_bonding_curves_by_reserves(): + """ + Fetch bonding curve accounts from the Pump.fun program that have real_token_reserves + below a defined threshold and are not marked as complete (i.e., not migrated). + """ + + # Define the reserve threshold we're interested in + # 100 trillion (in token base units) + threshold = 100_000_000_000_000 + + # Convert the threshold into 8-byte little-endian format (as stored on-chain) + threshold_bytes = threshold.to_bytes(8, 'little') + + # Extract the 2 most significant bytes (bytes 6 and 7 in little-endian) + # This helps us pre-filter for values less than 2^48 (~281T) + msb_prefix = threshold_bytes[6:] + + async with AsyncClient(RPC_ENDPOINT, commitment="processed", timeout=120) as client: + # Define on-chain filters for getProgramAccounts + filters = [ + MemcmpOpts(offset=0, bytes=BONDING_CURVE_DISCRIMINATOR_BYTES), # only bonding curve accounts + MemcmpOpts(offset=30, bytes=msb_prefix), # real_token_reserves < ~281T + MemcmpOpts(offset=48, bytes=b'\x00'), # complete == False (not migrated) + ] + + # Query accounts matching filters + response = await client.get_program_accounts( + PUMP_PROGRAM_ID, + encoding="base64", + filters=filters + ) + + result = [] + for acc in response.value: + raw = acc.account.data + + # Parse the account layout according to known struct (49 bytes): + # [discriminator][virtual_token_reserves][virtual_sol_reserves] + # [real_token_reserves][real_sol_reserves][token_total_supply][complete] + # Example: https://solscan.io/account/ASwuqjAGjWVhojrdot9yrnXTV4hMdTEryAWkGn4UUJma#anchorData + + offset = 8 # skip 8-byte discriminator + offset += 8 # skip virtual_token_reserves + offset += 8 # skip virtual_sol_reserves + + # Extract real_token_reserves (u64 = 8 bytes, little-endian) + real_token_reserves = struct.unpack(" Date: Sun, 13 Apr 2025 20:23:58 +0000 Subject: [PATCH 55/65] feat: add mint address extraction for graduating tokens --- learning-examples/get_graduating_tokens.py | 140 +++++++++++++++++---- 1 file changed, 117 insertions(+), 23 deletions(-) diff --git a/learning-examples/get_graduating_tokens.py b/learning-examples/get_graduating_tokens.py index 1695c13..df72be3 100644 --- a/learning-examples/get_graduating_tokens.py +++ b/learning-examples/get_graduating_tokens.py @@ -4,24 +4,31 @@ import struct from dotenv import load_dotenv from solana.rpc.async_api import AsyncClient -from solana.rpc.types import MemcmpOpts +from solana.rpc.types import MemcmpOpts, TokenAccountOpts from solders.pubkey import Pubkey load_dotenv() RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT1") PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") -# Known 8-byte discriminator for bonding curve accounts -# This ensures we're only fetching bonding curve accounts +TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + +# The 8-byte discriminator for bonding curve accounts in Pump.fun +# Used to identify account types in getProgramAccounts requests BONDING_CURVE_DISCRIMINATOR_BYTES = bytes.fromhex("17b7f83760d8ac60") -async def get_bonding_curves_by_reserves(): +async def get_bonding_curves_by_reserves(client: AsyncClient | None = None) -> list: """ Fetch bonding curve accounts from the Pump.fun program that have real_token_reserves below a defined threshold and are not marked as complete (i.e., not migrated). + + Args: + client: Optional AsyncClient instance. If None, a new one will be created. + + Returns: + List of bonding curve accounts matching the criteria """ - # Define the reserve threshold we're interested in # 100 trillion (in token base units) threshold = 100_000_000_000_000 @@ -29,16 +36,24 @@ async def get_bonding_curves_by_reserves(): # Convert the threshold into 8-byte little-endian format (as stored on-chain) threshold_bytes = threshold.to_bytes(8, 'little') - # Extract the 2 most significant bytes (bytes 6 and 7 in little-endian) - # This helps us pre-filter for values less than 2^48 (~281T) + # Extract the 2 most significant bytes to pre-filter values less than 2^48 (~281T) + # This optimization reduces the number of accounts returned by the RPC msb_prefix = threshold_bytes[6:] - async with AsyncClient(RPC_ENDPOINT, commitment="processed", timeout=120) as client: + should_close_client = client is None + try: + if should_close_client: + client = AsyncClient(RPC_ENDPOINT, commitment="processed", timeout=120) + await client.is_connected() + # Define on-chain filters for getProgramAccounts filters = [ - MemcmpOpts(offset=0, bytes=BONDING_CURVE_DISCRIMINATOR_BYTES), # only bonding curve accounts - MemcmpOpts(offset=30, bytes=msb_prefix), # real_token_reserves < ~281T - MemcmpOpts(offset=48, bytes=b'\x00'), # complete == False (not migrated) + # Match only bonding curve accounts + MemcmpOpts(offset=0, bytes=BONDING_CURVE_DISCRIMINATOR_BYTES), + # Real token reserves MSB bytes (pre-filter) + MemcmpOpts(offset=30, bytes=msb_prefix), + # Complete flag is False (not migrated) + MemcmpOpts(offset=48, bytes=b'\x00'), ] # Query accounts matching filters @@ -52,18 +67,20 @@ async def get_bonding_curves_by_reserves(): for acc in response.value: raw = acc.account.data - # Parse the account layout according to known struct (49 bytes): - # [discriminator][virtual_token_reserves][virtual_sol_reserves] - # [real_token_reserves][real_sol_reserves][token_total_supply][complete] - # Example: https://solscan.io/account/ASwuqjAGjWVhojrdot9yrnXTV4hMdTEryAWkGn4UUJma#anchorData - - offset = 8 # skip 8-byte discriminator - offset += 8 # skip virtual_token_reserves - offset += 8 # skip virtual_sol_reserves - + # Parse account data according to the Pump.fun bonding curve layout: + # [8] discriminator + # [8] virtual_token_reserves (u64) + # [8] virtual_sol_reserves (u64) + # [8] real_token_reserves (u64) + # [8] real_sol_reserves (u64) + # [8] token_total_supply (u64) + # [1] complete (bool) + + # Skip to real_token_reserves field (8 + 8 + 8 = 24 bytes offset) + offset = 24 + # Extract real_token_reserves (u64 = 8 bytes, little-endian) real_token_reserves = struct.unpack(" 0: + return response.value[0].account + else: + print(f"No token accounts found for {bonding_curve_address}") + return None + except Exception as e: + print(f"Error finding associated token account: {e}") + return None + finally: + if should_close_client and client: + await client.close() + + +def get_mint_address(data: bytes) -> str: + """ + Extract the mint address from SPL token account data. + + In SPL token account data, the mint address is stored in the first 32 bytes. + + Args: + data: The token account data as bytes + + Returns: + The mint address as a base58-encoded string + """ + # The mint address is stored in the first 32 bytes + mint_bytes = data[:32] + return str(Pubkey(mint_bytes)) async def main(): - bonding_curves_response = await get_bonding_curves_by_reserves() - print(f"Total matches: {len(bonding_curves_response)}") + async with AsyncClient(RPC_ENDPOINT, commitment="processed", timeout=120) as client: + await client.is_connected() + + bonding_curves = await get_bonding_curves_by_reserves(client) + print(f"Total matches: {len(bonding_curves)}") + print("="*50) + + for bonding_curve in bonding_curves: + # Find the SPL token account owned by the bonding curve + associated_token_account = await find_associated_bonding_curve( + str(bonding_curve.pubkey), client + ) + + if associated_token_account: + mint_address = get_mint_address(associated_token_account.data) + print(f"Bonding curve: {bonding_curve.pubkey}") + print(f"Mint address: {mint_address}") + print("="*50) + + # For demonstration, only process the first curve + break if __name__ == "__main__": From b9e29a8b128378e90830e1e489dc0b9472acf9b5 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Mon, 14 Apr 2025 15:02:52 +0000 Subject: [PATCH 56/65] feat: add geyser listener script --- learning-examples/generated/__init__.py | 0 learning-examples/generated/geyser_pb2.py | 140 +++++ learning-examples/generated/geyser_pb2.pyi | 485 ++++++++++++++++++ .../generated/geyser_pb2_grpc.py | 355 +++++++++++++ .../generated/solana_storage_pb2.py | 75 +++ .../generated/solana_storage_pb2.pyi | 238 +++++++++ .../generated/solana_storage_pb2_grpc.py | 24 + .../listen_create_from_geyser.py | 119 +++++ learning-examples/proto/geyser.proto | 262 ++++++++++ learning-examples/proto/solana-storage.proto | 149 ++++++ pyproject.toml | 5 +- 11 files changed, 1851 insertions(+), 1 deletion(-) create mode 100644 learning-examples/generated/__init__.py create mode 100644 learning-examples/generated/geyser_pb2.py create mode 100644 learning-examples/generated/geyser_pb2.pyi create mode 100644 learning-examples/generated/geyser_pb2_grpc.py create mode 100644 learning-examples/generated/solana_storage_pb2.py create mode 100644 learning-examples/generated/solana_storage_pb2.pyi create mode 100644 learning-examples/generated/solana_storage_pb2_grpc.py create mode 100644 learning-examples/listen_create_from_geyser.py create mode 100644 learning-examples/proto/geyser.proto create mode 100644 learning-examples/proto/solana-storage.proto diff --git a/learning-examples/generated/__init__.py b/learning-examples/generated/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/learning-examples/generated/geyser_pb2.py b/learning-examples/generated/geyser_pb2.py new file mode 100644 index 0000000..4db6702 --- /dev/null +++ b/learning-examples/generated/geyser_pb2.py @@ -0,0 +1,140 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: geyser.proto +# Protobuf Python Version: 5.29.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 0, + '', + 'geyser.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + +from generated.solana_storage_pb2 import * + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cgeyser.proto\x12\x06geyser\x1a\x14solana-storage.proto\"\xf6\t\n\x10SubscribeRequest\x12\x38\n\x08\x61\x63\x63ounts\x18\x01 \x03(\x0b\x32&.geyser.SubscribeRequest.AccountsEntry\x12\x32\n\x05slots\x18\x02 \x03(\x0b\x32#.geyser.SubscribeRequest.SlotsEntry\x12@\n\x0ctransactions\x18\x03 \x03(\x0b\x32*.geyser.SubscribeRequest.TransactionsEntry\x12M\n\x13transactions_status\x18\n \x03(\x0b\x32\x30.geyser.SubscribeRequest.TransactionsStatusEntry\x12\x34\n\x06\x62locks\x18\x04 \x03(\x0b\x32$.geyser.SubscribeRequest.BlocksEntry\x12=\n\x0b\x62locks_meta\x18\x05 \x03(\x0b\x32(.geyser.SubscribeRequest.BlocksMetaEntry\x12\x32\n\x05\x65ntry\x18\x08 \x03(\x0b\x32#.geyser.SubscribeRequest.EntryEntry\x12\x30\n\ncommitment\x18\x06 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x12\x46\n\x13\x61\x63\x63ounts_data_slice\x18\x07 \x03(\x0b\x32).geyser.SubscribeRequestAccountsDataSlice\x12/\n\x04ping\x18\t \x01(\x0b\x32\x1c.geyser.SubscribeRequestPingH\x01\x88\x01\x01\x1aW\n\rAccountsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32&.geyser.SubscribeRequestFilterAccounts:\x02\x38\x01\x1aQ\n\nSlotsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x32\n\x05value\x18\x02 \x01(\x0b\x32#.geyser.SubscribeRequestFilterSlots:\x02\x38\x01\x1a_\n\x11TransactionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x39\n\x05value\x18\x02 \x01(\x0b\x32*.geyser.SubscribeRequestFilterTransactions:\x02\x38\x01\x1a\x65\n\x17TransactionsStatusEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x39\n\x05value\x18\x02 \x01(\x0b\x32*.geyser.SubscribeRequestFilterTransactions:\x02\x38\x01\x1aS\n\x0b\x42locksEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.geyser.SubscribeRequestFilterBlocks:\x02\x38\x01\x1a[\n\x0f\x42locksMetaEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x37\n\x05value\x18\x02 \x01(\x0b\x32(.geyser.SubscribeRequestFilterBlocksMeta:\x02\x38\x01\x1aQ\n\nEntryEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x32\n\x05value\x18\x02 \x01(\x0b\x32#.geyser.SubscribeRequestFilterEntry:\x02\x38\x01\x42\r\n\x0b_commitmentB\x07\n\x05_ping\"\xbf\x01\n\x1eSubscribeRequestFilterAccounts\x12\x0f\n\x07\x61\x63\x63ount\x18\x02 \x03(\t\x12\r\n\x05owner\x18\x03 \x03(\t\x12=\n\x07\x66ilters\x18\x04 \x03(\x0b\x32,.geyser.SubscribeRequestFilterAccountsFilter\x12#\n\x16nonempty_txn_signature\x18\x05 \x01(\x08H\x00\x88\x01\x01\x42\x19\n\x17_nonempty_txn_signature\"\xf3\x01\n$SubscribeRequestFilterAccountsFilter\x12\x44\n\x06memcmp\x18\x01 \x01(\x0b\x32\x32.geyser.SubscribeRequestFilterAccountsFilterMemcmpH\x00\x12\x12\n\x08\x64\x61tasize\x18\x02 \x01(\x04H\x00\x12\x1d\n\x13token_account_state\x18\x03 \x01(\x08H\x00\x12H\n\x08lamports\x18\x04 \x01(\x0b\x32\x34.geyser.SubscribeRequestFilterAccountsFilterLamportsH\x00\x42\x08\n\x06\x66ilter\"y\n*SubscribeRequestFilterAccountsFilterMemcmp\x12\x0e\n\x06offset\x18\x01 \x01(\x04\x12\x0f\n\x05\x62ytes\x18\x02 \x01(\x0cH\x00\x12\x10\n\x06\x62\x61se58\x18\x03 \x01(\tH\x00\x12\x10\n\x06\x62\x61se64\x18\x04 \x01(\tH\x00\x42\x06\n\x04\x64\x61ta\"m\n,SubscribeRequestFilterAccountsFilterLamports\x12\x0c\n\x02\x65q\x18\x01 \x01(\x04H\x00\x12\x0c\n\x02ne\x18\x02 \x01(\x04H\x00\x12\x0c\n\x02lt\x18\x03 \x01(\x04H\x00\x12\x0c\n\x02gt\x18\x04 \x01(\x04H\x00\x42\x05\n\x03\x63mp\"Y\n\x1bSubscribeRequestFilterSlots\x12!\n\x14\x66ilter_by_commitment\x18\x01 \x01(\x08H\x00\x88\x01\x01\x42\x17\n\x15_filter_by_commitment\"\xd2\x01\n\"SubscribeRequestFilterTransactions\x12\x11\n\x04vote\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x13\n\x06\x66\x61iled\x18\x02 \x01(\x08H\x01\x88\x01\x01\x12\x16\n\tsignature\x18\x05 \x01(\tH\x02\x88\x01\x01\x12\x17\n\x0f\x61\x63\x63ount_include\x18\x03 \x03(\t\x12\x17\n\x0f\x61\x63\x63ount_exclude\x18\x04 \x03(\t\x12\x18\n\x10\x61\x63\x63ount_required\x18\x06 \x03(\tB\x07\n\x05_voteB\t\n\x07_failedB\x0c\n\n_signature\"\xd9\x01\n\x1cSubscribeRequestFilterBlocks\x12\x17\n\x0f\x61\x63\x63ount_include\x18\x01 \x03(\t\x12!\n\x14include_transactions\x18\x02 \x01(\x08H\x00\x88\x01\x01\x12\x1d\n\x10include_accounts\x18\x03 \x01(\x08H\x01\x88\x01\x01\x12\x1c\n\x0finclude_entries\x18\x04 \x01(\x08H\x02\x88\x01\x01\x42\x17\n\x15_include_transactionsB\x13\n\x11_include_accountsB\x12\n\x10_include_entries\"\"\n SubscribeRequestFilterBlocksMeta\"\x1d\n\x1bSubscribeRequestFilterEntry\"C\n!SubscribeRequestAccountsDataSlice\x12\x0e\n\x06offset\x18\x01 \x01(\x04\x12\x0e\n\x06length\x18\x02 \x01(\x04\"\"\n\x14SubscribeRequestPing\x12\n\n\x02id\x18\x01 \x01(\x05\"\x85\x04\n\x0fSubscribeUpdate\x12\x0f\n\x07\x66ilters\x18\x01 \x03(\t\x12\x31\n\x07\x61\x63\x63ount\x18\x02 \x01(\x0b\x32\x1e.geyser.SubscribeUpdateAccountH\x00\x12+\n\x04slot\x18\x03 \x01(\x0b\x32\x1b.geyser.SubscribeUpdateSlotH\x00\x12\x39\n\x0btransaction\x18\x04 \x01(\x0b\x32\".geyser.SubscribeUpdateTransactionH\x00\x12\x46\n\x12transaction_status\x18\n \x01(\x0b\x32(.geyser.SubscribeUpdateTransactionStatusH\x00\x12-\n\x05\x62lock\x18\x05 \x01(\x0b\x32\x1c.geyser.SubscribeUpdateBlockH\x00\x12+\n\x04ping\x18\x06 \x01(\x0b\x32\x1b.geyser.SubscribeUpdatePingH\x00\x12+\n\x04pong\x18\t \x01(\x0b\x32\x1b.geyser.SubscribeUpdatePongH\x00\x12\x36\n\nblock_meta\x18\x07 \x01(\x0b\x32 .geyser.SubscribeUpdateBlockMetaH\x00\x12-\n\x05\x65ntry\x18\x08 \x01(\x0b\x32\x1c.geyser.SubscribeUpdateEntryH\x00\x42\x0e\n\x0cupdate_oneof\"o\n\x16SubscribeUpdateAccount\x12\x33\n\x07\x61\x63\x63ount\x18\x01 \x01(\x0b\x32\".geyser.SubscribeUpdateAccountInfo\x12\x0c\n\x04slot\x18\x02 \x01(\x04\x12\x12\n\nis_startup\x18\x03 \x01(\x08\"\xc8\x01\n\x1aSubscribeUpdateAccountInfo\x12\x0e\n\x06pubkey\x18\x01 \x01(\x0c\x12\x10\n\x08lamports\x18\x02 \x01(\x04\x12\r\n\x05owner\x18\x03 \x01(\x0c\x12\x12\n\nexecutable\x18\x04 \x01(\x08\x12\x12\n\nrent_epoch\x18\x05 \x01(\x04\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x15\n\rwrite_version\x18\x07 \x01(\x04\x12\x1a\n\rtxn_signature\x18\x08 \x01(\x0cH\x00\x88\x01\x01\x42\x10\n\x0e_txn_signature\"\x94\x01\n\x13SubscribeUpdateSlot\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x13\n\x06parent\x18\x02 \x01(\x04H\x00\x88\x01\x01\x12\'\n\x06status\x18\x03 \x01(\x0e\x32\x17.geyser.CommitmentLevel\x12\x17\n\ndead_error\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\t\n\x07_parentB\r\n\x0b_dead_error\"g\n\x1aSubscribeUpdateTransaction\x12;\n\x0btransaction\x18\x01 \x01(\x0b\x32&.geyser.SubscribeUpdateTransactionInfo\x12\x0c\n\x04slot\x18\x02 \x01(\x04\"\xd8\x01\n\x1eSubscribeUpdateTransactionInfo\x12\x11\n\tsignature\x18\x01 \x01(\x0c\x12\x0f\n\x07is_vote\x18\x02 \x01(\x08\x12?\n\x0btransaction\x18\x03 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.Transaction\x12\x42\n\x04meta\x18\x04 \x01(\x0b\x32\x34.solana.storage.ConfirmedBlock.TransactionStatusMeta\x12\r\n\x05index\x18\x05 \x01(\x04\"\xa1\x01\n SubscribeUpdateTransactionStatus\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0f\n\x07is_vote\x18\x03 \x01(\x08\x12\r\n\x05index\x18\x04 \x01(\x04\x12<\n\x03\x65rr\x18\x05 \x01(\x0b\x32/.solana.storage.ConfirmedBlock.TransactionError\"\xa0\x04\n\x14SubscribeUpdateBlock\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tblockhash\x18\x02 \x01(\t\x12\x37\n\x07rewards\x18\x03 \x01(\x0b\x32&.solana.storage.ConfirmedBlock.Rewards\x12@\n\nblock_time\x18\x04 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.UnixTimestamp\x12@\n\x0c\x62lock_height\x18\x05 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.BlockHeight\x12\x13\n\x0bparent_slot\x18\x07 \x01(\x04\x12\x18\n\x10parent_blockhash\x18\x08 \x01(\t\x12\"\n\x1a\x65xecuted_transaction_count\x18\t \x01(\x04\x12<\n\x0ctransactions\x18\x06 \x03(\x0b\x32&.geyser.SubscribeUpdateTransactionInfo\x12\x1d\n\x15updated_account_count\x18\n \x01(\x04\x12\x34\n\x08\x61\x63\x63ounts\x18\x0b \x03(\x0b\x32\".geyser.SubscribeUpdateAccountInfo\x12\x15\n\rentries_count\x18\x0c \x01(\x04\x12-\n\x07\x65ntries\x18\r \x03(\x0b\x32\x1c.geyser.SubscribeUpdateEntry\"\xe2\x02\n\x18SubscribeUpdateBlockMeta\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tblockhash\x18\x02 \x01(\t\x12\x37\n\x07rewards\x18\x03 \x01(\x0b\x32&.solana.storage.ConfirmedBlock.Rewards\x12@\n\nblock_time\x18\x04 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.UnixTimestamp\x12@\n\x0c\x62lock_height\x18\x05 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.BlockHeight\x12\x13\n\x0bparent_slot\x18\x06 \x01(\x04\x12\x18\n\x10parent_blockhash\x18\x07 \x01(\t\x12\"\n\x1a\x65xecuted_transaction_count\x18\x08 \x01(\x04\x12\x15\n\rentries_count\x18\t \x01(\x04\"\x9d\x01\n\x14SubscribeUpdateEntry\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\r\n\x05index\x18\x02 \x01(\x04\x12\x12\n\nnum_hashes\x18\x03 \x01(\x04\x12\x0c\n\x04hash\x18\x04 \x01(\x0c\x12\"\n\x1a\x65xecuted_transaction_count\x18\x05 \x01(\x04\x12\"\n\x1astarting_transaction_index\x18\x06 \x01(\x04\"\x15\n\x13SubscribeUpdatePing\"!\n\x13SubscribeUpdatePong\x12\n\n\x02id\x18\x01 \x01(\x05\"\x1c\n\x0bPingRequest\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\"\x1d\n\x0cPongResponse\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\"\\\n\x19GetLatestBlockhashRequest\x12\x30\n\ncommitment\x18\x01 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\"^\n\x1aGetLatestBlockhashResponse\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tblockhash\x18\x02 \x01(\t\x12\x1f\n\x17last_valid_block_height\x18\x03 \x01(\x04\"X\n\x15GetBlockHeightRequest\x12\x30\n\ncommitment\x18\x01 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\".\n\x16GetBlockHeightResponse\x12\x14\n\x0c\x62lock_height\x18\x01 \x01(\x04\"Q\n\x0eGetSlotRequest\x12\x30\n\ncommitment\x18\x01 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\"\x1f\n\x0fGetSlotResponse\x12\x0c\n\x04slot\x18\x01 \x01(\x04\"\x13\n\x11GetVersionRequest\"%\n\x12GetVersionResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\"m\n\x17IsBlockhashValidRequest\x12\x11\n\tblockhash\x18\x01 \x01(\t\x12\x30\n\ncommitment\x18\x02 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\"7\n\x18IsBlockhashValidResponse\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\r\n\x05valid\x18\x02 \x01(\x08*\x83\x01\n\x0f\x43ommitmentLevel\x12\r\n\tPROCESSED\x10\x00\x12\r\n\tCONFIRMED\x10\x01\x12\r\n\tFINALIZED\x10\x02\x12\x18\n\x14\x46IRST_SHRED_RECEIVED\x10\x03\x12\r\n\tCOMPLETED\x10\x04\x12\x10\n\x0c\x43REATED_BANK\x10\x05\x12\x08\n\x04\x44\x45\x41\x44\x10\x06\x32\x93\x04\n\x06Geyser\x12\x44\n\tSubscribe\x12\x18.geyser.SubscribeRequest\x1a\x17.geyser.SubscribeUpdate\"\x00(\x01\x30\x01\x12\x33\n\x04Ping\x12\x13.geyser.PingRequest\x1a\x14.geyser.PongResponse\"\x00\x12]\n\x12GetLatestBlockhash\x12!.geyser.GetLatestBlockhashRequest\x1a\".geyser.GetLatestBlockhashResponse\"\x00\x12Q\n\x0eGetBlockHeight\x12\x1d.geyser.GetBlockHeightRequest\x1a\x1e.geyser.GetBlockHeightResponse\"\x00\x12<\n\x07GetSlot\x12\x16.geyser.GetSlotRequest\x1a\x17.geyser.GetSlotResponse\"\x00\x12W\n\x10IsBlockhashValid\x12\x1f.geyser.IsBlockhashValidRequest\x1a .geyser.IsBlockhashValidResponse\"\x00\x12\x45\n\nGetVersion\x12\x19.geyser.GetVersionRequest\x1a\x1a.geyser.GetVersionResponse\"\x00\x42;Z9github.com/rpcpool/yellowstone-grpc/examples/golang/protoP\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'geyser_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z9github.com/rpcpool/yellowstone-grpc/examples/golang/proto' + _globals['_SUBSCRIBEREQUEST_ACCOUNTSENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_ACCOUNTSENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_SLOTSENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_SLOTSENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_BLOCKSENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_BLOCKSENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_BLOCKSMETAENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_BLOCKSMETAENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_ENTRYENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_ENTRYENTRY']._serialized_options = b'8\001' + _globals['_COMMITMENTLEVEL']._serialized_start=6020 + _globals['_COMMITMENTLEVEL']._serialized_end=6151 + _globals['_SUBSCRIBEREQUEST']._serialized_start=47 + _globals['_SUBSCRIBEREQUEST']._serialized_end=1317 + _globals['_SUBSCRIBEREQUEST_ACCOUNTSENTRY']._serialized_start=662 + _globals['_SUBSCRIBEREQUEST_ACCOUNTSENTRY']._serialized_end=749 + _globals['_SUBSCRIBEREQUEST_SLOTSENTRY']._serialized_start=751 + _globals['_SUBSCRIBEREQUEST_SLOTSENTRY']._serialized_end=832 + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSENTRY']._serialized_start=834 + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSENTRY']._serialized_end=929 + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY']._serialized_start=931 + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY']._serialized_end=1032 + _globals['_SUBSCRIBEREQUEST_BLOCKSENTRY']._serialized_start=1034 + _globals['_SUBSCRIBEREQUEST_BLOCKSENTRY']._serialized_end=1117 + _globals['_SUBSCRIBEREQUEST_BLOCKSMETAENTRY']._serialized_start=1119 + _globals['_SUBSCRIBEREQUEST_BLOCKSMETAENTRY']._serialized_end=1210 + _globals['_SUBSCRIBEREQUEST_ENTRYENTRY']._serialized_start=1212 + _globals['_SUBSCRIBEREQUEST_ENTRYENTRY']._serialized_end=1293 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTS']._serialized_start=1320 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTS']._serialized_end=1511 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTER']._serialized_start=1514 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTER']._serialized_end=1757 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERMEMCMP']._serialized_start=1759 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERMEMCMP']._serialized_end=1880 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERLAMPORTS']._serialized_start=1882 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERLAMPORTS']._serialized_end=1991 + _globals['_SUBSCRIBEREQUESTFILTERSLOTS']._serialized_start=1993 + _globals['_SUBSCRIBEREQUESTFILTERSLOTS']._serialized_end=2082 + _globals['_SUBSCRIBEREQUESTFILTERTRANSACTIONS']._serialized_start=2085 + _globals['_SUBSCRIBEREQUESTFILTERTRANSACTIONS']._serialized_end=2295 + _globals['_SUBSCRIBEREQUESTFILTERBLOCKS']._serialized_start=2298 + _globals['_SUBSCRIBEREQUESTFILTERBLOCKS']._serialized_end=2515 + _globals['_SUBSCRIBEREQUESTFILTERBLOCKSMETA']._serialized_start=2517 + _globals['_SUBSCRIBEREQUESTFILTERBLOCKSMETA']._serialized_end=2551 + _globals['_SUBSCRIBEREQUESTFILTERENTRY']._serialized_start=2553 + _globals['_SUBSCRIBEREQUESTFILTERENTRY']._serialized_end=2582 + _globals['_SUBSCRIBEREQUESTACCOUNTSDATASLICE']._serialized_start=2584 + _globals['_SUBSCRIBEREQUESTACCOUNTSDATASLICE']._serialized_end=2651 + _globals['_SUBSCRIBEREQUESTPING']._serialized_start=2653 + _globals['_SUBSCRIBEREQUESTPING']._serialized_end=2687 + _globals['_SUBSCRIBEUPDATE']._serialized_start=2690 + _globals['_SUBSCRIBEUPDATE']._serialized_end=3207 + _globals['_SUBSCRIBEUPDATEACCOUNT']._serialized_start=3209 + _globals['_SUBSCRIBEUPDATEACCOUNT']._serialized_end=3320 + _globals['_SUBSCRIBEUPDATEACCOUNTINFO']._serialized_start=3323 + _globals['_SUBSCRIBEUPDATEACCOUNTINFO']._serialized_end=3523 + _globals['_SUBSCRIBEUPDATESLOT']._serialized_start=3526 + _globals['_SUBSCRIBEUPDATESLOT']._serialized_end=3674 + _globals['_SUBSCRIBEUPDATETRANSACTION']._serialized_start=3676 + _globals['_SUBSCRIBEUPDATETRANSACTION']._serialized_end=3779 + _globals['_SUBSCRIBEUPDATETRANSACTIONINFO']._serialized_start=3782 + _globals['_SUBSCRIBEUPDATETRANSACTIONINFO']._serialized_end=3998 + _globals['_SUBSCRIBEUPDATETRANSACTIONSTATUS']._serialized_start=4001 + _globals['_SUBSCRIBEUPDATETRANSACTIONSTATUS']._serialized_end=4162 + _globals['_SUBSCRIBEUPDATEBLOCK']._serialized_start=4165 + _globals['_SUBSCRIBEUPDATEBLOCK']._serialized_end=4709 + _globals['_SUBSCRIBEUPDATEBLOCKMETA']._serialized_start=4712 + _globals['_SUBSCRIBEUPDATEBLOCKMETA']._serialized_end=5066 + _globals['_SUBSCRIBEUPDATEENTRY']._serialized_start=5069 + _globals['_SUBSCRIBEUPDATEENTRY']._serialized_end=5226 + _globals['_SUBSCRIBEUPDATEPING']._serialized_start=5228 + _globals['_SUBSCRIBEUPDATEPING']._serialized_end=5249 + _globals['_SUBSCRIBEUPDATEPONG']._serialized_start=5251 + _globals['_SUBSCRIBEUPDATEPONG']._serialized_end=5284 + _globals['_PINGREQUEST']._serialized_start=5286 + _globals['_PINGREQUEST']._serialized_end=5314 + _globals['_PONGRESPONSE']._serialized_start=5316 + _globals['_PONGRESPONSE']._serialized_end=5345 + _globals['_GETLATESTBLOCKHASHREQUEST']._serialized_start=5347 + _globals['_GETLATESTBLOCKHASHREQUEST']._serialized_end=5439 + _globals['_GETLATESTBLOCKHASHRESPONSE']._serialized_start=5441 + _globals['_GETLATESTBLOCKHASHRESPONSE']._serialized_end=5535 + _globals['_GETBLOCKHEIGHTREQUEST']._serialized_start=5537 + _globals['_GETBLOCKHEIGHTREQUEST']._serialized_end=5625 + _globals['_GETBLOCKHEIGHTRESPONSE']._serialized_start=5627 + _globals['_GETBLOCKHEIGHTRESPONSE']._serialized_end=5673 + _globals['_GETSLOTREQUEST']._serialized_start=5675 + _globals['_GETSLOTREQUEST']._serialized_end=5756 + _globals['_GETSLOTRESPONSE']._serialized_start=5758 + _globals['_GETSLOTRESPONSE']._serialized_end=5789 + _globals['_GETVERSIONREQUEST']._serialized_start=5791 + _globals['_GETVERSIONREQUEST']._serialized_end=5810 + _globals['_GETVERSIONRESPONSE']._serialized_start=5812 + _globals['_GETVERSIONRESPONSE']._serialized_end=5849 + _globals['_ISBLOCKHASHVALIDREQUEST']._serialized_start=5851 + _globals['_ISBLOCKHASHVALIDREQUEST']._serialized_end=5960 + _globals['_ISBLOCKHASHVALIDRESPONSE']._serialized_start=5962 + _globals['_ISBLOCKHASHVALIDRESPONSE']._serialized_end=6017 + _globals['_GEYSER']._serialized_start=6154 + _globals['_GEYSER']._serialized_end=6685 +# @@protoc_insertion_point(module_scope) diff --git a/learning-examples/generated/geyser_pb2.pyi b/learning-examples/generated/geyser_pb2.pyi new file mode 100644 index 0000000..909f4fb --- /dev/null +++ b/learning-examples/generated/geyser_pb2.pyi @@ -0,0 +1,485 @@ +import solana_storage_pb2 as _solana_storage_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union +from solana_storage_pb2 import ConfirmedBlock as ConfirmedBlock +from solana_storage_pb2 import ConfirmedTransaction as ConfirmedTransaction +from solana_storage_pb2 import Transaction as Transaction +from solana_storage_pb2 import Message as Message +from solana_storage_pb2 import MessageHeader as MessageHeader +from solana_storage_pb2 import MessageAddressTableLookup as MessageAddressTableLookup +from solana_storage_pb2 import TransactionStatusMeta as TransactionStatusMeta +from solana_storage_pb2 import TransactionError as TransactionError +from solana_storage_pb2 import InnerInstructions as InnerInstructions +from solana_storage_pb2 import InnerInstruction as InnerInstruction +from solana_storage_pb2 import CompiledInstruction as CompiledInstruction +from solana_storage_pb2 import TokenBalance as TokenBalance +from solana_storage_pb2 import UiTokenAmount as UiTokenAmount +from solana_storage_pb2 import ReturnData as ReturnData +from solana_storage_pb2 import Reward as Reward +from solana_storage_pb2 import Rewards as Rewards +from solana_storage_pb2 import UnixTimestamp as UnixTimestamp +from solana_storage_pb2 import BlockHeight as BlockHeight +from solana_storage_pb2 import NumPartitions as NumPartitions +from solana_storage_pb2 import RewardType as RewardType + +DESCRIPTOR: _descriptor.FileDescriptor +Unspecified: _solana_storage_pb2.RewardType +Fee: _solana_storage_pb2.RewardType +Rent: _solana_storage_pb2.RewardType +Staking: _solana_storage_pb2.RewardType +Voting: _solana_storage_pb2.RewardType + +class CommitmentLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + PROCESSED: _ClassVar[CommitmentLevel] + CONFIRMED: _ClassVar[CommitmentLevel] + FINALIZED: _ClassVar[CommitmentLevel] + FIRST_SHRED_RECEIVED: _ClassVar[CommitmentLevel] + COMPLETED: _ClassVar[CommitmentLevel] + CREATED_BANK: _ClassVar[CommitmentLevel] + DEAD: _ClassVar[CommitmentLevel] +PROCESSED: CommitmentLevel +CONFIRMED: CommitmentLevel +FINALIZED: CommitmentLevel +FIRST_SHRED_RECEIVED: CommitmentLevel +COMPLETED: CommitmentLevel +CREATED_BANK: CommitmentLevel +DEAD: CommitmentLevel + +class SubscribeRequest(_message.Message): + __slots__ = ("accounts", "slots", "transactions", "transactions_status", "blocks", "blocks_meta", "entry", "commitment", "accounts_data_slice", "ping") + class AccountsEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterAccounts + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterAccounts, _Mapping]] = ...) -> None: ... + class SlotsEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterSlots + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterSlots, _Mapping]] = ...) -> None: ... + class TransactionsEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterTransactions + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterTransactions, _Mapping]] = ...) -> None: ... + class TransactionsStatusEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterTransactions + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterTransactions, _Mapping]] = ...) -> None: ... + class BlocksEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterBlocks + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterBlocks, _Mapping]] = ...) -> None: ... + class BlocksMetaEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterBlocksMeta + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterBlocksMeta, _Mapping]] = ...) -> None: ... + class EntryEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterEntry + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterEntry, _Mapping]] = ...) -> None: ... + ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + SLOTS_FIELD_NUMBER: _ClassVar[int] + TRANSACTIONS_FIELD_NUMBER: _ClassVar[int] + TRANSACTIONS_STATUS_FIELD_NUMBER: _ClassVar[int] + BLOCKS_FIELD_NUMBER: _ClassVar[int] + BLOCKS_META_FIELD_NUMBER: _ClassVar[int] + ENTRY_FIELD_NUMBER: _ClassVar[int] + COMMITMENT_FIELD_NUMBER: _ClassVar[int] + ACCOUNTS_DATA_SLICE_FIELD_NUMBER: _ClassVar[int] + PING_FIELD_NUMBER: _ClassVar[int] + accounts: _containers.MessageMap[str, SubscribeRequestFilterAccounts] + slots: _containers.MessageMap[str, SubscribeRequestFilterSlots] + transactions: _containers.MessageMap[str, SubscribeRequestFilterTransactions] + transactions_status: _containers.MessageMap[str, SubscribeRequestFilterTransactions] + blocks: _containers.MessageMap[str, SubscribeRequestFilterBlocks] + blocks_meta: _containers.MessageMap[str, SubscribeRequestFilterBlocksMeta] + entry: _containers.MessageMap[str, SubscribeRequestFilterEntry] + commitment: CommitmentLevel + accounts_data_slice: _containers.RepeatedCompositeFieldContainer[SubscribeRequestAccountsDataSlice] + ping: SubscribeRequestPing + def __init__(self, accounts: _Optional[_Mapping[str, SubscribeRequestFilterAccounts]] = ..., slots: _Optional[_Mapping[str, SubscribeRequestFilterSlots]] = ..., transactions: _Optional[_Mapping[str, SubscribeRequestFilterTransactions]] = ..., transactions_status: _Optional[_Mapping[str, SubscribeRequestFilterTransactions]] = ..., blocks: _Optional[_Mapping[str, SubscribeRequestFilterBlocks]] = ..., blocks_meta: _Optional[_Mapping[str, SubscribeRequestFilterBlocksMeta]] = ..., entry: _Optional[_Mapping[str, SubscribeRequestFilterEntry]] = ..., commitment: _Optional[_Union[CommitmentLevel, str]] = ..., accounts_data_slice: _Optional[_Iterable[_Union[SubscribeRequestAccountsDataSlice, _Mapping]]] = ..., ping: _Optional[_Union[SubscribeRequestPing, _Mapping]] = ...) -> None: ... + +class SubscribeRequestFilterAccounts(_message.Message): + __slots__ = ("account", "owner", "filters", "nonempty_txn_signature") + ACCOUNT_FIELD_NUMBER: _ClassVar[int] + OWNER_FIELD_NUMBER: _ClassVar[int] + FILTERS_FIELD_NUMBER: _ClassVar[int] + NONEMPTY_TXN_SIGNATURE_FIELD_NUMBER: _ClassVar[int] + account: _containers.RepeatedScalarFieldContainer[str] + owner: _containers.RepeatedScalarFieldContainer[str] + filters: _containers.RepeatedCompositeFieldContainer[SubscribeRequestFilterAccountsFilter] + nonempty_txn_signature: bool + def __init__(self, account: _Optional[_Iterable[str]] = ..., owner: _Optional[_Iterable[str]] = ..., filters: _Optional[_Iterable[_Union[SubscribeRequestFilterAccountsFilter, _Mapping]]] = ..., nonempty_txn_signature: bool = ...) -> None: ... + +class SubscribeRequestFilterAccountsFilter(_message.Message): + __slots__ = ("memcmp", "datasize", "token_account_state", "lamports") + MEMCMP_FIELD_NUMBER: _ClassVar[int] + DATASIZE_FIELD_NUMBER: _ClassVar[int] + TOKEN_ACCOUNT_STATE_FIELD_NUMBER: _ClassVar[int] + LAMPORTS_FIELD_NUMBER: _ClassVar[int] + memcmp: SubscribeRequestFilterAccountsFilterMemcmp + datasize: int + token_account_state: bool + lamports: SubscribeRequestFilterAccountsFilterLamports + def __init__(self, memcmp: _Optional[_Union[SubscribeRequestFilterAccountsFilterMemcmp, _Mapping]] = ..., datasize: _Optional[int] = ..., token_account_state: bool = ..., lamports: _Optional[_Union[SubscribeRequestFilterAccountsFilterLamports, _Mapping]] = ...) -> None: ... + +class SubscribeRequestFilterAccountsFilterMemcmp(_message.Message): + __slots__ = ("offset", "bytes", "base58", "base64") + OFFSET_FIELD_NUMBER: _ClassVar[int] + BYTES_FIELD_NUMBER: _ClassVar[int] + BASE58_FIELD_NUMBER: _ClassVar[int] + BASE64_FIELD_NUMBER: _ClassVar[int] + offset: int + bytes: bytes + base58: str + base64: str + def __init__(self, offset: _Optional[int] = ..., bytes: _Optional[bytes] = ..., base58: _Optional[str] = ..., base64: _Optional[str] = ...) -> None: ... + +class SubscribeRequestFilterAccountsFilterLamports(_message.Message): + __slots__ = ("eq", "ne", "lt", "gt") + EQ_FIELD_NUMBER: _ClassVar[int] + NE_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + eq: int + ne: int + lt: int + gt: int + def __init__(self, eq: _Optional[int] = ..., ne: _Optional[int] = ..., lt: _Optional[int] = ..., gt: _Optional[int] = ...) -> None: ... + +class SubscribeRequestFilterSlots(_message.Message): + __slots__ = ("filter_by_commitment",) + FILTER_BY_COMMITMENT_FIELD_NUMBER: _ClassVar[int] + filter_by_commitment: bool + def __init__(self, filter_by_commitment: bool = ...) -> None: ... + +class SubscribeRequestFilterTransactions(_message.Message): + __slots__ = ("vote", "failed", "signature", "account_include", "account_exclude", "account_required") + VOTE_FIELD_NUMBER: _ClassVar[int] + FAILED_FIELD_NUMBER: _ClassVar[int] + SIGNATURE_FIELD_NUMBER: _ClassVar[int] + ACCOUNT_INCLUDE_FIELD_NUMBER: _ClassVar[int] + ACCOUNT_EXCLUDE_FIELD_NUMBER: _ClassVar[int] + ACCOUNT_REQUIRED_FIELD_NUMBER: _ClassVar[int] + vote: bool + failed: bool + signature: str + account_include: _containers.RepeatedScalarFieldContainer[str] + account_exclude: _containers.RepeatedScalarFieldContainer[str] + account_required: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, vote: bool = ..., failed: bool = ..., signature: _Optional[str] = ..., account_include: _Optional[_Iterable[str]] = ..., account_exclude: _Optional[_Iterable[str]] = ..., account_required: _Optional[_Iterable[str]] = ...) -> None: ... + +class SubscribeRequestFilterBlocks(_message.Message): + __slots__ = ("account_include", "include_transactions", "include_accounts", "include_entries") + ACCOUNT_INCLUDE_FIELD_NUMBER: _ClassVar[int] + INCLUDE_TRANSACTIONS_FIELD_NUMBER: _ClassVar[int] + INCLUDE_ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + INCLUDE_ENTRIES_FIELD_NUMBER: _ClassVar[int] + account_include: _containers.RepeatedScalarFieldContainer[str] + include_transactions: bool + include_accounts: bool + include_entries: bool + def __init__(self, account_include: _Optional[_Iterable[str]] = ..., include_transactions: bool = ..., include_accounts: bool = ..., include_entries: bool = ...) -> None: ... + +class SubscribeRequestFilterBlocksMeta(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class SubscribeRequestFilterEntry(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class SubscribeRequestAccountsDataSlice(_message.Message): + __slots__ = ("offset", "length") + OFFSET_FIELD_NUMBER: _ClassVar[int] + LENGTH_FIELD_NUMBER: _ClassVar[int] + offset: int + length: int + def __init__(self, offset: _Optional[int] = ..., length: _Optional[int] = ...) -> None: ... + +class SubscribeRequestPing(_message.Message): + __slots__ = ("id",) + ID_FIELD_NUMBER: _ClassVar[int] + id: int + def __init__(self, id: _Optional[int] = ...) -> None: ... + +class SubscribeUpdate(_message.Message): + __slots__ = ("filters", "account", "slot", "transaction", "transaction_status", "block", "ping", "pong", "block_meta", "entry") + FILTERS_FIELD_NUMBER: _ClassVar[int] + ACCOUNT_FIELD_NUMBER: _ClassVar[int] + SLOT_FIELD_NUMBER: _ClassVar[int] + TRANSACTION_FIELD_NUMBER: _ClassVar[int] + TRANSACTION_STATUS_FIELD_NUMBER: _ClassVar[int] + BLOCK_FIELD_NUMBER: _ClassVar[int] + PING_FIELD_NUMBER: _ClassVar[int] + PONG_FIELD_NUMBER: _ClassVar[int] + BLOCK_META_FIELD_NUMBER: _ClassVar[int] + ENTRY_FIELD_NUMBER: _ClassVar[int] + filters: _containers.RepeatedScalarFieldContainer[str] + account: SubscribeUpdateAccount + slot: SubscribeUpdateSlot + transaction: SubscribeUpdateTransaction + transaction_status: SubscribeUpdateTransactionStatus + block: SubscribeUpdateBlock + ping: SubscribeUpdatePing + pong: SubscribeUpdatePong + block_meta: SubscribeUpdateBlockMeta + entry: SubscribeUpdateEntry + def __init__(self, filters: _Optional[_Iterable[str]] = ..., account: _Optional[_Union[SubscribeUpdateAccount, _Mapping]] = ..., slot: _Optional[_Union[SubscribeUpdateSlot, _Mapping]] = ..., transaction: _Optional[_Union[SubscribeUpdateTransaction, _Mapping]] = ..., transaction_status: _Optional[_Union[SubscribeUpdateTransactionStatus, _Mapping]] = ..., block: _Optional[_Union[SubscribeUpdateBlock, _Mapping]] = ..., ping: _Optional[_Union[SubscribeUpdatePing, _Mapping]] = ..., pong: _Optional[_Union[SubscribeUpdatePong, _Mapping]] = ..., block_meta: _Optional[_Union[SubscribeUpdateBlockMeta, _Mapping]] = ..., entry: _Optional[_Union[SubscribeUpdateEntry, _Mapping]] = ...) -> None: ... + +class SubscribeUpdateAccount(_message.Message): + __slots__ = ("account", "slot", "is_startup") + ACCOUNT_FIELD_NUMBER: _ClassVar[int] + SLOT_FIELD_NUMBER: _ClassVar[int] + IS_STARTUP_FIELD_NUMBER: _ClassVar[int] + account: SubscribeUpdateAccountInfo + slot: int + is_startup: bool + def __init__(self, account: _Optional[_Union[SubscribeUpdateAccountInfo, _Mapping]] = ..., slot: _Optional[int] = ..., is_startup: bool = ...) -> None: ... + +class SubscribeUpdateAccountInfo(_message.Message): + __slots__ = ("pubkey", "lamports", "owner", "executable", "rent_epoch", "data", "write_version", "txn_signature") + PUBKEY_FIELD_NUMBER: _ClassVar[int] + LAMPORTS_FIELD_NUMBER: _ClassVar[int] + OWNER_FIELD_NUMBER: _ClassVar[int] + EXECUTABLE_FIELD_NUMBER: _ClassVar[int] + RENT_EPOCH_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + WRITE_VERSION_FIELD_NUMBER: _ClassVar[int] + TXN_SIGNATURE_FIELD_NUMBER: _ClassVar[int] + pubkey: bytes + lamports: int + owner: bytes + executable: bool + rent_epoch: int + data: bytes + write_version: int + txn_signature: bytes + def __init__(self, pubkey: _Optional[bytes] = ..., lamports: _Optional[int] = ..., owner: _Optional[bytes] = ..., executable: bool = ..., rent_epoch: _Optional[int] = ..., data: _Optional[bytes] = ..., write_version: _Optional[int] = ..., txn_signature: _Optional[bytes] = ...) -> None: ... + +class SubscribeUpdateSlot(_message.Message): + __slots__ = ("slot", "parent", "status", "dead_error") + SLOT_FIELD_NUMBER: _ClassVar[int] + PARENT_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + DEAD_ERROR_FIELD_NUMBER: _ClassVar[int] + slot: int + parent: int + status: CommitmentLevel + dead_error: str + def __init__(self, slot: _Optional[int] = ..., parent: _Optional[int] = ..., status: _Optional[_Union[CommitmentLevel, str]] = ..., dead_error: _Optional[str] = ...) -> None: ... + +class SubscribeUpdateTransaction(_message.Message): + __slots__ = ("transaction", "slot") + TRANSACTION_FIELD_NUMBER: _ClassVar[int] + SLOT_FIELD_NUMBER: _ClassVar[int] + transaction: SubscribeUpdateTransactionInfo + slot: int + def __init__(self, transaction: _Optional[_Union[SubscribeUpdateTransactionInfo, _Mapping]] = ..., slot: _Optional[int] = ...) -> None: ... + +class SubscribeUpdateTransactionInfo(_message.Message): + __slots__ = ("signature", "is_vote", "transaction", "meta", "index") + SIGNATURE_FIELD_NUMBER: _ClassVar[int] + IS_VOTE_FIELD_NUMBER: _ClassVar[int] + TRANSACTION_FIELD_NUMBER: _ClassVar[int] + META_FIELD_NUMBER: _ClassVar[int] + INDEX_FIELD_NUMBER: _ClassVar[int] + signature: bytes + is_vote: bool + transaction: _solana_storage_pb2.Transaction + meta: _solana_storage_pb2.TransactionStatusMeta + index: int + def __init__(self, signature: _Optional[bytes] = ..., is_vote: bool = ..., transaction: _Optional[_Union[_solana_storage_pb2.Transaction, _Mapping]] = ..., meta: _Optional[_Union[_solana_storage_pb2.TransactionStatusMeta, _Mapping]] = ..., index: _Optional[int] = ...) -> None: ... + +class SubscribeUpdateTransactionStatus(_message.Message): + __slots__ = ("slot", "signature", "is_vote", "index", "err") + SLOT_FIELD_NUMBER: _ClassVar[int] + SIGNATURE_FIELD_NUMBER: _ClassVar[int] + IS_VOTE_FIELD_NUMBER: _ClassVar[int] + INDEX_FIELD_NUMBER: _ClassVar[int] + ERR_FIELD_NUMBER: _ClassVar[int] + slot: int + signature: bytes + is_vote: bool + index: int + err: _solana_storage_pb2.TransactionError + def __init__(self, slot: _Optional[int] = ..., signature: _Optional[bytes] = ..., is_vote: bool = ..., index: _Optional[int] = ..., err: _Optional[_Union[_solana_storage_pb2.TransactionError, _Mapping]] = ...) -> None: ... + +class SubscribeUpdateBlock(_message.Message): + __slots__ = ("slot", "blockhash", "rewards", "block_time", "block_height", "parent_slot", "parent_blockhash", "executed_transaction_count", "transactions", "updated_account_count", "accounts", "entries_count", "entries") + SLOT_FIELD_NUMBER: _ClassVar[int] + BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + REWARDS_FIELD_NUMBER: _ClassVar[int] + BLOCK_TIME_FIELD_NUMBER: _ClassVar[int] + BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + PARENT_SLOT_FIELD_NUMBER: _ClassVar[int] + PARENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int] + TRANSACTIONS_FIELD_NUMBER: _ClassVar[int] + UPDATED_ACCOUNT_COUNT_FIELD_NUMBER: _ClassVar[int] + ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + ENTRIES_COUNT_FIELD_NUMBER: _ClassVar[int] + ENTRIES_FIELD_NUMBER: _ClassVar[int] + slot: int + blockhash: str + rewards: _solana_storage_pb2.Rewards + block_time: _solana_storage_pb2.UnixTimestamp + block_height: _solana_storage_pb2.BlockHeight + parent_slot: int + parent_blockhash: str + executed_transaction_count: int + transactions: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateTransactionInfo] + updated_account_count: int + accounts: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateAccountInfo] + entries_count: int + entries: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateEntry] + def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ..., block_time: _Optional[_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[_solana_storage_pb2.BlockHeight, _Mapping]] = ..., parent_slot: _Optional[int] = ..., parent_blockhash: _Optional[str] = ..., executed_transaction_count: _Optional[int] = ..., transactions: _Optional[_Iterable[_Union[SubscribeUpdateTransactionInfo, _Mapping]]] = ..., updated_account_count: _Optional[int] = ..., accounts: _Optional[_Iterable[_Union[SubscribeUpdateAccountInfo, _Mapping]]] = ..., entries_count: _Optional[int] = ..., entries: _Optional[_Iterable[_Union[SubscribeUpdateEntry, _Mapping]]] = ...) -> None: ... + +class SubscribeUpdateBlockMeta(_message.Message): + __slots__ = ("slot", "blockhash", "rewards", "block_time", "block_height", "parent_slot", "parent_blockhash", "executed_transaction_count", "entries_count") + SLOT_FIELD_NUMBER: _ClassVar[int] + BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + REWARDS_FIELD_NUMBER: _ClassVar[int] + BLOCK_TIME_FIELD_NUMBER: _ClassVar[int] + BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + PARENT_SLOT_FIELD_NUMBER: _ClassVar[int] + PARENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int] + ENTRIES_COUNT_FIELD_NUMBER: _ClassVar[int] + slot: int + blockhash: str + rewards: _solana_storage_pb2.Rewards + block_time: _solana_storage_pb2.UnixTimestamp + block_height: _solana_storage_pb2.BlockHeight + parent_slot: int + parent_blockhash: str + executed_transaction_count: int + entries_count: int + def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ..., block_time: _Optional[_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[_solana_storage_pb2.BlockHeight, _Mapping]] = ..., parent_slot: _Optional[int] = ..., parent_blockhash: _Optional[str] = ..., executed_transaction_count: _Optional[int] = ..., entries_count: _Optional[int] = ...) -> None: ... + +class SubscribeUpdateEntry(_message.Message): + __slots__ = ("slot", "index", "num_hashes", "hash", "executed_transaction_count", "starting_transaction_index") + SLOT_FIELD_NUMBER: _ClassVar[int] + INDEX_FIELD_NUMBER: _ClassVar[int] + NUM_HASHES_FIELD_NUMBER: _ClassVar[int] + HASH_FIELD_NUMBER: _ClassVar[int] + EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int] + STARTING_TRANSACTION_INDEX_FIELD_NUMBER: _ClassVar[int] + slot: int + index: int + num_hashes: int + hash: bytes + executed_transaction_count: int + starting_transaction_index: int + def __init__(self, slot: _Optional[int] = ..., index: _Optional[int] = ..., num_hashes: _Optional[int] = ..., hash: _Optional[bytes] = ..., executed_transaction_count: _Optional[int] = ..., starting_transaction_index: _Optional[int] = ...) -> None: ... + +class SubscribeUpdatePing(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class SubscribeUpdatePong(_message.Message): + __slots__ = ("id",) + ID_FIELD_NUMBER: _ClassVar[int] + id: int + def __init__(self, id: _Optional[int] = ...) -> None: ... + +class PingRequest(_message.Message): + __slots__ = ("count",) + COUNT_FIELD_NUMBER: _ClassVar[int] + count: int + def __init__(self, count: _Optional[int] = ...) -> None: ... + +class PongResponse(_message.Message): + __slots__ = ("count",) + COUNT_FIELD_NUMBER: _ClassVar[int] + count: int + def __init__(self, count: _Optional[int] = ...) -> None: ... + +class GetLatestBlockhashRequest(_message.Message): + __slots__ = ("commitment",) + COMMITMENT_FIELD_NUMBER: _ClassVar[int] + commitment: CommitmentLevel + def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ... + +class GetLatestBlockhashResponse(_message.Message): + __slots__ = ("slot", "blockhash", "last_valid_block_height") + SLOT_FIELD_NUMBER: _ClassVar[int] + BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + LAST_VALID_BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + slot: int + blockhash: str + last_valid_block_height: int + def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., last_valid_block_height: _Optional[int] = ...) -> None: ... + +class GetBlockHeightRequest(_message.Message): + __slots__ = ("commitment",) + COMMITMENT_FIELD_NUMBER: _ClassVar[int] + commitment: CommitmentLevel + def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ... + +class GetBlockHeightResponse(_message.Message): + __slots__ = ("block_height",) + BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + block_height: int + def __init__(self, block_height: _Optional[int] = ...) -> None: ... + +class GetSlotRequest(_message.Message): + __slots__ = ("commitment",) + COMMITMENT_FIELD_NUMBER: _ClassVar[int] + commitment: CommitmentLevel + def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ... + +class GetSlotResponse(_message.Message): + __slots__ = ("slot",) + SLOT_FIELD_NUMBER: _ClassVar[int] + slot: int + def __init__(self, slot: _Optional[int] = ...) -> None: ... + +class GetVersionRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class GetVersionResponse(_message.Message): + __slots__ = ("version",) + VERSION_FIELD_NUMBER: _ClassVar[int] + version: str + def __init__(self, version: _Optional[str] = ...) -> None: ... + +class IsBlockhashValidRequest(_message.Message): + __slots__ = ("blockhash", "commitment") + BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + COMMITMENT_FIELD_NUMBER: _ClassVar[int] + blockhash: str + commitment: CommitmentLevel + def __init__(self, blockhash: _Optional[str] = ..., commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ... + +class IsBlockhashValidResponse(_message.Message): + __slots__ = ("slot", "valid") + SLOT_FIELD_NUMBER: _ClassVar[int] + VALID_FIELD_NUMBER: _ClassVar[int] + slot: int + valid: bool + def __init__(self, slot: _Optional[int] = ..., valid: bool = ...) -> None: ... diff --git a/learning-examples/generated/geyser_pb2_grpc.py b/learning-examples/generated/geyser_pb2_grpc.py new file mode 100644 index 0000000..f56411e --- /dev/null +++ b/learning-examples/generated/geyser_pb2_grpc.py @@ -0,0 +1,355 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" + +import grpc + +import generated.geyser_pb2 as geyser__pb2 + +GRPC_GENERATED_VERSION = '1.71.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in geyser_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class GeyserStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Subscribe = channel.stream_stream( + '/geyser.Geyser/Subscribe', + request_serializer=geyser__pb2.SubscribeRequest.SerializeToString, + response_deserializer=geyser__pb2.SubscribeUpdate.FromString, + _registered_method=True) + self.Ping = channel.unary_unary( + '/geyser.Geyser/Ping', + request_serializer=geyser__pb2.PingRequest.SerializeToString, + response_deserializer=geyser__pb2.PongResponse.FromString, + _registered_method=True) + self.GetLatestBlockhash = channel.unary_unary( + '/geyser.Geyser/GetLatestBlockhash', + request_serializer=geyser__pb2.GetLatestBlockhashRequest.SerializeToString, + response_deserializer=geyser__pb2.GetLatestBlockhashResponse.FromString, + _registered_method=True) + self.GetBlockHeight = channel.unary_unary( + '/geyser.Geyser/GetBlockHeight', + request_serializer=geyser__pb2.GetBlockHeightRequest.SerializeToString, + response_deserializer=geyser__pb2.GetBlockHeightResponse.FromString, + _registered_method=True) + self.GetSlot = channel.unary_unary( + '/geyser.Geyser/GetSlot', + request_serializer=geyser__pb2.GetSlotRequest.SerializeToString, + response_deserializer=geyser__pb2.GetSlotResponse.FromString, + _registered_method=True) + self.IsBlockhashValid = channel.unary_unary( + '/geyser.Geyser/IsBlockhashValid', + request_serializer=geyser__pb2.IsBlockhashValidRequest.SerializeToString, + response_deserializer=geyser__pb2.IsBlockhashValidResponse.FromString, + _registered_method=True) + self.GetVersion = channel.unary_unary( + '/geyser.Geyser/GetVersion', + request_serializer=geyser__pb2.GetVersionRequest.SerializeToString, + response_deserializer=geyser__pb2.GetVersionResponse.FromString, + _registered_method=True) + + +class GeyserServicer: + """Missing associated documentation comment in .proto file.""" + + def Subscribe(self, request_iterator, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Ping(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetLatestBlockhash(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetBlockHeight(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetSlot(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def IsBlockhashValid(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetVersion(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_GeyserServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Subscribe': grpc.stream_stream_rpc_method_handler( + servicer.Subscribe, + request_deserializer=geyser__pb2.SubscribeRequest.FromString, + response_serializer=geyser__pb2.SubscribeUpdate.SerializeToString, + ), + 'Ping': grpc.unary_unary_rpc_method_handler( + servicer.Ping, + request_deserializer=geyser__pb2.PingRequest.FromString, + response_serializer=geyser__pb2.PongResponse.SerializeToString, + ), + 'GetLatestBlockhash': grpc.unary_unary_rpc_method_handler( + servicer.GetLatestBlockhash, + request_deserializer=geyser__pb2.GetLatestBlockhashRequest.FromString, + response_serializer=geyser__pb2.GetLatestBlockhashResponse.SerializeToString, + ), + 'GetBlockHeight': grpc.unary_unary_rpc_method_handler( + servicer.GetBlockHeight, + request_deserializer=geyser__pb2.GetBlockHeightRequest.FromString, + response_serializer=geyser__pb2.GetBlockHeightResponse.SerializeToString, + ), + 'GetSlot': grpc.unary_unary_rpc_method_handler( + servicer.GetSlot, + request_deserializer=geyser__pb2.GetSlotRequest.FromString, + response_serializer=geyser__pb2.GetSlotResponse.SerializeToString, + ), + 'IsBlockhashValid': grpc.unary_unary_rpc_method_handler( + servicer.IsBlockhashValid, + request_deserializer=geyser__pb2.IsBlockhashValidRequest.FromString, + response_serializer=geyser__pb2.IsBlockhashValidResponse.SerializeToString, + ), + 'GetVersion': grpc.unary_unary_rpc_method_handler( + servicer.GetVersion, + request_deserializer=geyser__pb2.GetVersionRequest.FromString, + response_serializer=geyser__pb2.GetVersionResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'geyser.Geyser', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('geyser.Geyser', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class Geyser: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def Subscribe(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream( + request_iterator, + target, + '/geyser.Geyser/Subscribe', + geyser__pb2.SubscribeRequest.SerializeToString, + geyser__pb2.SubscribeUpdate.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Ping(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/Ping', + geyser__pb2.PingRequest.SerializeToString, + geyser__pb2.PongResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetLatestBlockhash(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/GetLatestBlockhash', + geyser__pb2.GetLatestBlockhashRequest.SerializeToString, + geyser__pb2.GetLatestBlockhashResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetBlockHeight(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/GetBlockHeight', + geyser__pb2.GetBlockHeightRequest.SerializeToString, + geyser__pb2.GetBlockHeightResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetSlot(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/GetSlot', + geyser__pb2.GetSlotRequest.SerializeToString, + geyser__pb2.GetSlotResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def IsBlockhashValid(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/IsBlockhashValid', + geyser__pb2.IsBlockhashValidRequest.SerializeToString, + geyser__pb2.IsBlockhashValidResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetVersion(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/GetVersion', + geyser__pb2.GetVersionRequest.SerializeToString, + geyser__pb2.GetVersionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/learning-examples/generated/solana_storage_pb2.py b/learning-examples/generated/solana_storage_pb2.py new file mode 100644 index 0000000..7458363 --- /dev/null +++ b/learning-examples/generated/solana_storage_pb2.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: solana-storage.proto +# Protobuf Python Version: 5.29.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 0, + '', + 'solana-storage.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14solana-storage.proto\x12\x1dsolana.storage.ConfirmedBlock\"\xa1\x03\n\x0e\x43onfirmedBlock\x12\x1a\n\x12previous_blockhash\x18\x01 \x01(\t\x12\x11\n\tblockhash\x18\x02 \x01(\t\x12\x13\n\x0bparent_slot\x18\x03 \x01(\x04\x12I\n\x0ctransactions\x18\x04 \x03(\x0b\x32\x33.solana.storage.ConfirmedBlock.ConfirmedTransaction\x12\x36\n\x07rewards\x18\x05 \x03(\x0b\x32%.solana.storage.ConfirmedBlock.Reward\x12@\n\nblock_time\x18\x06 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.UnixTimestamp\x12@\n\x0c\x62lock_height\x18\x07 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.BlockHeight\x12\x44\n\x0enum_partitions\x18\x08 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.NumPartitions\"\x9b\x01\n\x14\x43onfirmedTransaction\x12?\n\x0btransaction\x18\x01 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.Transaction\x12\x42\n\x04meta\x18\x02 \x01(\x0b\x32\x34.solana.storage.ConfirmedBlock.TransactionStatusMeta\"Z\n\x0bTransaction\x12\x12\n\nsignatures\x18\x01 \x03(\x0c\x12\x37\n\x07message\x18\x02 \x01(\x0b\x32&.solana.storage.ConfirmedBlock.Message\"\xad\x02\n\x07Message\x12<\n\x06header\x18\x01 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.MessageHeader\x12\x14\n\x0c\x61\x63\x63ount_keys\x18\x02 \x03(\x0c\x12\x18\n\x10recent_blockhash\x18\x03 \x01(\x0c\x12H\n\x0cinstructions\x18\x04 \x03(\x0b\x32\x32.solana.storage.ConfirmedBlock.CompiledInstruction\x12\x11\n\tversioned\x18\x05 \x01(\x08\x12W\n\x15\x61\x64\x64ress_table_lookups\x18\x06 \x03(\x0b\x32\x38.solana.storage.ConfirmedBlock.MessageAddressTableLookup\"~\n\rMessageHeader\x12\x1f\n\x17num_required_signatures\x18\x01 \x01(\r\x12$\n\x1cnum_readonly_signed_accounts\x18\x02 \x01(\r\x12&\n\x1enum_readonly_unsigned_accounts\x18\x03 \x01(\r\"d\n\x19MessageAddressTableLookup\x12\x13\n\x0b\x61\x63\x63ount_key\x18\x01 \x01(\x0c\x12\x18\n\x10writable_indexes\x18\x02 \x01(\x0c\x12\x18\n\x10readonly_indexes\x18\x03 \x01(\x0c\"\xda\x05\n\x15TransactionStatusMeta\x12<\n\x03\x65rr\x18\x01 \x01(\x0b\x32/.solana.storage.ConfirmedBlock.TransactionError\x12\x0b\n\x03\x66\x65\x65\x18\x02 \x01(\x04\x12\x14\n\x0cpre_balances\x18\x03 \x03(\x04\x12\x15\n\rpost_balances\x18\x04 \x03(\x04\x12L\n\x12inner_instructions\x18\x05 \x03(\x0b\x32\x30.solana.storage.ConfirmedBlock.InnerInstructions\x12\x1f\n\x17inner_instructions_none\x18\n \x01(\x08\x12\x14\n\x0clog_messages\x18\x06 \x03(\t\x12\x19\n\x11log_messages_none\x18\x0b \x01(\x08\x12G\n\x12pre_token_balances\x18\x07 \x03(\x0b\x32+.solana.storage.ConfirmedBlock.TokenBalance\x12H\n\x13post_token_balances\x18\x08 \x03(\x0b\x32+.solana.storage.ConfirmedBlock.TokenBalance\x12\x36\n\x07rewards\x18\t \x03(\x0b\x32%.solana.storage.ConfirmedBlock.Reward\x12!\n\x19loaded_writable_addresses\x18\x0c \x03(\x0c\x12!\n\x19loaded_readonly_addresses\x18\r \x03(\x0c\x12>\n\x0breturn_data\x18\x0e \x01(\x0b\x32).solana.storage.ConfirmedBlock.ReturnData\x12\x18\n\x10return_data_none\x18\x0f \x01(\x08\x12#\n\x16\x63ompute_units_consumed\x18\x10 \x01(\x04H\x00\x88\x01\x01\x42\x19\n\x17_compute_units_consumed\"\x1f\n\x10TransactionError\x12\x0b\n\x03\x65rr\x18\x01 \x01(\x0c\"i\n\x11InnerInstructions\x12\r\n\x05index\x18\x01 \x01(\r\x12\x45\n\x0cinstructions\x18\x02 \x03(\x0b\x32/.solana.storage.ConfirmedBlock.InnerInstruction\"x\n\x10InnerInstruction\x12\x18\n\x10program_id_index\x18\x01 \x01(\r\x12\x10\n\x08\x61\x63\x63ounts\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x19\n\x0cstack_height\x18\x04 \x01(\rH\x00\x88\x01\x01\x42\x0f\n\r_stack_height\"O\n\x13\x43ompiledInstruction\x12\x18\n\x10program_id_index\x18\x01 \x01(\r\x12\x10\n\x08\x61\x63\x63ounts\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\"\x9d\x01\n\x0cTokenBalance\x12\x15\n\raccount_index\x18\x01 \x01(\r\x12\x0c\n\x04mint\x18\x02 \x01(\t\x12\x45\n\x0fui_token_amount\x18\x03 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.UiTokenAmount\x12\r\n\x05owner\x18\x04 \x01(\t\x12\x12\n\nprogram_id\x18\x05 \x01(\t\"^\n\rUiTokenAmount\x12\x11\n\tui_amount\x18\x01 \x01(\x01\x12\x10\n\x08\x64\x65\x63imals\x18\x02 \x01(\r\x12\x0e\n\x06\x61mount\x18\x03 \x01(\t\x12\x18\n\x10ui_amount_string\x18\x04 \x01(\t\".\n\nReturnData\x12\x12\n\nprogram_id\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x94\x01\n\x06Reward\x12\x0e\n\x06pubkey\x18\x01 \x01(\t\x12\x10\n\x08lamports\x18\x02 \x01(\x03\x12\x14\n\x0cpost_balance\x18\x03 \x01(\x04\x12>\n\x0breward_type\x18\x04 \x01(\x0e\x32).solana.storage.ConfirmedBlock.RewardType\x12\x12\n\ncommission\x18\x05 \x01(\t\"\x87\x01\n\x07Rewards\x12\x36\n\x07rewards\x18\x01 \x03(\x0b\x32%.solana.storage.ConfirmedBlock.Reward\x12\x44\n\x0enum_partitions\x18\x02 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.NumPartitions\"\"\n\rUnixTimestamp\x12\x11\n\ttimestamp\x18\x01 \x01(\x03\"#\n\x0b\x42lockHeight\x12\x14\n\x0c\x62lock_height\x18\x01 \x01(\x04\"\'\n\rNumPartitions\x12\x16\n\x0enum_partitions\x18\x01 \x01(\x04*I\n\nRewardType\x12\x0f\n\x0bUnspecified\x10\x00\x12\x07\n\x03\x46\x65\x65\x10\x01\x12\x08\n\x04Rent\x10\x02\x12\x0b\n\x07Staking\x10\x03\x12\n\n\x06Voting\x10\x04\x42;Z9github.com/rpcpool/yellowstone-grpc/examples/golang/protob\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'solana_storage_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z9github.com/rpcpool/yellowstone-grpc/examples/golang/proto' + _globals['_REWARDTYPE']._serialized_start=3042 + _globals['_REWARDTYPE']._serialized_end=3115 + _globals['_CONFIRMEDBLOCK']._serialized_start=56 + _globals['_CONFIRMEDBLOCK']._serialized_end=473 + _globals['_CONFIRMEDTRANSACTION']._serialized_start=476 + _globals['_CONFIRMEDTRANSACTION']._serialized_end=631 + _globals['_TRANSACTION']._serialized_start=633 + _globals['_TRANSACTION']._serialized_end=723 + _globals['_MESSAGE']._serialized_start=726 + _globals['_MESSAGE']._serialized_end=1027 + _globals['_MESSAGEHEADER']._serialized_start=1029 + _globals['_MESSAGEHEADER']._serialized_end=1155 + _globals['_MESSAGEADDRESSTABLELOOKUP']._serialized_start=1157 + _globals['_MESSAGEADDRESSTABLELOOKUP']._serialized_end=1257 + _globals['_TRANSACTIONSTATUSMETA']._serialized_start=1260 + _globals['_TRANSACTIONSTATUSMETA']._serialized_end=1990 + _globals['_TRANSACTIONERROR']._serialized_start=1992 + _globals['_TRANSACTIONERROR']._serialized_end=2023 + _globals['_INNERINSTRUCTIONS']._serialized_start=2025 + _globals['_INNERINSTRUCTIONS']._serialized_end=2130 + _globals['_INNERINSTRUCTION']._serialized_start=2132 + _globals['_INNERINSTRUCTION']._serialized_end=2252 + _globals['_COMPILEDINSTRUCTION']._serialized_start=2254 + _globals['_COMPILEDINSTRUCTION']._serialized_end=2333 + _globals['_TOKENBALANCE']._serialized_start=2336 + _globals['_TOKENBALANCE']._serialized_end=2493 + _globals['_UITOKENAMOUNT']._serialized_start=2495 + _globals['_UITOKENAMOUNT']._serialized_end=2589 + _globals['_RETURNDATA']._serialized_start=2591 + _globals['_RETURNDATA']._serialized_end=2637 + _globals['_REWARD']._serialized_start=2640 + _globals['_REWARD']._serialized_end=2788 + _globals['_REWARDS']._serialized_start=2791 + _globals['_REWARDS']._serialized_end=2926 + _globals['_UNIXTIMESTAMP']._serialized_start=2928 + _globals['_UNIXTIMESTAMP']._serialized_end=2962 + _globals['_BLOCKHEIGHT']._serialized_start=2964 + _globals['_BLOCKHEIGHT']._serialized_end=2999 + _globals['_NUMPARTITIONS']._serialized_start=3001 + _globals['_NUMPARTITIONS']._serialized_end=3040 +# @@protoc_insertion_point(module_scope) diff --git a/learning-examples/generated/solana_storage_pb2.pyi b/learning-examples/generated/solana_storage_pb2.pyi new file mode 100644 index 0000000..10312a6 --- /dev/null +++ b/learning-examples/generated/solana_storage_pb2.pyi @@ -0,0 +1,238 @@ +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class RewardType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + Unspecified: _ClassVar[RewardType] + Fee: _ClassVar[RewardType] + Rent: _ClassVar[RewardType] + Staking: _ClassVar[RewardType] + Voting: _ClassVar[RewardType] +Unspecified: RewardType +Fee: RewardType +Rent: RewardType +Staking: RewardType +Voting: RewardType + +class ConfirmedBlock(_message.Message): + __slots__ = ("previous_blockhash", "blockhash", "parent_slot", "transactions", "rewards", "block_time", "block_height", "num_partitions") + PREVIOUS_BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + PARENT_SLOT_FIELD_NUMBER: _ClassVar[int] + TRANSACTIONS_FIELD_NUMBER: _ClassVar[int] + REWARDS_FIELD_NUMBER: _ClassVar[int] + BLOCK_TIME_FIELD_NUMBER: _ClassVar[int] + BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int] + previous_blockhash: str + blockhash: str + parent_slot: int + transactions: _containers.RepeatedCompositeFieldContainer[ConfirmedTransaction] + rewards: _containers.RepeatedCompositeFieldContainer[Reward] + block_time: UnixTimestamp + block_height: BlockHeight + num_partitions: NumPartitions + def __init__(self, previous_blockhash: _Optional[str] = ..., blockhash: _Optional[str] = ..., parent_slot: _Optional[int] = ..., transactions: _Optional[_Iterable[_Union[ConfirmedTransaction, _Mapping]]] = ..., rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., block_time: _Optional[_Union[UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[BlockHeight, _Mapping]] = ..., num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...) -> None: ... + +class ConfirmedTransaction(_message.Message): + __slots__ = ("transaction", "meta") + TRANSACTION_FIELD_NUMBER: _ClassVar[int] + META_FIELD_NUMBER: _ClassVar[int] + transaction: Transaction + meta: TransactionStatusMeta + def __init__(self, transaction: _Optional[_Union[Transaction, _Mapping]] = ..., meta: _Optional[_Union[TransactionStatusMeta, _Mapping]] = ...) -> None: ... + +class Transaction(_message.Message): + __slots__ = ("signatures", "message") + SIGNATURES_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + signatures: _containers.RepeatedScalarFieldContainer[bytes] + message: Message + def __init__(self, signatures: _Optional[_Iterable[bytes]] = ..., message: _Optional[_Union[Message, _Mapping]] = ...) -> None: ... + +class Message(_message.Message): + __slots__ = ("header", "account_keys", "recent_blockhash", "instructions", "versioned", "address_table_lookups") + HEADER_FIELD_NUMBER: _ClassVar[int] + ACCOUNT_KEYS_FIELD_NUMBER: _ClassVar[int] + RECENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] + VERSIONED_FIELD_NUMBER: _ClassVar[int] + ADDRESS_TABLE_LOOKUPS_FIELD_NUMBER: _ClassVar[int] + header: MessageHeader + account_keys: _containers.RepeatedScalarFieldContainer[bytes] + recent_blockhash: bytes + instructions: _containers.RepeatedCompositeFieldContainer[CompiledInstruction] + versioned: bool + address_table_lookups: _containers.RepeatedCompositeFieldContainer[MessageAddressTableLookup] + def __init__(self, header: _Optional[_Union[MessageHeader, _Mapping]] = ..., account_keys: _Optional[_Iterable[bytes]] = ..., recent_blockhash: _Optional[bytes] = ..., instructions: _Optional[_Iterable[_Union[CompiledInstruction, _Mapping]]] = ..., versioned: bool = ..., address_table_lookups: _Optional[_Iterable[_Union[MessageAddressTableLookup, _Mapping]]] = ...) -> None: ... + +class MessageHeader(_message.Message): + __slots__ = ("num_required_signatures", "num_readonly_signed_accounts", "num_readonly_unsigned_accounts") + NUM_REQUIRED_SIGNATURES_FIELD_NUMBER: _ClassVar[int] + NUM_READONLY_SIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + NUM_READONLY_UNSIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + num_required_signatures: int + num_readonly_signed_accounts: int + num_readonly_unsigned_accounts: int + def __init__(self, num_required_signatures: _Optional[int] = ..., num_readonly_signed_accounts: _Optional[int] = ..., num_readonly_unsigned_accounts: _Optional[int] = ...) -> None: ... + +class MessageAddressTableLookup(_message.Message): + __slots__ = ("account_key", "writable_indexes", "readonly_indexes") + ACCOUNT_KEY_FIELD_NUMBER: _ClassVar[int] + WRITABLE_INDEXES_FIELD_NUMBER: _ClassVar[int] + READONLY_INDEXES_FIELD_NUMBER: _ClassVar[int] + account_key: bytes + writable_indexes: bytes + readonly_indexes: bytes + def __init__(self, account_key: _Optional[bytes] = ..., writable_indexes: _Optional[bytes] = ..., readonly_indexes: _Optional[bytes] = ...) -> None: ... + +class TransactionStatusMeta(_message.Message): + __slots__ = ("err", "fee", "pre_balances", "post_balances", "inner_instructions", "inner_instructions_none", "log_messages", "log_messages_none", "pre_token_balances", "post_token_balances", "rewards", "loaded_writable_addresses", "loaded_readonly_addresses", "return_data", "return_data_none", "compute_units_consumed") + ERR_FIELD_NUMBER: _ClassVar[int] + FEE_FIELD_NUMBER: _ClassVar[int] + PRE_BALANCES_FIELD_NUMBER: _ClassVar[int] + POST_BALANCES_FIELD_NUMBER: _ClassVar[int] + INNER_INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] + INNER_INSTRUCTIONS_NONE_FIELD_NUMBER: _ClassVar[int] + LOG_MESSAGES_FIELD_NUMBER: _ClassVar[int] + LOG_MESSAGES_NONE_FIELD_NUMBER: _ClassVar[int] + PRE_TOKEN_BALANCES_FIELD_NUMBER: _ClassVar[int] + POST_TOKEN_BALANCES_FIELD_NUMBER: _ClassVar[int] + REWARDS_FIELD_NUMBER: _ClassVar[int] + LOADED_WRITABLE_ADDRESSES_FIELD_NUMBER: _ClassVar[int] + LOADED_READONLY_ADDRESSES_FIELD_NUMBER: _ClassVar[int] + RETURN_DATA_FIELD_NUMBER: _ClassVar[int] + RETURN_DATA_NONE_FIELD_NUMBER: _ClassVar[int] + COMPUTE_UNITS_CONSUMED_FIELD_NUMBER: _ClassVar[int] + err: TransactionError + fee: int + pre_balances: _containers.RepeatedScalarFieldContainer[int] + post_balances: _containers.RepeatedScalarFieldContainer[int] + inner_instructions: _containers.RepeatedCompositeFieldContainer[InnerInstructions] + inner_instructions_none: bool + log_messages: _containers.RepeatedScalarFieldContainer[str] + log_messages_none: bool + pre_token_balances: _containers.RepeatedCompositeFieldContainer[TokenBalance] + post_token_balances: _containers.RepeatedCompositeFieldContainer[TokenBalance] + rewards: _containers.RepeatedCompositeFieldContainer[Reward] + loaded_writable_addresses: _containers.RepeatedScalarFieldContainer[bytes] + loaded_readonly_addresses: _containers.RepeatedScalarFieldContainer[bytes] + return_data: ReturnData + return_data_none: bool + compute_units_consumed: int + def __init__(self, err: _Optional[_Union[TransactionError, _Mapping]] = ..., fee: _Optional[int] = ..., pre_balances: _Optional[_Iterable[int]] = ..., post_balances: _Optional[_Iterable[int]] = ..., inner_instructions: _Optional[_Iterable[_Union[InnerInstructions, _Mapping]]] = ..., inner_instructions_none: bool = ..., log_messages: _Optional[_Iterable[str]] = ..., log_messages_none: bool = ..., pre_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ..., post_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ..., rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., loaded_writable_addresses: _Optional[_Iterable[bytes]] = ..., loaded_readonly_addresses: _Optional[_Iterable[bytes]] = ..., return_data: _Optional[_Union[ReturnData, _Mapping]] = ..., return_data_none: bool = ..., compute_units_consumed: _Optional[int] = ...) -> None: ... + +class TransactionError(_message.Message): + __slots__ = ("err",) + ERR_FIELD_NUMBER: _ClassVar[int] + err: bytes + def __init__(self, err: _Optional[bytes] = ...) -> None: ... + +class InnerInstructions(_message.Message): + __slots__ = ("index", "instructions") + INDEX_FIELD_NUMBER: _ClassVar[int] + INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] + index: int + instructions: _containers.RepeatedCompositeFieldContainer[InnerInstruction] + def __init__(self, index: _Optional[int] = ..., instructions: _Optional[_Iterable[_Union[InnerInstruction, _Mapping]]] = ...) -> None: ... + +class InnerInstruction(_message.Message): + __slots__ = ("program_id_index", "accounts", "data", "stack_height") + PROGRAM_ID_INDEX_FIELD_NUMBER: _ClassVar[int] + ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + STACK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + program_id_index: int + accounts: bytes + data: bytes + stack_height: int + def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ..., stack_height: _Optional[int] = ...) -> None: ... + +class CompiledInstruction(_message.Message): + __slots__ = ("program_id_index", "accounts", "data") + PROGRAM_ID_INDEX_FIELD_NUMBER: _ClassVar[int] + ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + program_id_index: int + accounts: bytes + data: bytes + def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ... + +class TokenBalance(_message.Message): + __slots__ = ("account_index", "mint", "ui_token_amount", "owner", "program_id") + ACCOUNT_INDEX_FIELD_NUMBER: _ClassVar[int] + MINT_FIELD_NUMBER: _ClassVar[int] + UI_TOKEN_AMOUNT_FIELD_NUMBER: _ClassVar[int] + OWNER_FIELD_NUMBER: _ClassVar[int] + PROGRAM_ID_FIELD_NUMBER: _ClassVar[int] + account_index: int + mint: str + ui_token_amount: UiTokenAmount + owner: str + program_id: str + def __init__(self, account_index: _Optional[int] = ..., mint: _Optional[str] = ..., ui_token_amount: _Optional[_Union[UiTokenAmount, _Mapping]] = ..., owner: _Optional[str] = ..., program_id: _Optional[str] = ...) -> None: ... + +class UiTokenAmount(_message.Message): + __slots__ = ("ui_amount", "decimals", "amount", "ui_amount_string") + UI_AMOUNT_FIELD_NUMBER: _ClassVar[int] + DECIMALS_FIELD_NUMBER: _ClassVar[int] + AMOUNT_FIELD_NUMBER: _ClassVar[int] + UI_AMOUNT_STRING_FIELD_NUMBER: _ClassVar[int] + ui_amount: float + decimals: int + amount: str + ui_amount_string: str + def __init__(self, ui_amount: _Optional[float] = ..., decimals: _Optional[int] = ..., amount: _Optional[str] = ..., ui_amount_string: _Optional[str] = ...) -> None: ... + +class ReturnData(_message.Message): + __slots__ = ("program_id", "data") + PROGRAM_ID_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + program_id: bytes + data: bytes + def __init__(self, program_id: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ... + +class Reward(_message.Message): + __slots__ = ("pubkey", "lamports", "post_balance", "reward_type", "commission") + PUBKEY_FIELD_NUMBER: _ClassVar[int] + LAMPORTS_FIELD_NUMBER: _ClassVar[int] + POST_BALANCE_FIELD_NUMBER: _ClassVar[int] + REWARD_TYPE_FIELD_NUMBER: _ClassVar[int] + COMMISSION_FIELD_NUMBER: _ClassVar[int] + pubkey: str + lamports: int + post_balance: int + reward_type: RewardType + commission: str + def __init__(self, pubkey: _Optional[str] = ..., lamports: _Optional[int] = ..., post_balance: _Optional[int] = ..., reward_type: _Optional[_Union[RewardType, str]] = ..., commission: _Optional[str] = ...) -> None: ... + +class Rewards(_message.Message): + __slots__ = ("rewards", "num_partitions") + REWARDS_FIELD_NUMBER: _ClassVar[int] + NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int] + rewards: _containers.RepeatedCompositeFieldContainer[Reward] + num_partitions: NumPartitions + def __init__(self, rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...) -> None: ... + +class UnixTimestamp(_message.Message): + __slots__ = ("timestamp",) + TIMESTAMP_FIELD_NUMBER: _ClassVar[int] + timestamp: int + def __init__(self, timestamp: _Optional[int] = ...) -> None: ... + +class BlockHeight(_message.Message): + __slots__ = ("block_height",) + BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + block_height: int + def __init__(self, block_height: _Optional[int] = ...) -> None: ... + +class NumPartitions(_message.Message): + __slots__ = ("num_partitions",) + NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int] + num_partitions: int + def __init__(self, num_partitions: _Optional[int] = ...) -> None: ... diff --git a/learning-examples/generated/solana_storage_pb2_grpc.py b/learning-examples/generated/solana_storage_pb2_grpc.py new file mode 100644 index 0000000..1544a78 --- /dev/null +++ b/learning-examples/generated/solana_storage_pb2_grpc.py @@ -0,0 +1,24 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + + +GRPC_GENERATED_VERSION = '1.71.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in solana_storage_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) diff --git a/learning-examples/listen_create_from_geyser.py b/learning-examples/listen_create_from_geyser.py new file mode 100644 index 0000000..4d57168 --- /dev/null +++ b/learning-examples/listen_create_from_geyser.py @@ -0,0 +1,119 @@ +import asyncio +import os +import struct + +import base58 +import grpc +from dotenv import load_dotenv +from generated import geyser_pb2, geyser_pb2_grpc + +load_dotenv() + + +GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT") +GEYSER_API_TOKEN = os.getenv("GEYSER_API_TOKEN") + +PUMP_PROGRAM = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" +PUMP_CREATE_PREFIX = struct.pack(" dict: + """Decode a create instruction from 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(" accounts = 1; + map slots = 2; + map transactions = 3; + map transactions_status = 10; + map blocks = 4; + map blocks_meta = 5; + map entry = 8; + optional CommitmentLevel commitment = 6; + repeated SubscribeRequestAccountsDataSlice accounts_data_slice = 7; + optional SubscribeRequestPing ping = 9; +} + +message SubscribeRequestFilterAccounts { + repeated string account = 2; + repeated string owner = 3; + repeated SubscribeRequestFilterAccountsFilter filters = 4; + optional bool nonempty_txn_signature = 5; +} + +message SubscribeRequestFilterAccountsFilter { + oneof filter { + SubscribeRequestFilterAccountsFilterMemcmp memcmp = 1; + uint64 datasize = 2; + bool token_account_state = 3; + SubscribeRequestFilterAccountsFilterLamports lamports = 4; + } +} + +message SubscribeRequestFilterAccountsFilterMemcmp { + uint64 offset = 1; + oneof data { + bytes bytes = 2; + string base58 = 3; + string base64 = 4; + } +} + +message SubscribeRequestFilterAccountsFilterLamports { + oneof cmp { + uint64 eq = 1; + uint64 ne = 2; + uint64 lt = 3; + uint64 gt = 4; + } +} + +message SubscribeRequestFilterSlots { + optional bool filter_by_commitment = 1; +} + +message SubscribeRequestFilterTransactions { + optional bool vote = 1; + optional bool failed = 2; + optional string signature = 5; + repeated string account_include = 3; + repeated string account_exclude = 4; + repeated string account_required = 6; +} + +message SubscribeRequestFilterBlocks { + repeated string account_include = 1; + optional bool include_transactions = 2; + optional bool include_accounts = 3; + optional bool include_entries = 4; +} + +message SubscribeRequestFilterBlocksMeta {} + +message SubscribeRequestFilterEntry {} + +message SubscribeRequestAccountsDataSlice { + uint64 offset = 1; + uint64 length = 2; +} + +message SubscribeRequestPing { + int32 id = 1; +} + +message SubscribeUpdate { + repeated string filters = 1; + oneof update_oneof { + SubscribeUpdateAccount account = 2; + SubscribeUpdateSlot slot = 3; + SubscribeUpdateTransaction transaction = 4; + SubscribeUpdateTransactionStatus transaction_status = 10; + SubscribeUpdateBlock block = 5; + SubscribeUpdatePing ping = 6; + SubscribeUpdatePong pong = 9; + SubscribeUpdateBlockMeta block_meta = 7; + SubscribeUpdateEntry entry = 8; + } +} + +message SubscribeUpdateAccount { + SubscribeUpdateAccountInfo account = 1; + uint64 slot = 2; + bool is_startup = 3; +} + +message SubscribeUpdateAccountInfo { + bytes pubkey = 1; + uint64 lamports = 2; + bytes owner = 3; + bool executable = 4; + uint64 rent_epoch = 5; + bytes data = 6; + uint64 write_version = 7; + optional bytes txn_signature = 8; +} + +message SubscribeUpdateSlot { + uint64 slot = 1; + optional uint64 parent = 2; + CommitmentLevel status = 3; + optional string dead_error = 4; +} + +message SubscribeUpdateTransaction { + SubscribeUpdateTransactionInfo transaction = 1; + uint64 slot = 2; +} + +message SubscribeUpdateTransactionInfo { + bytes signature = 1; + bool is_vote = 2; + solana.storage.ConfirmedBlock.Transaction transaction = 3; + solana.storage.ConfirmedBlock.TransactionStatusMeta meta = 4; + uint64 index = 5; +} + +message SubscribeUpdateTransactionStatus { + uint64 slot = 1; + bytes signature = 2; + bool is_vote = 3; + uint64 index = 4; + solana.storage.ConfirmedBlock.TransactionError err = 5; +} + +message SubscribeUpdateBlock { + uint64 slot = 1; + string blockhash = 2; + solana.storage.ConfirmedBlock.Rewards rewards = 3; + solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4; + solana.storage.ConfirmedBlock.BlockHeight block_height = 5; + uint64 parent_slot = 7; + string parent_blockhash = 8; + uint64 executed_transaction_count = 9; + repeated SubscribeUpdateTransactionInfo transactions = 6; + uint64 updated_account_count = 10; + repeated SubscribeUpdateAccountInfo accounts = 11; + uint64 entries_count = 12; + repeated SubscribeUpdateEntry entries = 13; +} + +message SubscribeUpdateBlockMeta { + uint64 slot = 1; + string blockhash = 2; + solana.storage.ConfirmedBlock.Rewards rewards = 3; + solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4; + solana.storage.ConfirmedBlock.BlockHeight block_height = 5; + uint64 parent_slot = 6; + string parent_blockhash = 7; + uint64 executed_transaction_count = 8; + uint64 entries_count = 9; +} + +message SubscribeUpdateEntry { + uint64 slot = 1; + uint64 index = 2; + uint64 num_hashes = 3; + bytes hash = 4; + uint64 executed_transaction_count = 5; + uint64 starting_transaction_index = 6; // added in v1.18, for solana 1.17 value is always 0 +} + +message SubscribeUpdatePing {} + +message SubscribeUpdatePong { + int32 id = 1; +} + +// non-streaming methods + +message PingRequest { + int32 count = 1; +} + +message PongResponse { + int32 count = 1; +} + +message GetLatestBlockhashRequest { + optional CommitmentLevel commitment = 1; +} + +message GetLatestBlockhashResponse { + uint64 slot = 1; + string blockhash = 2; + uint64 last_valid_block_height = 3; +} + +message GetBlockHeightRequest { + optional CommitmentLevel commitment = 1; +} + +message GetBlockHeightResponse { + uint64 block_height = 1; +} + +message GetSlotRequest { + optional CommitmentLevel commitment = 1; +} + +message GetSlotResponse { + uint64 slot = 1; +} + +message GetVersionRequest {} + +message GetVersionResponse { + string version = 1; +} + +message IsBlockhashValidRequest { + string blockhash = 1; + optional CommitmentLevel commitment = 2; +} + +message IsBlockhashValidResponse { + uint64 slot = 1; + bool valid = 2; +} \ No newline at end of file diff --git a/learning-examples/proto/solana-storage.proto b/learning-examples/proto/solana-storage.proto new file mode 100644 index 0000000..56e3eb0 --- /dev/null +++ b/learning-examples/proto/solana-storage.proto @@ -0,0 +1,149 @@ +syntax = "proto3"; + +package solana.storage.ConfirmedBlock; + +option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto"; + +message ConfirmedBlock { + string previous_blockhash = 1; + string blockhash = 2; + uint64 parent_slot = 3; + repeated ConfirmedTransaction transactions = 4; + repeated Reward rewards = 5; + UnixTimestamp block_time = 6; + BlockHeight block_height = 7; + NumPartitions num_partitions = 8; +} + +message ConfirmedTransaction { + Transaction transaction = 1; + TransactionStatusMeta meta = 2; +} + +message Transaction { + repeated bytes signatures = 1; + Message message = 2; +} + +message Message { + MessageHeader header = 1; + repeated bytes account_keys = 2; + bytes recent_blockhash = 3; + repeated CompiledInstruction instructions = 4; + bool versioned = 5; + repeated MessageAddressTableLookup address_table_lookups = 6; +} + +message MessageHeader { + uint32 num_required_signatures = 1; + uint32 num_readonly_signed_accounts = 2; + uint32 num_readonly_unsigned_accounts = 3; +} + +message MessageAddressTableLookup { + bytes account_key = 1; + bytes writable_indexes = 2; + bytes readonly_indexes = 3; +} + +message TransactionStatusMeta { + TransactionError err = 1; + uint64 fee = 2; + repeated uint64 pre_balances = 3; + repeated uint64 post_balances = 4; + repeated InnerInstructions inner_instructions = 5; + bool inner_instructions_none = 10; + repeated string log_messages = 6; + bool log_messages_none = 11; + repeated TokenBalance pre_token_balances = 7; + repeated TokenBalance post_token_balances = 8; + repeated Reward rewards = 9; + repeated bytes loaded_writable_addresses = 12; + repeated bytes loaded_readonly_addresses = 13; + ReturnData return_data = 14; + bool return_data_none = 15; + + // Sum of compute units consumed by all instructions. + // Available since Solana v1.10.35 / v1.11.6. + // Set to `None` for txs executed on earlier versions. + optional uint64 compute_units_consumed = 16; +} + +message TransactionError { + bytes err = 1; +} + +message InnerInstructions { + uint32 index = 1; + repeated InnerInstruction instructions = 2; +} + +message InnerInstruction { + uint32 program_id_index = 1; + bytes accounts = 2; + bytes data = 3; + + // Invocation stack height of an inner instruction. + // Available since Solana v1.14.6 + // Set to `None` for txs executed on earlier versions. + optional uint32 stack_height = 4; +} + +message CompiledInstruction { + uint32 program_id_index = 1; + bytes accounts = 2; + bytes data = 3; +} + +message TokenBalance { + uint32 account_index = 1; + string mint = 2; + UiTokenAmount ui_token_amount = 3; + string owner = 4; + string program_id = 5; +} + +message UiTokenAmount { + double ui_amount = 1; + uint32 decimals = 2; + string amount = 3; + string ui_amount_string = 4; +} + +message ReturnData { + bytes program_id = 1; + bytes data = 2; +} + +enum RewardType { + Unspecified = 0; + Fee = 1; + Rent = 2; + Staking = 3; + Voting = 4; +} + +message Reward { + string pubkey = 1; + int64 lamports = 2; + uint64 post_balance = 3; + RewardType reward_type = 4; + string commission = 5; +} + +message Rewards { + repeated Reward rewards = 1; + NumPartitions num_partitions = 2; +} + +message UnixTimestamp { + int64 timestamp = 1; +} + +message BlockHeight { + uint64 block_height = 1; +} + +message NumPartitions { + uint64 num_partitions = 1; +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e28e445..02a80b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,9 @@ dependencies = [ "websockets>=15.0", "python-dotenv>=1.0.1", "aiohttp>=3.11.13", + "grpcio>=1.71.0", + "grpcio-tools>=1.71.0", + "protobuf>=5.29.4", ] [project.optional-dependencies] @@ -79,4 +82,4 @@ ignore = ["E501"] [tool.ruff.format] quote-style = "double" indent-style = "space" -line-ending = "auto" \ No newline at end of file +line-ending = "auto" From 2177bf8c1b90a4ab326d8676fe000c6e8f418dd1 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Mon, 14 Apr 2025 20:12:10 +0000 Subject: [PATCH 57/65] feat: add geyser listener support to bot --- .env.example | 2 + src/cli.py | 13 + src/config.py | 11 +- src/core/client.py | 4 +- src/geyser/generated/geyser_pb2.py | 140 +++++ src/geyser/generated/geyser_pb2.pyi | 485 ++++++++++++++++++ src/geyser/generated/geyser_pb2_grpc.py | 355 +++++++++++++ src/geyser/generated/solana_storage_pb2.py | 75 +++ src/geyser/generated/solana_storage_pb2.pyi | 238 +++++++++ .../generated/solana_storage_pb2_grpc.py | 24 + src/geyser/proto/geyser.proto | 262 ++++++++++ src/geyser/proto/solana-storage.proto | 149 ++++++ src/monitoring/geyser_event_processor.py | 92 ++++ src/monitoring/geyser_listener.py | 159 ++++++ src/trading/trader.py | 22 +- tests/compare_listeners.py | 175 +++++-- tests/test_block_listener.py | 6 +- tests/test_geyser_listener.py | 107 ++++ tests/test_logs_listener.py | 6 +- 19 files changed, 2261 insertions(+), 64 deletions(-) create mode 100644 src/geyser/generated/geyser_pb2.py create mode 100644 src/geyser/generated/geyser_pb2.pyi create mode 100644 src/geyser/generated/geyser_pb2_grpc.py create mode 100644 src/geyser/generated/solana_storage_pb2.py create mode 100644 src/geyser/generated/solana_storage_pb2.pyi create mode 100644 src/geyser/generated/solana_storage_pb2_grpc.py create mode 100644 src/geyser/proto/geyser.proto create mode 100644 src/geyser/proto/solana-storage.proto create mode 100644 src/monitoring/geyser_event_processor.py create mode 100644 src/monitoring/geyser_listener.py create mode 100644 tests/test_geyser_listener.py diff --git a/.env.example b/.env.example index 5fed379..b3d084c 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,5 @@ SOLANA_NODE_RPC_ENDPOINT=... SOLANA_NODE_WSS_ENDPOINT=... +GEYSER_ENDPOINT=... +GEYSER_API_TOKEN=... SOLANA_PRIVATE_KEY=... \ No newline at end of file diff --git a/src/cli.py b/src/cli.py index 90a4620..3f35bc0 100644 --- a/src/cli.py +++ b/src/cli.py @@ -65,6 +65,9 @@ async def main() -> None: rpc_endpoint: str | None = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") wss_endpoint: str | None = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") private_key: str | None = os.environ.get("SOLANA_PRIVATE_KEY") + geyser_endpoint: str | None = os.environ.get("GEYSER_ENDPOINT") + geyser_api_token: str | None = os.environ.get("GEYSER_API_TOKEN") + # Validate configuration values if not rpc_endpoint or not rpc_endpoint.startswith(("http://", "https://")): @@ -79,6 +82,14 @@ async def main() -> None: logger.error("Invalid private key. Key appears to be missing or too short") sys.exit(1) + if config.LISTENER_TYPE.lower() == "geyser": + if not geyser_endpoint: + logger.error("GEYSER_ENDPOINT environment variable is not set") + sys.exit(1) + if not geyser_api_token: + logger.error("GEYSER_API_TOKEN environment variable is not set") + sys.exit(1) + # Get trading parameters buy_amount: float = args.amount if args.amount is not None else config.BUY_AMOUNT buy_slippage: float = ( @@ -97,6 +108,8 @@ async def main() -> None: sell_slippage=sell_slippage, max_retries=config.MAX_RETRIES, listener_type=config.LISTENER_TYPE, + geyser_endpoint=geyser_endpoint, + geyser_api_token=geyser_api_token, ) try: diff --git a/src/config.py b/src/config.py index b90c11e..70497bd 100644 --- a/src/config.py +++ b/src/config.py @@ -15,7 +15,7 @@ SELL_SLIPPAGE: float = 0.4 # Consistent slippage tolerance to maintain trading # EXTREME FAST Mode configuration # When enabled, skips waiting for the bonding curve to stabilize and RPC price check. # The bot buys the specified number of tokens directly, making the process faster but less precise. -EXTREME_FAST_MODE: bool = False +EXTREME_FAST_MODE: bool = True # Amount of tokens to buy in EXTREME FAST mode. No price calculation is done; the bot buys exactly this amount. EXTREME_FAST_TOKEN_AMOUNT: int = 30 @@ -24,15 +24,16 @@ EXTREME_FAST_TOKEN_AMOUNT: int = 30 # Manage transaction speed and cost on the Solana network ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Adaptive fee calculation ENABLE_FIXED_PRIORITY_FEE: bool = True # Use consistent, predictable fee -FIXED_PRIORITY_FEE: int = 2_000 # Base fee in microlamports +FIXED_PRIORITY_FEE: int = 500_000 # Base fee in microlamports EXTRA_PRIORITY_FEE: float = 0.0 # Percentage increase on priority fee (0.1 = 10%) -HARD_CAP_PRIOR_FEE: int = 200_000 # Maximum allowable fee to prevent excessive spending in microlamports +HARD_CAP_PRIOR_FEE: int = 500_000 # Maximum allowable fee to prevent excessive spending in microlamports # Listener configuration # Choose method for detecting new tokens on the network # "logs": Recommended for more stable token detection # "blocks": Unstable method, potentially less reliable +# "geyser": The fastest way for getting updates, requires Geyser endpoint LISTENER_TYPE = "logs" @@ -48,7 +49,7 @@ WAIT_TIME_BEFORE_NEW_TOKEN: int | float = 15 # Pause between token trades # Token and account management # Control token processing and account cleanup strategies -MAX_TOKEN_AGE: int | float = 0.1 # Maximum token age in seconds for processing +MAX_TOKEN_AGE: int | float = 0.001 # Maximum token age in seconds for processing # Cleanup mode determines when to manage token accounts. Options: # "disabled": No cleanup will occur. @@ -98,7 +99,7 @@ def validate_configuration() -> None: raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously") # Validate listener type - if LISTENER_TYPE not in ["logs", "blocks"]: + if LISTENER_TYPE not in ["logs", "blocks", "geyser"]: raise ValueError("LISTENER_TYPE must be either 'logs' or 'blocks'") # Validate cleanup mode diff --git a/src/core/client.py b/src/core/client.py index a39140b..d0caa16 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -8,7 +8,7 @@ from typing import Any import aiohttp from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Confirmed +from solana.rpc.commitment import Processed from solana.rpc.types import TxOpts from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price from solders.hash import Hash @@ -163,7 +163,7 @@ class SolanaClient: for attempt in range(max_retries): try: tx_opts = TxOpts( - skip_preflight=skip_preflight, preflight_commitment=Confirmed + skip_preflight=skip_preflight, preflight_commitment=Processed ) response = await client.send_transaction(transaction, tx_opts) return response.value diff --git a/src/geyser/generated/geyser_pb2.py b/src/geyser/generated/geyser_pb2.py new file mode 100644 index 0000000..222873f --- /dev/null +++ b/src/geyser/generated/geyser_pb2.py @@ -0,0 +1,140 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: geyser.proto +# Protobuf Python Version: 5.29.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder + +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 0, + '', + 'geyser.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + +from geyser.generated.solana_storage_pb2 import * + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cgeyser.proto\x12\x06geyser\x1a\x14solana-storage.proto\"\xf6\t\n\x10SubscribeRequest\x12\x38\n\x08\x61\x63\x63ounts\x18\x01 \x03(\x0b\x32&.geyser.SubscribeRequest.AccountsEntry\x12\x32\n\x05slots\x18\x02 \x03(\x0b\x32#.geyser.SubscribeRequest.SlotsEntry\x12@\n\x0ctransactions\x18\x03 \x03(\x0b\x32*.geyser.SubscribeRequest.TransactionsEntry\x12M\n\x13transactions_status\x18\n \x03(\x0b\x32\x30.geyser.SubscribeRequest.TransactionsStatusEntry\x12\x34\n\x06\x62locks\x18\x04 \x03(\x0b\x32$.geyser.SubscribeRequest.BlocksEntry\x12=\n\x0b\x62locks_meta\x18\x05 \x03(\x0b\x32(.geyser.SubscribeRequest.BlocksMetaEntry\x12\x32\n\x05\x65ntry\x18\x08 \x03(\x0b\x32#.geyser.SubscribeRequest.EntryEntry\x12\x30\n\ncommitment\x18\x06 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x12\x46\n\x13\x61\x63\x63ounts_data_slice\x18\x07 \x03(\x0b\x32).geyser.SubscribeRequestAccountsDataSlice\x12/\n\x04ping\x18\t \x01(\x0b\x32\x1c.geyser.SubscribeRequestPingH\x01\x88\x01\x01\x1aW\n\rAccountsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32&.geyser.SubscribeRequestFilterAccounts:\x02\x38\x01\x1aQ\n\nSlotsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x32\n\x05value\x18\x02 \x01(\x0b\x32#.geyser.SubscribeRequestFilterSlots:\x02\x38\x01\x1a_\n\x11TransactionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x39\n\x05value\x18\x02 \x01(\x0b\x32*.geyser.SubscribeRequestFilterTransactions:\x02\x38\x01\x1a\x65\n\x17TransactionsStatusEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x39\n\x05value\x18\x02 \x01(\x0b\x32*.geyser.SubscribeRequestFilterTransactions:\x02\x38\x01\x1aS\n\x0b\x42locksEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.geyser.SubscribeRequestFilterBlocks:\x02\x38\x01\x1a[\n\x0f\x42locksMetaEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x37\n\x05value\x18\x02 \x01(\x0b\x32(.geyser.SubscribeRequestFilterBlocksMeta:\x02\x38\x01\x1aQ\n\nEntryEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x32\n\x05value\x18\x02 \x01(\x0b\x32#.geyser.SubscribeRequestFilterEntry:\x02\x38\x01\x42\r\n\x0b_commitmentB\x07\n\x05_ping\"\xbf\x01\n\x1eSubscribeRequestFilterAccounts\x12\x0f\n\x07\x61\x63\x63ount\x18\x02 \x03(\t\x12\r\n\x05owner\x18\x03 \x03(\t\x12=\n\x07\x66ilters\x18\x04 \x03(\x0b\x32,.geyser.SubscribeRequestFilterAccountsFilter\x12#\n\x16nonempty_txn_signature\x18\x05 \x01(\x08H\x00\x88\x01\x01\x42\x19\n\x17_nonempty_txn_signature\"\xf3\x01\n$SubscribeRequestFilterAccountsFilter\x12\x44\n\x06memcmp\x18\x01 \x01(\x0b\x32\x32.geyser.SubscribeRequestFilterAccountsFilterMemcmpH\x00\x12\x12\n\x08\x64\x61tasize\x18\x02 \x01(\x04H\x00\x12\x1d\n\x13token_account_state\x18\x03 \x01(\x08H\x00\x12H\n\x08lamports\x18\x04 \x01(\x0b\x32\x34.geyser.SubscribeRequestFilterAccountsFilterLamportsH\x00\x42\x08\n\x06\x66ilter\"y\n*SubscribeRequestFilterAccountsFilterMemcmp\x12\x0e\n\x06offset\x18\x01 \x01(\x04\x12\x0f\n\x05\x62ytes\x18\x02 \x01(\x0cH\x00\x12\x10\n\x06\x62\x61se58\x18\x03 \x01(\tH\x00\x12\x10\n\x06\x62\x61se64\x18\x04 \x01(\tH\x00\x42\x06\n\x04\x64\x61ta\"m\n,SubscribeRequestFilterAccountsFilterLamports\x12\x0c\n\x02\x65q\x18\x01 \x01(\x04H\x00\x12\x0c\n\x02ne\x18\x02 \x01(\x04H\x00\x12\x0c\n\x02lt\x18\x03 \x01(\x04H\x00\x12\x0c\n\x02gt\x18\x04 \x01(\x04H\x00\x42\x05\n\x03\x63mp\"Y\n\x1bSubscribeRequestFilterSlots\x12!\n\x14\x66ilter_by_commitment\x18\x01 \x01(\x08H\x00\x88\x01\x01\x42\x17\n\x15_filter_by_commitment\"\xd2\x01\n\"SubscribeRequestFilterTransactions\x12\x11\n\x04vote\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x13\n\x06\x66\x61iled\x18\x02 \x01(\x08H\x01\x88\x01\x01\x12\x16\n\tsignature\x18\x05 \x01(\tH\x02\x88\x01\x01\x12\x17\n\x0f\x61\x63\x63ount_include\x18\x03 \x03(\t\x12\x17\n\x0f\x61\x63\x63ount_exclude\x18\x04 \x03(\t\x12\x18\n\x10\x61\x63\x63ount_required\x18\x06 \x03(\tB\x07\n\x05_voteB\t\n\x07_failedB\x0c\n\n_signature\"\xd9\x01\n\x1cSubscribeRequestFilterBlocks\x12\x17\n\x0f\x61\x63\x63ount_include\x18\x01 \x03(\t\x12!\n\x14include_transactions\x18\x02 \x01(\x08H\x00\x88\x01\x01\x12\x1d\n\x10include_accounts\x18\x03 \x01(\x08H\x01\x88\x01\x01\x12\x1c\n\x0finclude_entries\x18\x04 \x01(\x08H\x02\x88\x01\x01\x42\x17\n\x15_include_transactionsB\x13\n\x11_include_accountsB\x12\n\x10_include_entries\"\"\n SubscribeRequestFilterBlocksMeta\"\x1d\n\x1bSubscribeRequestFilterEntry\"C\n!SubscribeRequestAccountsDataSlice\x12\x0e\n\x06offset\x18\x01 \x01(\x04\x12\x0e\n\x06length\x18\x02 \x01(\x04\"\"\n\x14SubscribeRequestPing\x12\n\n\x02id\x18\x01 \x01(\x05\"\x85\x04\n\x0fSubscribeUpdate\x12\x0f\n\x07\x66ilters\x18\x01 \x03(\t\x12\x31\n\x07\x61\x63\x63ount\x18\x02 \x01(\x0b\x32\x1e.geyser.SubscribeUpdateAccountH\x00\x12+\n\x04slot\x18\x03 \x01(\x0b\x32\x1b.geyser.SubscribeUpdateSlotH\x00\x12\x39\n\x0btransaction\x18\x04 \x01(\x0b\x32\".geyser.SubscribeUpdateTransactionH\x00\x12\x46\n\x12transaction_status\x18\n \x01(\x0b\x32(.geyser.SubscribeUpdateTransactionStatusH\x00\x12-\n\x05\x62lock\x18\x05 \x01(\x0b\x32\x1c.geyser.SubscribeUpdateBlockH\x00\x12+\n\x04ping\x18\x06 \x01(\x0b\x32\x1b.geyser.SubscribeUpdatePingH\x00\x12+\n\x04pong\x18\t \x01(\x0b\x32\x1b.geyser.SubscribeUpdatePongH\x00\x12\x36\n\nblock_meta\x18\x07 \x01(\x0b\x32 .geyser.SubscribeUpdateBlockMetaH\x00\x12-\n\x05\x65ntry\x18\x08 \x01(\x0b\x32\x1c.geyser.SubscribeUpdateEntryH\x00\x42\x0e\n\x0cupdate_oneof\"o\n\x16SubscribeUpdateAccount\x12\x33\n\x07\x61\x63\x63ount\x18\x01 \x01(\x0b\x32\".geyser.SubscribeUpdateAccountInfo\x12\x0c\n\x04slot\x18\x02 \x01(\x04\x12\x12\n\nis_startup\x18\x03 \x01(\x08\"\xc8\x01\n\x1aSubscribeUpdateAccountInfo\x12\x0e\n\x06pubkey\x18\x01 \x01(\x0c\x12\x10\n\x08lamports\x18\x02 \x01(\x04\x12\r\n\x05owner\x18\x03 \x01(\x0c\x12\x12\n\nexecutable\x18\x04 \x01(\x08\x12\x12\n\nrent_epoch\x18\x05 \x01(\x04\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x15\n\rwrite_version\x18\x07 \x01(\x04\x12\x1a\n\rtxn_signature\x18\x08 \x01(\x0cH\x00\x88\x01\x01\x42\x10\n\x0e_txn_signature\"\x94\x01\n\x13SubscribeUpdateSlot\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x13\n\x06parent\x18\x02 \x01(\x04H\x00\x88\x01\x01\x12\'\n\x06status\x18\x03 \x01(\x0e\x32\x17.geyser.CommitmentLevel\x12\x17\n\ndead_error\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\t\n\x07_parentB\r\n\x0b_dead_error\"g\n\x1aSubscribeUpdateTransaction\x12;\n\x0btransaction\x18\x01 \x01(\x0b\x32&.geyser.SubscribeUpdateTransactionInfo\x12\x0c\n\x04slot\x18\x02 \x01(\x04\"\xd8\x01\n\x1eSubscribeUpdateTransactionInfo\x12\x11\n\tsignature\x18\x01 \x01(\x0c\x12\x0f\n\x07is_vote\x18\x02 \x01(\x08\x12?\n\x0btransaction\x18\x03 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.Transaction\x12\x42\n\x04meta\x18\x04 \x01(\x0b\x32\x34.solana.storage.ConfirmedBlock.TransactionStatusMeta\x12\r\n\x05index\x18\x05 \x01(\x04\"\xa1\x01\n SubscribeUpdateTransactionStatus\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0f\n\x07is_vote\x18\x03 \x01(\x08\x12\r\n\x05index\x18\x04 \x01(\x04\x12<\n\x03\x65rr\x18\x05 \x01(\x0b\x32/.solana.storage.ConfirmedBlock.TransactionError\"\xa0\x04\n\x14SubscribeUpdateBlock\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tblockhash\x18\x02 \x01(\t\x12\x37\n\x07rewards\x18\x03 \x01(\x0b\x32&.solana.storage.ConfirmedBlock.Rewards\x12@\n\nblock_time\x18\x04 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.UnixTimestamp\x12@\n\x0c\x62lock_height\x18\x05 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.BlockHeight\x12\x13\n\x0bparent_slot\x18\x07 \x01(\x04\x12\x18\n\x10parent_blockhash\x18\x08 \x01(\t\x12\"\n\x1a\x65xecuted_transaction_count\x18\t \x01(\x04\x12<\n\x0ctransactions\x18\x06 \x03(\x0b\x32&.geyser.SubscribeUpdateTransactionInfo\x12\x1d\n\x15updated_account_count\x18\n \x01(\x04\x12\x34\n\x08\x61\x63\x63ounts\x18\x0b \x03(\x0b\x32\".geyser.SubscribeUpdateAccountInfo\x12\x15\n\rentries_count\x18\x0c \x01(\x04\x12-\n\x07\x65ntries\x18\r \x03(\x0b\x32\x1c.geyser.SubscribeUpdateEntry\"\xe2\x02\n\x18SubscribeUpdateBlockMeta\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tblockhash\x18\x02 \x01(\t\x12\x37\n\x07rewards\x18\x03 \x01(\x0b\x32&.solana.storage.ConfirmedBlock.Rewards\x12@\n\nblock_time\x18\x04 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.UnixTimestamp\x12@\n\x0c\x62lock_height\x18\x05 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.BlockHeight\x12\x13\n\x0bparent_slot\x18\x06 \x01(\x04\x12\x18\n\x10parent_blockhash\x18\x07 \x01(\t\x12\"\n\x1a\x65xecuted_transaction_count\x18\x08 \x01(\x04\x12\x15\n\rentries_count\x18\t \x01(\x04\"\x9d\x01\n\x14SubscribeUpdateEntry\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\r\n\x05index\x18\x02 \x01(\x04\x12\x12\n\nnum_hashes\x18\x03 \x01(\x04\x12\x0c\n\x04hash\x18\x04 \x01(\x0c\x12\"\n\x1a\x65xecuted_transaction_count\x18\x05 \x01(\x04\x12\"\n\x1astarting_transaction_index\x18\x06 \x01(\x04\"\x15\n\x13SubscribeUpdatePing\"!\n\x13SubscribeUpdatePong\x12\n\n\x02id\x18\x01 \x01(\x05\"\x1c\n\x0bPingRequest\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\"\x1d\n\x0cPongResponse\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\"\\\n\x19GetLatestBlockhashRequest\x12\x30\n\ncommitment\x18\x01 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\"^\n\x1aGetLatestBlockhashResponse\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tblockhash\x18\x02 \x01(\t\x12\x1f\n\x17last_valid_block_height\x18\x03 \x01(\x04\"X\n\x15GetBlockHeightRequest\x12\x30\n\ncommitment\x18\x01 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\".\n\x16GetBlockHeightResponse\x12\x14\n\x0c\x62lock_height\x18\x01 \x01(\x04\"Q\n\x0eGetSlotRequest\x12\x30\n\ncommitment\x18\x01 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\"\x1f\n\x0fGetSlotResponse\x12\x0c\n\x04slot\x18\x01 \x01(\x04\"\x13\n\x11GetVersionRequest\"%\n\x12GetVersionResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\"m\n\x17IsBlockhashValidRequest\x12\x11\n\tblockhash\x18\x01 \x01(\t\x12\x30\n\ncommitment\x18\x02 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\"7\n\x18IsBlockhashValidResponse\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\r\n\x05valid\x18\x02 \x01(\x08*\x83\x01\n\x0f\x43ommitmentLevel\x12\r\n\tPROCESSED\x10\x00\x12\r\n\tCONFIRMED\x10\x01\x12\r\n\tFINALIZED\x10\x02\x12\x18\n\x14\x46IRST_SHRED_RECEIVED\x10\x03\x12\r\n\tCOMPLETED\x10\x04\x12\x10\n\x0c\x43REATED_BANK\x10\x05\x12\x08\n\x04\x44\x45\x41\x44\x10\x06\x32\x93\x04\n\x06Geyser\x12\x44\n\tSubscribe\x12\x18.geyser.SubscribeRequest\x1a\x17.geyser.SubscribeUpdate\"\x00(\x01\x30\x01\x12\x33\n\x04Ping\x12\x13.geyser.PingRequest\x1a\x14.geyser.PongResponse\"\x00\x12]\n\x12GetLatestBlockhash\x12!.geyser.GetLatestBlockhashRequest\x1a\".geyser.GetLatestBlockhashResponse\"\x00\x12Q\n\x0eGetBlockHeight\x12\x1d.geyser.GetBlockHeightRequest\x1a\x1e.geyser.GetBlockHeightResponse\"\x00\x12<\n\x07GetSlot\x12\x16.geyser.GetSlotRequest\x1a\x17.geyser.GetSlotResponse\"\x00\x12W\n\x10IsBlockhashValid\x12\x1f.geyser.IsBlockhashValidRequest\x1a .geyser.IsBlockhashValidResponse\"\x00\x12\x45\n\nGetVersion\x12\x19.geyser.GetVersionRequest\x1a\x1a.geyser.GetVersionResponse\"\x00\x42;Z9github.com/rpcpool/yellowstone-grpc/examples/golang/protoP\x00\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'geyser_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z9github.com/rpcpool/yellowstone-grpc/examples/golang/proto' + _globals['_SUBSCRIBEREQUEST_ACCOUNTSENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_ACCOUNTSENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_SLOTSENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_SLOTSENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_BLOCKSENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_BLOCKSENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_BLOCKSMETAENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_BLOCKSMETAENTRY']._serialized_options = b'8\001' + _globals['_SUBSCRIBEREQUEST_ENTRYENTRY']._loaded_options = None + _globals['_SUBSCRIBEREQUEST_ENTRYENTRY']._serialized_options = b'8\001' + _globals['_COMMITMENTLEVEL']._serialized_start=6020 + _globals['_COMMITMENTLEVEL']._serialized_end=6151 + _globals['_SUBSCRIBEREQUEST']._serialized_start=47 + _globals['_SUBSCRIBEREQUEST']._serialized_end=1317 + _globals['_SUBSCRIBEREQUEST_ACCOUNTSENTRY']._serialized_start=662 + _globals['_SUBSCRIBEREQUEST_ACCOUNTSENTRY']._serialized_end=749 + _globals['_SUBSCRIBEREQUEST_SLOTSENTRY']._serialized_start=751 + _globals['_SUBSCRIBEREQUEST_SLOTSENTRY']._serialized_end=832 + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSENTRY']._serialized_start=834 + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSENTRY']._serialized_end=929 + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY']._serialized_start=931 + _globals['_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY']._serialized_end=1032 + _globals['_SUBSCRIBEREQUEST_BLOCKSENTRY']._serialized_start=1034 + _globals['_SUBSCRIBEREQUEST_BLOCKSENTRY']._serialized_end=1117 + _globals['_SUBSCRIBEREQUEST_BLOCKSMETAENTRY']._serialized_start=1119 + _globals['_SUBSCRIBEREQUEST_BLOCKSMETAENTRY']._serialized_end=1210 + _globals['_SUBSCRIBEREQUEST_ENTRYENTRY']._serialized_start=1212 + _globals['_SUBSCRIBEREQUEST_ENTRYENTRY']._serialized_end=1293 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTS']._serialized_start=1320 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTS']._serialized_end=1511 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTER']._serialized_start=1514 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTER']._serialized_end=1757 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERMEMCMP']._serialized_start=1759 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERMEMCMP']._serialized_end=1880 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERLAMPORTS']._serialized_start=1882 + _globals['_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERLAMPORTS']._serialized_end=1991 + _globals['_SUBSCRIBEREQUESTFILTERSLOTS']._serialized_start=1993 + _globals['_SUBSCRIBEREQUESTFILTERSLOTS']._serialized_end=2082 + _globals['_SUBSCRIBEREQUESTFILTERTRANSACTIONS']._serialized_start=2085 + _globals['_SUBSCRIBEREQUESTFILTERTRANSACTIONS']._serialized_end=2295 + _globals['_SUBSCRIBEREQUESTFILTERBLOCKS']._serialized_start=2298 + _globals['_SUBSCRIBEREQUESTFILTERBLOCKS']._serialized_end=2515 + _globals['_SUBSCRIBEREQUESTFILTERBLOCKSMETA']._serialized_start=2517 + _globals['_SUBSCRIBEREQUESTFILTERBLOCKSMETA']._serialized_end=2551 + _globals['_SUBSCRIBEREQUESTFILTERENTRY']._serialized_start=2553 + _globals['_SUBSCRIBEREQUESTFILTERENTRY']._serialized_end=2582 + _globals['_SUBSCRIBEREQUESTACCOUNTSDATASLICE']._serialized_start=2584 + _globals['_SUBSCRIBEREQUESTACCOUNTSDATASLICE']._serialized_end=2651 + _globals['_SUBSCRIBEREQUESTPING']._serialized_start=2653 + _globals['_SUBSCRIBEREQUESTPING']._serialized_end=2687 + _globals['_SUBSCRIBEUPDATE']._serialized_start=2690 + _globals['_SUBSCRIBEUPDATE']._serialized_end=3207 + _globals['_SUBSCRIBEUPDATEACCOUNT']._serialized_start=3209 + _globals['_SUBSCRIBEUPDATEACCOUNT']._serialized_end=3320 + _globals['_SUBSCRIBEUPDATEACCOUNTINFO']._serialized_start=3323 + _globals['_SUBSCRIBEUPDATEACCOUNTINFO']._serialized_end=3523 + _globals['_SUBSCRIBEUPDATESLOT']._serialized_start=3526 + _globals['_SUBSCRIBEUPDATESLOT']._serialized_end=3674 + _globals['_SUBSCRIBEUPDATETRANSACTION']._serialized_start=3676 + _globals['_SUBSCRIBEUPDATETRANSACTION']._serialized_end=3779 + _globals['_SUBSCRIBEUPDATETRANSACTIONINFO']._serialized_start=3782 + _globals['_SUBSCRIBEUPDATETRANSACTIONINFO']._serialized_end=3998 + _globals['_SUBSCRIBEUPDATETRANSACTIONSTATUS']._serialized_start=4001 + _globals['_SUBSCRIBEUPDATETRANSACTIONSTATUS']._serialized_end=4162 + _globals['_SUBSCRIBEUPDATEBLOCK']._serialized_start=4165 + _globals['_SUBSCRIBEUPDATEBLOCK']._serialized_end=4709 + _globals['_SUBSCRIBEUPDATEBLOCKMETA']._serialized_start=4712 + _globals['_SUBSCRIBEUPDATEBLOCKMETA']._serialized_end=5066 + _globals['_SUBSCRIBEUPDATEENTRY']._serialized_start=5069 + _globals['_SUBSCRIBEUPDATEENTRY']._serialized_end=5226 + _globals['_SUBSCRIBEUPDATEPING']._serialized_start=5228 + _globals['_SUBSCRIBEUPDATEPING']._serialized_end=5249 + _globals['_SUBSCRIBEUPDATEPONG']._serialized_start=5251 + _globals['_SUBSCRIBEUPDATEPONG']._serialized_end=5284 + _globals['_PINGREQUEST']._serialized_start=5286 + _globals['_PINGREQUEST']._serialized_end=5314 + _globals['_PONGRESPONSE']._serialized_start=5316 + _globals['_PONGRESPONSE']._serialized_end=5345 + _globals['_GETLATESTBLOCKHASHREQUEST']._serialized_start=5347 + _globals['_GETLATESTBLOCKHASHREQUEST']._serialized_end=5439 + _globals['_GETLATESTBLOCKHASHRESPONSE']._serialized_start=5441 + _globals['_GETLATESTBLOCKHASHRESPONSE']._serialized_end=5535 + _globals['_GETBLOCKHEIGHTREQUEST']._serialized_start=5537 + _globals['_GETBLOCKHEIGHTREQUEST']._serialized_end=5625 + _globals['_GETBLOCKHEIGHTRESPONSE']._serialized_start=5627 + _globals['_GETBLOCKHEIGHTRESPONSE']._serialized_end=5673 + _globals['_GETSLOTREQUEST']._serialized_start=5675 + _globals['_GETSLOTREQUEST']._serialized_end=5756 + _globals['_GETSLOTRESPONSE']._serialized_start=5758 + _globals['_GETSLOTRESPONSE']._serialized_end=5789 + _globals['_GETVERSIONREQUEST']._serialized_start=5791 + _globals['_GETVERSIONREQUEST']._serialized_end=5810 + _globals['_GETVERSIONRESPONSE']._serialized_start=5812 + _globals['_GETVERSIONRESPONSE']._serialized_end=5849 + _globals['_ISBLOCKHASHVALIDREQUEST']._serialized_start=5851 + _globals['_ISBLOCKHASHVALIDREQUEST']._serialized_end=5960 + _globals['_ISBLOCKHASHVALIDRESPONSE']._serialized_start=5962 + _globals['_ISBLOCKHASHVALIDRESPONSE']._serialized_end=6017 + _globals['_GEYSER']._serialized_start=6154 + _globals['_GEYSER']._serialized_end=6685 +# @@protoc_insertion_point(module_scope) diff --git a/src/geyser/generated/geyser_pb2.pyi b/src/geyser/generated/geyser_pb2.pyi new file mode 100644 index 0000000..909f4fb --- /dev/null +++ b/src/geyser/generated/geyser_pb2.pyi @@ -0,0 +1,485 @@ +import solana_storage_pb2 as _solana_storage_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union +from solana_storage_pb2 import ConfirmedBlock as ConfirmedBlock +from solana_storage_pb2 import ConfirmedTransaction as ConfirmedTransaction +from solana_storage_pb2 import Transaction as Transaction +from solana_storage_pb2 import Message as Message +from solana_storage_pb2 import MessageHeader as MessageHeader +from solana_storage_pb2 import MessageAddressTableLookup as MessageAddressTableLookup +from solana_storage_pb2 import TransactionStatusMeta as TransactionStatusMeta +from solana_storage_pb2 import TransactionError as TransactionError +from solana_storage_pb2 import InnerInstructions as InnerInstructions +from solana_storage_pb2 import InnerInstruction as InnerInstruction +from solana_storage_pb2 import CompiledInstruction as CompiledInstruction +from solana_storage_pb2 import TokenBalance as TokenBalance +from solana_storage_pb2 import UiTokenAmount as UiTokenAmount +from solana_storage_pb2 import ReturnData as ReturnData +from solana_storage_pb2 import Reward as Reward +from solana_storage_pb2 import Rewards as Rewards +from solana_storage_pb2 import UnixTimestamp as UnixTimestamp +from solana_storage_pb2 import BlockHeight as BlockHeight +from solana_storage_pb2 import NumPartitions as NumPartitions +from solana_storage_pb2 import RewardType as RewardType + +DESCRIPTOR: _descriptor.FileDescriptor +Unspecified: _solana_storage_pb2.RewardType +Fee: _solana_storage_pb2.RewardType +Rent: _solana_storage_pb2.RewardType +Staking: _solana_storage_pb2.RewardType +Voting: _solana_storage_pb2.RewardType + +class CommitmentLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + PROCESSED: _ClassVar[CommitmentLevel] + CONFIRMED: _ClassVar[CommitmentLevel] + FINALIZED: _ClassVar[CommitmentLevel] + FIRST_SHRED_RECEIVED: _ClassVar[CommitmentLevel] + COMPLETED: _ClassVar[CommitmentLevel] + CREATED_BANK: _ClassVar[CommitmentLevel] + DEAD: _ClassVar[CommitmentLevel] +PROCESSED: CommitmentLevel +CONFIRMED: CommitmentLevel +FINALIZED: CommitmentLevel +FIRST_SHRED_RECEIVED: CommitmentLevel +COMPLETED: CommitmentLevel +CREATED_BANK: CommitmentLevel +DEAD: CommitmentLevel + +class SubscribeRequest(_message.Message): + __slots__ = ("accounts", "slots", "transactions", "transactions_status", "blocks", "blocks_meta", "entry", "commitment", "accounts_data_slice", "ping") + class AccountsEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterAccounts + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterAccounts, _Mapping]] = ...) -> None: ... + class SlotsEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterSlots + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterSlots, _Mapping]] = ...) -> None: ... + class TransactionsEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterTransactions + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterTransactions, _Mapping]] = ...) -> None: ... + class TransactionsStatusEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterTransactions + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterTransactions, _Mapping]] = ...) -> None: ... + class BlocksEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterBlocks + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterBlocks, _Mapping]] = ...) -> None: ... + class BlocksMetaEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterBlocksMeta + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterBlocksMeta, _Mapping]] = ...) -> None: ... + class EntryEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: SubscribeRequestFilterEntry + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterEntry, _Mapping]] = ...) -> None: ... + ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + SLOTS_FIELD_NUMBER: _ClassVar[int] + TRANSACTIONS_FIELD_NUMBER: _ClassVar[int] + TRANSACTIONS_STATUS_FIELD_NUMBER: _ClassVar[int] + BLOCKS_FIELD_NUMBER: _ClassVar[int] + BLOCKS_META_FIELD_NUMBER: _ClassVar[int] + ENTRY_FIELD_NUMBER: _ClassVar[int] + COMMITMENT_FIELD_NUMBER: _ClassVar[int] + ACCOUNTS_DATA_SLICE_FIELD_NUMBER: _ClassVar[int] + PING_FIELD_NUMBER: _ClassVar[int] + accounts: _containers.MessageMap[str, SubscribeRequestFilterAccounts] + slots: _containers.MessageMap[str, SubscribeRequestFilterSlots] + transactions: _containers.MessageMap[str, SubscribeRequestFilterTransactions] + transactions_status: _containers.MessageMap[str, SubscribeRequestFilterTransactions] + blocks: _containers.MessageMap[str, SubscribeRequestFilterBlocks] + blocks_meta: _containers.MessageMap[str, SubscribeRequestFilterBlocksMeta] + entry: _containers.MessageMap[str, SubscribeRequestFilterEntry] + commitment: CommitmentLevel + accounts_data_slice: _containers.RepeatedCompositeFieldContainer[SubscribeRequestAccountsDataSlice] + ping: SubscribeRequestPing + def __init__(self, accounts: _Optional[_Mapping[str, SubscribeRequestFilterAccounts]] = ..., slots: _Optional[_Mapping[str, SubscribeRequestFilterSlots]] = ..., transactions: _Optional[_Mapping[str, SubscribeRequestFilterTransactions]] = ..., transactions_status: _Optional[_Mapping[str, SubscribeRequestFilterTransactions]] = ..., blocks: _Optional[_Mapping[str, SubscribeRequestFilterBlocks]] = ..., blocks_meta: _Optional[_Mapping[str, SubscribeRequestFilterBlocksMeta]] = ..., entry: _Optional[_Mapping[str, SubscribeRequestFilterEntry]] = ..., commitment: _Optional[_Union[CommitmentLevel, str]] = ..., accounts_data_slice: _Optional[_Iterable[_Union[SubscribeRequestAccountsDataSlice, _Mapping]]] = ..., ping: _Optional[_Union[SubscribeRequestPing, _Mapping]] = ...) -> None: ... + +class SubscribeRequestFilterAccounts(_message.Message): + __slots__ = ("account", "owner", "filters", "nonempty_txn_signature") + ACCOUNT_FIELD_NUMBER: _ClassVar[int] + OWNER_FIELD_NUMBER: _ClassVar[int] + FILTERS_FIELD_NUMBER: _ClassVar[int] + NONEMPTY_TXN_SIGNATURE_FIELD_NUMBER: _ClassVar[int] + account: _containers.RepeatedScalarFieldContainer[str] + owner: _containers.RepeatedScalarFieldContainer[str] + filters: _containers.RepeatedCompositeFieldContainer[SubscribeRequestFilterAccountsFilter] + nonempty_txn_signature: bool + def __init__(self, account: _Optional[_Iterable[str]] = ..., owner: _Optional[_Iterable[str]] = ..., filters: _Optional[_Iterable[_Union[SubscribeRequestFilterAccountsFilter, _Mapping]]] = ..., nonempty_txn_signature: bool = ...) -> None: ... + +class SubscribeRequestFilterAccountsFilter(_message.Message): + __slots__ = ("memcmp", "datasize", "token_account_state", "lamports") + MEMCMP_FIELD_NUMBER: _ClassVar[int] + DATASIZE_FIELD_NUMBER: _ClassVar[int] + TOKEN_ACCOUNT_STATE_FIELD_NUMBER: _ClassVar[int] + LAMPORTS_FIELD_NUMBER: _ClassVar[int] + memcmp: SubscribeRequestFilterAccountsFilterMemcmp + datasize: int + token_account_state: bool + lamports: SubscribeRequestFilterAccountsFilterLamports + def __init__(self, memcmp: _Optional[_Union[SubscribeRequestFilterAccountsFilterMemcmp, _Mapping]] = ..., datasize: _Optional[int] = ..., token_account_state: bool = ..., lamports: _Optional[_Union[SubscribeRequestFilterAccountsFilterLamports, _Mapping]] = ...) -> None: ... + +class SubscribeRequestFilterAccountsFilterMemcmp(_message.Message): + __slots__ = ("offset", "bytes", "base58", "base64") + OFFSET_FIELD_NUMBER: _ClassVar[int] + BYTES_FIELD_NUMBER: _ClassVar[int] + BASE58_FIELD_NUMBER: _ClassVar[int] + BASE64_FIELD_NUMBER: _ClassVar[int] + offset: int + bytes: bytes + base58: str + base64: str + def __init__(self, offset: _Optional[int] = ..., bytes: _Optional[bytes] = ..., base58: _Optional[str] = ..., base64: _Optional[str] = ...) -> None: ... + +class SubscribeRequestFilterAccountsFilterLamports(_message.Message): + __slots__ = ("eq", "ne", "lt", "gt") + EQ_FIELD_NUMBER: _ClassVar[int] + NE_FIELD_NUMBER: _ClassVar[int] + LT_FIELD_NUMBER: _ClassVar[int] + GT_FIELD_NUMBER: _ClassVar[int] + eq: int + ne: int + lt: int + gt: int + def __init__(self, eq: _Optional[int] = ..., ne: _Optional[int] = ..., lt: _Optional[int] = ..., gt: _Optional[int] = ...) -> None: ... + +class SubscribeRequestFilterSlots(_message.Message): + __slots__ = ("filter_by_commitment",) + FILTER_BY_COMMITMENT_FIELD_NUMBER: _ClassVar[int] + filter_by_commitment: bool + def __init__(self, filter_by_commitment: bool = ...) -> None: ... + +class SubscribeRequestFilterTransactions(_message.Message): + __slots__ = ("vote", "failed", "signature", "account_include", "account_exclude", "account_required") + VOTE_FIELD_NUMBER: _ClassVar[int] + FAILED_FIELD_NUMBER: _ClassVar[int] + SIGNATURE_FIELD_NUMBER: _ClassVar[int] + ACCOUNT_INCLUDE_FIELD_NUMBER: _ClassVar[int] + ACCOUNT_EXCLUDE_FIELD_NUMBER: _ClassVar[int] + ACCOUNT_REQUIRED_FIELD_NUMBER: _ClassVar[int] + vote: bool + failed: bool + signature: str + account_include: _containers.RepeatedScalarFieldContainer[str] + account_exclude: _containers.RepeatedScalarFieldContainer[str] + account_required: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, vote: bool = ..., failed: bool = ..., signature: _Optional[str] = ..., account_include: _Optional[_Iterable[str]] = ..., account_exclude: _Optional[_Iterable[str]] = ..., account_required: _Optional[_Iterable[str]] = ...) -> None: ... + +class SubscribeRequestFilterBlocks(_message.Message): + __slots__ = ("account_include", "include_transactions", "include_accounts", "include_entries") + ACCOUNT_INCLUDE_FIELD_NUMBER: _ClassVar[int] + INCLUDE_TRANSACTIONS_FIELD_NUMBER: _ClassVar[int] + INCLUDE_ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + INCLUDE_ENTRIES_FIELD_NUMBER: _ClassVar[int] + account_include: _containers.RepeatedScalarFieldContainer[str] + include_transactions: bool + include_accounts: bool + include_entries: bool + def __init__(self, account_include: _Optional[_Iterable[str]] = ..., include_transactions: bool = ..., include_accounts: bool = ..., include_entries: bool = ...) -> None: ... + +class SubscribeRequestFilterBlocksMeta(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class SubscribeRequestFilterEntry(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class SubscribeRequestAccountsDataSlice(_message.Message): + __slots__ = ("offset", "length") + OFFSET_FIELD_NUMBER: _ClassVar[int] + LENGTH_FIELD_NUMBER: _ClassVar[int] + offset: int + length: int + def __init__(self, offset: _Optional[int] = ..., length: _Optional[int] = ...) -> None: ... + +class SubscribeRequestPing(_message.Message): + __slots__ = ("id",) + ID_FIELD_NUMBER: _ClassVar[int] + id: int + def __init__(self, id: _Optional[int] = ...) -> None: ... + +class SubscribeUpdate(_message.Message): + __slots__ = ("filters", "account", "slot", "transaction", "transaction_status", "block", "ping", "pong", "block_meta", "entry") + FILTERS_FIELD_NUMBER: _ClassVar[int] + ACCOUNT_FIELD_NUMBER: _ClassVar[int] + SLOT_FIELD_NUMBER: _ClassVar[int] + TRANSACTION_FIELD_NUMBER: _ClassVar[int] + TRANSACTION_STATUS_FIELD_NUMBER: _ClassVar[int] + BLOCK_FIELD_NUMBER: _ClassVar[int] + PING_FIELD_NUMBER: _ClassVar[int] + PONG_FIELD_NUMBER: _ClassVar[int] + BLOCK_META_FIELD_NUMBER: _ClassVar[int] + ENTRY_FIELD_NUMBER: _ClassVar[int] + filters: _containers.RepeatedScalarFieldContainer[str] + account: SubscribeUpdateAccount + slot: SubscribeUpdateSlot + transaction: SubscribeUpdateTransaction + transaction_status: SubscribeUpdateTransactionStatus + block: SubscribeUpdateBlock + ping: SubscribeUpdatePing + pong: SubscribeUpdatePong + block_meta: SubscribeUpdateBlockMeta + entry: SubscribeUpdateEntry + def __init__(self, filters: _Optional[_Iterable[str]] = ..., account: _Optional[_Union[SubscribeUpdateAccount, _Mapping]] = ..., slot: _Optional[_Union[SubscribeUpdateSlot, _Mapping]] = ..., transaction: _Optional[_Union[SubscribeUpdateTransaction, _Mapping]] = ..., transaction_status: _Optional[_Union[SubscribeUpdateTransactionStatus, _Mapping]] = ..., block: _Optional[_Union[SubscribeUpdateBlock, _Mapping]] = ..., ping: _Optional[_Union[SubscribeUpdatePing, _Mapping]] = ..., pong: _Optional[_Union[SubscribeUpdatePong, _Mapping]] = ..., block_meta: _Optional[_Union[SubscribeUpdateBlockMeta, _Mapping]] = ..., entry: _Optional[_Union[SubscribeUpdateEntry, _Mapping]] = ...) -> None: ... + +class SubscribeUpdateAccount(_message.Message): + __slots__ = ("account", "slot", "is_startup") + ACCOUNT_FIELD_NUMBER: _ClassVar[int] + SLOT_FIELD_NUMBER: _ClassVar[int] + IS_STARTUP_FIELD_NUMBER: _ClassVar[int] + account: SubscribeUpdateAccountInfo + slot: int + is_startup: bool + def __init__(self, account: _Optional[_Union[SubscribeUpdateAccountInfo, _Mapping]] = ..., slot: _Optional[int] = ..., is_startup: bool = ...) -> None: ... + +class SubscribeUpdateAccountInfo(_message.Message): + __slots__ = ("pubkey", "lamports", "owner", "executable", "rent_epoch", "data", "write_version", "txn_signature") + PUBKEY_FIELD_NUMBER: _ClassVar[int] + LAMPORTS_FIELD_NUMBER: _ClassVar[int] + OWNER_FIELD_NUMBER: _ClassVar[int] + EXECUTABLE_FIELD_NUMBER: _ClassVar[int] + RENT_EPOCH_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + WRITE_VERSION_FIELD_NUMBER: _ClassVar[int] + TXN_SIGNATURE_FIELD_NUMBER: _ClassVar[int] + pubkey: bytes + lamports: int + owner: bytes + executable: bool + rent_epoch: int + data: bytes + write_version: int + txn_signature: bytes + def __init__(self, pubkey: _Optional[bytes] = ..., lamports: _Optional[int] = ..., owner: _Optional[bytes] = ..., executable: bool = ..., rent_epoch: _Optional[int] = ..., data: _Optional[bytes] = ..., write_version: _Optional[int] = ..., txn_signature: _Optional[bytes] = ...) -> None: ... + +class SubscribeUpdateSlot(_message.Message): + __slots__ = ("slot", "parent", "status", "dead_error") + SLOT_FIELD_NUMBER: _ClassVar[int] + PARENT_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + DEAD_ERROR_FIELD_NUMBER: _ClassVar[int] + slot: int + parent: int + status: CommitmentLevel + dead_error: str + def __init__(self, slot: _Optional[int] = ..., parent: _Optional[int] = ..., status: _Optional[_Union[CommitmentLevel, str]] = ..., dead_error: _Optional[str] = ...) -> None: ... + +class SubscribeUpdateTransaction(_message.Message): + __slots__ = ("transaction", "slot") + TRANSACTION_FIELD_NUMBER: _ClassVar[int] + SLOT_FIELD_NUMBER: _ClassVar[int] + transaction: SubscribeUpdateTransactionInfo + slot: int + def __init__(self, transaction: _Optional[_Union[SubscribeUpdateTransactionInfo, _Mapping]] = ..., slot: _Optional[int] = ...) -> None: ... + +class SubscribeUpdateTransactionInfo(_message.Message): + __slots__ = ("signature", "is_vote", "transaction", "meta", "index") + SIGNATURE_FIELD_NUMBER: _ClassVar[int] + IS_VOTE_FIELD_NUMBER: _ClassVar[int] + TRANSACTION_FIELD_NUMBER: _ClassVar[int] + META_FIELD_NUMBER: _ClassVar[int] + INDEX_FIELD_NUMBER: _ClassVar[int] + signature: bytes + is_vote: bool + transaction: _solana_storage_pb2.Transaction + meta: _solana_storage_pb2.TransactionStatusMeta + index: int + def __init__(self, signature: _Optional[bytes] = ..., is_vote: bool = ..., transaction: _Optional[_Union[_solana_storage_pb2.Transaction, _Mapping]] = ..., meta: _Optional[_Union[_solana_storage_pb2.TransactionStatusMeta, _Mapping]] = ..., index: _Optional[int] = ...) -> None: ... + +class SubscribeUpdateTransactionStatus(_message.Message): + __slots__ = ("slot", "signature", "is_vote", "index", "err") + SLOT_FIELD_NUMBER: _ClassVar[int] + SIGNATURE_FIELD_NUMBER: _ClassVar[int] + IS_VOTE_FIELD_NUMBER: _ClassVar[int] + INDEX_FIELD_NUMBER: _ClassVar[int] + ERR_FIELD_NUMBER: _ClassVar[int] + slot: int + signature: bytes + is_vote: bool + index: int + err: _solana_storage_pb2.TransactionError + def __init__(self, slot: _Optional[int] = ..., signature: _Optional[bytes] = ..., is_vote: bool = ..., index: _Optional[int] = ..., err: _Optional[_Union[_solana_storage_pb2.TransactionError, _Mapping]] = ...) -> None: ... + +class SubscribeUpdateBlock(_message.Message): + __slots__ = ("slot", "blockhash", "rewards", "block_time", "block_height", "parent_slot", "parent_blockhash", "executed_transaction_count", "transactions", "updated_account_count", "accounts", "entries_count", "entries") + SLOT_FIELD_NUMBER: _ClassVar[int] + BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + REWARDS_FIELD_NUMBER: _ClassVar[int] + BLOCK_TIME_FIELD_NUMBER: _ClassVar[int] + BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + PARENT_SLOT_FIELD_NUMBER: _ClassVar[int] + PARENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int] + TRANSACTIONS_FIELD_NUMBER: _ClassVar[int] + UPDATED_ACCOUNT_COUNT_FIELD_NUMBER: _ClassVar[int] + ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + ENTRIES_COUNT_FIELD_NUMBER: _ClassVar[int] + ENTRIES_FIELD_NUMBER: _ClassVar[int] + slot: int + blockhash: str + rewards: _solana_storage_pb2.Rewards + block_time: _solana_storage_pb2.UnixTimestamp + block_height: _solana_storage_pb2.BlockHeight + parent_slot: int + parent_blockhash: str + executed_transaction_count: int + transactions: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateTransactionInfo] + updated_account_count: int + accounts: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateAccountInfo] + entries_count: int + entries: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateEntry] + def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ..., block_time: _Optional[_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[_solana_storage_pb2.BlockHeight, _Mapping]] = ..., parent_slot: _Optional[int] = ..., parent_blockhash: _Optional[str] = ..., executed_transaction_count: _Optional[int] = ..., transactions: _Optional[_Iterable[_Union[SubscribeUpdateTransactionInfo, _Mapping]]] = ..., updated_account_count: _Optional[int] = ..., accounts: _Optional[_Iterable[_Union[SubscribeUpdateAccountInfo, _Mapping]]] = ..., entries_count: _Optional[int] = ..., entries: _Optional[_Iterable[_Union[SubscribeUpdateEntry, _Mapping]]] = ...) -> None: ... + +class SubscribeUpdateBlockMeta(_message.Message): + __slots__ = ("slot", "blockhash", "rewards", "block_time", "block_height", "parent_slot", "parent_blockhash", "executed_transaction_count", "entries_count") + SLOT_FIELD_NUMBER: _ClassVar[int] + BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + REWARDS_FIELD_NUMBER: _ClassVar[int] + BLOCK_TIME_FIELD_NUMBER: _ClassVar[int] + BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + PARENT_SLOT_FIELD_NUMBER: _ClassVar[int] + PARENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int] + ENTRIES_COUNT_FIELD_NUMBER: _ClassVar[int] + slot: int + blockhash: str + rewards: _solana_storage_pb2.Rewards + block_time: _solana_storage_pb2.UnixTimestamp + block_height: _solana_storage_pb2.BlockHeight + parent_slot: int + parent_blockhash: str + executed_transaction_count: int + entries_count: int + def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ..., block_time: _Optional[_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[_solana_storage_pb2.BlockHeight, _Mapping]] = ..., parent_slot: _Optional[int] = ..., parent_blockhash: _Optional[str] = ..., executed_transaction_count: _Optional[int] = ..., entries_count: _Optional[int] = ...) -> None: ... + +class SubscribeUpdateEntry(_message.Message): + __slots__ = ("slot", "index", "num_hashes", "hash", "executed_transaction_count", "starting_transaction_index") + SLOT_FIELD_NUMBER: _ClassVar[int] + INDEX_FIELD_NUMBER: _ClassVar[int] + NUM_HASHES_FIELD_NUMBER: _ClassVar[int] + HASH_FIELD_NUMBER: _ClassVar[int] + EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int] + STARTING_TRANSACTION_INDEX_FIELD_NUMBER: _ClassVar[int] + slot: int + index: int + num_hashes: int + hash: bytes + executed_transaction_count: int + starting_transaction_index: int + def __init__(self, slot: _Optional[int] = ..., index: _Optional[int] = ..., num_hashes: _Optional[int] = ..., hash: _Optional[bytes] = ..., executed_transaction_count: _Optional[int] = ..., starting_transaction_index: _Optional[int] = ...) -> None: ... + +class SubscribeUpdatePing(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class SubscribeUpdatePong(_message.Message): + __slots__ = ("id",) + ID_FIELD_NUMBER: _ClassVar[int] + id: int + def __init__(self, id: _Optional[int] = ...) -> None: ... + +class PingRequest(_message.Message): + __slots__ = ("count",) + COUNT_FIELD_NUMBER: _ClassVar[int] + count: int + def __init__(self, count: _Optional[int] = ...) -> None: ... + +class PongResponse(_message.Message): + __slots__ = ("count",) + COUNT_FIELD_NUMBER: _ClassVar[int] + count: int + def __init__(self, count: _Optional[int] = ...) -> None: ... + +class GetLatestBlockhashRequest(_message.Message): + __slots__ = ("commitment",) + COMMITMENT_FIELD_NUMBER: _ClassVar[int] + commitment: CommitmentLevel + def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ... + +class GetLatestBlockhashResponse(_message.Message): + __slots__ = ("slot", "blockhash", "last_valid_block_height") + SLOT_FIELD_NUMBER: _ClassVar[int] + BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + LAST_VALID_BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + slot: int + blockhash: str + last_valid_block_height: int + def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., last_valid_block_height: _Optional[int] = ...) -> None: ... + +class GetBlockHeightRequest(_message.Message): + __slots__ = ("commitment",) + COMMITMENT_FIELD_NUMBER: _ClassVar[int] + commitment: CommitmentLevel + def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ... + +class GetBlockHeightResponse(_message.Message): + __slots__ = ("block_height",) + BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + block_height: int + def __init__(self, block_height: _Optional[int] = ...) -> None: ... + +class GetSlotRequest(_message.Message): + __slots__ = ("commitment",) + COMMITMENT_FIELD_NUMBER: _ClassVar[int] + commitment: CommitmentLevel + def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ... + +class GetSlotResponse(_message.Message): + __slots__ = ("slot",) + SLOT_FIELD_NUMBER: _ClassVar[int] + slot: int + def __init__(self, slot: _Optional[int] = ...) -> None: ... + +class GetVersionRequest(_message.Message): + __slots__ = () + def __init__(self) -> None: ... + +class GetVersionResponse(_message.Message): + __slots__ = ("version",) + VERSION_FIELD_NUMBER: _ClassVar[int] + version: str + def __init__(self, version: _Optional[str] = ...) -> None: ... + +class IsBlockhashValidRequest(_message.Message): + __slots__ = ("blockhash", "commitment") + BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + COMMITMENT_FIELD_NUMBER: _ClassVar[int] + blockhash: str + commitment: CommitmentLevel + def __init__(self, blockhash: _Optional[str] = ..., commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ... + +class IsBlockhashValidResponse(_message.Message): + __slots__ = ("slot", "valid") + SLOT_FIELD_NUMBER: _ClassVar[int] + VALID_FIELD_NUMBER: _ClassVar[int] + slot: int + valid: bool + def __init__(self, slot: _Optional[int] = ..., valid: bool = ...) -> None: ... diff --git a/src/geyser/generated/geyser_pb2_grpc.py b/src/geyser/generated/geyser_pb2_grpc.py new file mode 100644 index 0000000..84fcd5e --- /dev/null +++ b/src/geyser/generated/geyser_pb2_grpc.py @@ -0,0 +1,355 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" + +import grpc + +import geyser.generated.geyser_pb2 as geyser__pb2 + +GRPC_GENERATED_VERSION = '1.71.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in geyser_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class GeyserStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Subscribe = channel.stream_stream( + '/geyser.Geyser/Subscribe', + request_serializer=geyser__pb2.SubscribeRequest.SerializeToString, + response_deserializer=geyser__pb2.SubscribeUpdate.FromString, + _registered_method=True) + self.Ping = channel.unary_unary( + '/geyser.Geyser/Ping', + request_serializer=geyser__pb2.PingRequest.SerializeToString, + response_deserializer=geyser__pb2.PongResponse.FromString, + _registered_method=True) + self.GetLatestBlockhash = channel.unary_unary( + '/geyser.Geyser/GetLatestBlockhash', + request_serializer=geyser__pb2.GetLatestBlockhashRequest.SerializeToString, + response_deserializer=geyser__pb2.GetLatestBlockhashResponse.FromString, + _registered_method=True) + self.GetBlockHeight = channel.unary_unary( + '/geyser.Geyser/GetBlockHeight', + request_serializer=geyser__pb2.GetBlockHeightRequest.SerializeToString, + response_deserializer=geyser__pb2.GetBlockHeightResponse.FromString, + _registered_method=True) + self.GetSlot = channel.unary_unary( + '/geyser.Geyser/GetSlot', + request_serializer=geyser__pb2.GetSlotRequest.SerializeToString, + response_deserializer=geyser__pb2.GetSlotResponse.FromString, + _registered_method=True) + self.IsBlockhashValid = channel.unary_unary( + '/geyser.Geyser/IsBlockhashValid', + request_serializer=geyser__pb2.IsBlockhashValidRequest.SerializeToString, + response_deserializer=geyser__pb2.IsBlockhashValidResponse.FromString, + _registered_method=True) + self.GetVersion = channel.unary_unary( + '/geyser.Geyser/GetVersion', + request_serializer=geyser__pb2.GetVersionRequest.SerializeToString, + response_deserializer=geyser__pb2.GetVersionResponse.FromString, + _registered_method=True) + + +class GeyserServicer: + """Missing associated documentation comment in .proto file.""" + + def Subscribe(self, request_iterator, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Ping(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetLatestBlockhash(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetBlockHeight(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetSlot(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def IsBlockhashValid(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetVersion(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_GeyserServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Subscribe': grpc.stream_stream_rpc_method_handler( + servicer.Subscribe, + request_deserializer=geyser__pb2.SubscribeRequest.FromString, + response_serializer=geyser__pb2.SubscribeUpdate.SerializeToString, + ), + 'Ping': grpc.unary_unary_rpc_method_handler( + servicer.Ping, + request_deserializer=geyser__pb2.PingRequest.FromString, + response_serializer=geyser__pb2.PongResponse.SerializeToString, + ), + 'GetLatestBlockhash': grpc.unary_unary_rpc_method_handler( + servicer.GetLatestBlockhash, + request_deserializer=geyser__pb2.GetLatestBlockhashRequest.FromString, + response_serializer=geyser__pb2.GetLatestBlockhashResponse.SerializeToString, + ), + 'GetBlockHeight': grpc.unary_unary_rpc_method_handler( + servicer.GetBlockHeight, + request_deserializer=geyser__pb2.GetBlockHeightRequest.FromString, + response_serializer=geyser__pb2.GetBlockHeightResponse.SerializeToString, + ), + 'GetSlot': grpc.unary_unary_rpc_method_handler( + servicer.GetSlot, + request_deserializer=geyser__pb2.GetSlotRequest.FromString, + response_serializer=geyser__pb2.GetSlotResponse.SerializeToString, + ), + 'IsBlockhashValid': grpc.unary_unary_rpc_method_handler( + servicer.IsBlockhashValid, + request_deserializer=geyser__pb2.IsBlockhashValidRequest.FromString, + response_serializer=geyser__pb2.IsBlockhashValidResponse.SerializeToString, + ), + 'GetVersion': grpc.unary_unary_rpc_method_handler( + servicer.GetVersion, + request_deserializer=geyser__pb2.GetVersionRequest.FromString, + response_serializer=geyser__pb2.GetVersionResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'geyser.Geyser', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('geyser.Geyser', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class Geyser: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def Subscribe(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream( + request_iterator, + target, + '/geyser.Geyser/Subscribe', + geyser__pb2.SubscribeRequest.SerializeToString, + geyser__pb2.SubscribeUpdate.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Ping(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/Ping', + geyser__pb2.PingRequest.SerializeToString, + geyser__pb2.PongResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetLatestBlockhash(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/GetLatestBlockhash', + geyser__pb2.GetLatestBlockhashRequest.SerializeToString, + geyser__pb2.GetLatestBlockhashResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetBlockHeight(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/GetBlockHeight', + geyser__pb2.GetBlockHeightRequest.SerializeToString, + geyser__pb2.GetBlockHeightResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetSlot(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/GetSlot', + geyser__pb2.GetSlotRequest.SerializeToString, + geyser__pb2.GetSlotResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def IsBlockhashValid(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/IsBlockhashValid', + geyser__pb2.IsBlockhashValidRequest.SerializeToString, + geyser__pb2.IsBlockhashValidResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetVersion(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/geyser.Geyser/GetVersion', + geyser__pb2.GetVersionRequest.SerializeToString, + geyser__pb2.GetVersionResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/src/geyser/generated/solana_storage_pb2.py b/src/geyser/generated/solana_storage_pb2.py new file mode 100644 index 0000000..7458363 --- /dev/null +++ b/src/geyser/generated/solana_storage_pb2.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: solana-storage.proto +# Protobuf Python Version: 5.29.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 0, + '', + 'solana-storage.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14solana-storage.proto\x12\x1dsolana.storage.ConfirmedBlock\"\xa1\x03\n\x0e\x43onfirmedBlock\x12\x1a\n\x12previous_blockhash\x18\x01 \x01(\t\x12\x11\n\tblockhash\x18\x02 \x01(\t\x12\x13\n\x0bparent_slot\x18\x03 \x01(\x04\x12I\n\x0ctransactions\x18\x04 \x03(\x0b\x32\x33.solana.storage.ConfirmedBlock.ConfirmedTransaction\x12\x36\n\x07rewards\x18\x05 \x03(\x0b\x32%.solana.storage.ConfirmedBlock.Reward\x12@\n\nblock_time\x18\x06 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.UnixTimestamp\x12@\n\x0c\x62lock_height\x18\x07 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.BlockHeight\x12\x44\n\x0enum_partitions\x18\x08 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.NumPartitions\"\x9b\x01\n\x14\x43onfirmedTransaction\x12?\n\x0btransaction\x18\x01 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.Transaction\x12\x42\n\x04meta\x18\x02 \x01(\x0b\x32\x34.solana.storage.ConfirmedBlock.TransactionStatusMeta\"Z\n\x0bTransaction\x12\x12\n\nsignatures\x18\x01 \x03(\x0c\x12\x37\n\x07message\x18\x02 \x01(\x0b\x32&.solana.storage.ConfirmedBlock.Message\"\xad\x02\n\x07Message\x12<\n\x06header\x18\x01 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.MessageHeader\x12\x14\n\x0c\x61\x63\x63ount_keys\x18\x02 \x03(\x0c\x12\x18\n\x10recent_blockhash\x18\x03 \x01(\x0c\x12H\n\x0cinstructions\x18\x04 \x03(\x0b\x32\x32.solana.storage.ConfirmedBlock.CompiledInstruction\x12\x11\n\tversioned\x18\x05 \x01(\x08\x12W\n\x15\x61\x64\x64ress_table_lookups\x18\x06 \x03(\x0b\x32\x38.solana.storage.ConfirmedBlock.MessageAddressTableLookup\"~\n\rMessageHeader\x12\x1f\n\x17num_required_signatures\x18\x01 \x01(\r\x12$\n\x1cnum_readonly_signed_accounts\x18\x02 \x01(\r\x12&\n\x1enum_readonly_unsigned_accounts\x18\x03 \x01(\r\"d\n\x19MessageAddressTableLookup\x12\x13\n\x0b\x61\x63\x63ount_key\x18\x01 \x01(\x0c\x12\x18\n\x10writable_indexes\x18\x02 \x01(\x0c\x12\x18\n\x10readonly_indexes\x18\x03 \x01(\x0c\"\xda\x05\n\x15TransactionStatusMeta\x12<\n\x03\x65rr\x18\x01 \x01(\x0b\x32/.solana.storage.ConfirmedBlock.TransactionError\x12\x0b\n\x03\x66\x65\x65\x18\x02 \x01(\x04\x12\x14\n\x0cpre_balances\x18\x03 \x03(\x04\x12\x15\n\rpost_balances\x18\x04 \x03(\x04\x12L\n\x12inner_instructions\x18\x05 \x03(\x0b\x32\x30.solana.storage.ConfirmedBlock.InnerInstructions\x12\x1f\n\x17inner_instructions_none\x18\n \x01(\x08\x12\x14\n\x0clog_messages\x18\x06 \x03(\t\x12\x19\n\x11log_messages_none\x18\x0b \x01(\x08\x12G\n\x12pre_token_balances\x18\x07 \x03(\x0b\x32+.solana.storage.ConfirmedBlock.TokenBalance\x12H\n\x13post_token_balances\x18\x08 \x03(\x0b\x32+.solana.storage.ConfirmedBlock.TokenBalance\x12\x36\n\x07rewards\x18\t \x03(\x0b\x32%.solana.storage.ConfirmedBlock.Reward\x12!\n\x19loaded_writable_addresses\x18\x0c \x03(\x0c\x12!\n\x19loaded_readonly_addresses\x18\r \x03(\x0c\x12>\n\x0breturn_data\x18\x0e \x01(\x0b\x32).solana.storage.ConfirmedBlock.ReturnData\x12\x18\n\x10return_data_none\x18\x0f \x01(\x08\x12#\n\x16\x63ompute_units_consumed\x18\x10 \x01(\x04H\x00\x88\x01\x01\x42\x19\n\x17_compute_units_consumed\"\x1f\n\x10TransactionError\x12\x0b\n\x03\x65rr\x18\x01 \x01(\x0c\"i\n\x11InnerInstructions\x12\r\n\x05index\x18\x01 \x01(\r\x12\x45\n\x0cinstructions\x18\x02 \x03(\x0b\x32/.solana.storage.ConfirmedBlock.InnerInstruction\"x\n\x10InnerInstruction\x12\x18\n\x10program_id_index\x18\x01 \x01(\r\x12\x10\n\x08\x61\x63\x63ounts\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x19\n\x0cstack_height\x18\x04 \x01(\rH\x00\x88\x01\x01\x42\x0f\n\r_stack_height\"O\n\x13\x43ompiledInstruction\x12\x18\n\x10program_id_index\x18\x01 \x01(\r\x12\x10\n\x08\x61\x63\x63ounts\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\"\x9d\x01\n\x0cTokenBalance\x12\x15\n\raccount_index\x18\x01 \x01(\r\x12\x0c\n\x04mint\x18\x02 \x01(\t\x12\x45\n\x0fui_token_amount\x18\x03 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.UiTokenAmount\x12\r\n\x05owner\x18\x04 \x01(\t\x12\x12\n\nprogram_id\x18\x05 \x01(\t\"^\n\rUiTokenAmount\x12\x11\n\tui_amount\x18\x01 \x01(\x01\x12\x10\n\x08\x64\x65\x63imals\x18\x02 \x01(\r\x12\x0e\n\x06\x61mount\x18\x03 \x01(\t\x12\x18\n\x10ui_amount_string\x18\x04 \x01(\t\".\n\nReturnData\x12\x12\n\nprogram_id\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x94\x01\n\x06Reward\x12\x0e\n\x06pubkey\x18\x01 \x01(\t\x12\x10\n\x08lamports\x18\x02 \x01(\x03\x12\x14\n\x0cpost_balance\x18\x03 \x01(\x04\x12>\n\x0breward_type\x18\x04 \x01(\x0e\x32).solana.storage.ConfirmedBlock.RewardType\x12\x12\n\ncommission\x18\x05 \x01(\t\"\x87\x01\n\x07Rewards\x12\x36\n\x07rewards\x18\x01 \x03(\x0b\x32%.solana.storage.ConfirmedBlock.Reward\x12\x44\n\x0enum_partitions\x18\x02 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.NumPartitions\"\"\n\rUnixTimestamp\x12\x11\n\ttimestamp\x18\x01 \x01(\x03\"#\n\x0b\x42lockHeight\x12\x14\n\x0c\x62lock_height\x18\x01 \x01(\x04\"\'\n\rNumPartitions\x12\x16\n\x0enum_partitions\x18\x01 \x01(\x04*I\n\nRewardType\x12\x0f\n\x0bUnspecified\x10\x00\x12\x07\n\x03\x46\x65\x65\x10\x01\x12\x08\n\x04Rent\x10\x02\x12\x0b\n\x07Staking\x10\x03\x12\n\n\x06Voting\x10\x04\x42;Z9github.com/rpcpool/yellowstone-grpc/examples/golang/protob\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'solana_storage_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z9github.com/rpcpool/yellowstone-grpc/examples/golang/proto' + _globals['_REWARDTYPE']._serialized_start=3042 + _globals['_REWARDTYPE']._serialized_end=3115 + _globals['_CONFIRMEDBLOCK']._serialized_start=56 + _globals['_CONFIRMEDBLOCK']._serialized_end=473 + _globals['_CONFIRMEDTRANSACTION']._serialized_start=476 + _globals['_CONFIRMEDTRANSACTION']._serialized_end=631 + _globals['_TRANSACTION']._serialized_start=633 + _globals['_TRANSACTION']._serialized_end=723 + _globals['_MESSAGE']._serialized_start=726 + _globals['_MESSAGE']._serialized_end=1027 + _globals['_MESSAGEHEADER']._serialized_start=1029 + _globals['_MESSAGEHEADER']._serialized_end=1155 + _globals['_MESSAGEADDRESSTABLELOOKUP']._serialized_start=1157 + _globals['_MESSAGEADDRESSTABLELOOKUP']._serialized_end=1257 + _globals['_TRANSACTIONSTATUSMETA']._serialized_start=1260 + _globals['_TRANSACTIONSTATUSMETA']._serialized_end=1990 + _globals['_TRANSACTIONERROR']._serialized_start=1992 + _globals['_TRANSACTIONERROR']._serialized_end=2023 + _globals['_INNERINSTRUCTIONS']._serialized_start=2025 + _globals['_INNERINSTRUCTIONS']._serialized_end=2130 + _globals['_INNERINSTRUCTION']._serialized_start=2132 + _globals['_INNERINSTRUCTION']._serialized_end=2252 + _globals['_COMPILEDINSTRUCTION']._serialized_start=2254 + _globals['_COMPILEDINSTRUCTION']._serialized_end=2333 + _globals['_TOKENBALANCE']._serialized_start=2336 + _globals['_TOKENBALANCE']._serialized_end=2493 + _globals['_UITOKENAMOUNT']._serialized_start=2495 + _globals['_UITOKENAMOUNT']._serialized_end=2589 + _globals['_RETURNDATA']._serialized_start=2591 + _globals['_RETURNDATA']._serialized_end=2637 + _globals['_REWARD']._serialized_start=2640 + _globals['_REWARD']._serialized_end=2788 + _globals['_REWARDS']._serialized_start=2791 + _globals['_REWARDS']._serialized_end=2926 + _globals['_UNIXTIMESTAMP']._serialized_start=2928 + _globals['_UNIXTIMESTAMP']._serialized_end=2962 + _globals['_BLOCKHEIGHT']._serialized_start=2964 + _globals['_BLOCKHEIGHT']._serialized_end=2999 + _globals['_NUMPARTITIONS']._serialized_start=3001 + _globals['_NUMPARTITIONS']._serialized_end=3040 +# @@protoc_insertion_point(module_scope) diff --git a/src/geyser/generated/solana_storage_pb2.pyi b/src/geyser/generated/solana_storage_pb2.pyi new file mode 100644 index 0000000..10312a6 --- /dev/null +++ b/src/geyser/generated/solana_storage_pb2.pyi @@ -0,0 +1,238 @@ +from google.protobuf.internal import containers as _containers +from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class RewardType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + Unspecified: _ClassVar[RewardType] + Fee: _ClassVar[RewardType] + Rent: _ClassVar[RewardType] + Staking: _ClassVar[RewardType] + Voting: _ClassVar[RewardType] +Unspecified: RewardType +Fee: RewardType +Rent: RewardType +Staking: RewardType +Voting: RewardType + +class ConfirmedBlock(_message.Message): + __slots__ = ("previous_blockhash", "blockhash", "parent_slot", "transactions", "rewards", "block_time", "block_height", "num_partitions") + PREVIOUS_BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + PARENT_SLOT_FIELD_NUMBER: _ClassVar[int] + TRANSACTIONS_FIELD_NUMBER: _ClassVar[int] + REWARDS_FIELD_NUMBER: _ClassVar[int] + BLOCK_TIME_FIELD_NUMBER: _ClassVar[int] + BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int] + previous_blockhash: str + blockhash: str + parent_slot: int + transactions: _containers.RepeatedCompositeFieldContainer[ConfirmedTransaction] + rewards: _containers.RepeatedCompositeFieldContainer[Reward] + block_time: UnixTimestamp + block_height: BlockHeight + num_partitions: NumPartitions + def __init__(self, previous_blockhash: _Optional[str] = ..., blockhash: _Optional[str] = ..., parent_slot: _Optional[int] = ..., transactions: _Optional[_Iterable[_Union[ConfirmedTransaction, _Mapping]]] = ..., rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., block_time: _Optional[_Union[UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[BlockHeight, _Mapping]] = ..., num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...) -> None: ... + +class ConfirmedTransaction(_message.Message): + __slots__ = ("transaction", "meta") + TRANSACTION_FIELD_NUMBER: _ClassVar[int] + META_FIELD_NUMBER: _ClassVar[int] + transaction: Transaction + meta: TransactionStatusMeta + def __init__(self, transaction: _Optional[_Union[Transaction, _Mapping]] = ..., meta: _Optional[_Union[TransactionStatusMeta, _Mapping]] = ...) -> None: ... + +class Transaction(_message.Message): + __slots__ = ("signatures", "message") + SIGNATURES_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + signatures: _containers.RepeatedScalarFieldContainer[bytes] + message: Message + def __init__(self, signatures: _Optional[_Iterable[bytes]] = ..., message: _Optional[_Union[Message, _Mapping]] = ...) -> None: ... + +class Message(_message.Message): + __slots__ = ("header", "account_keys", "recent_blockhash", "instructions", "versioned", "address_table_lookups") + HEADER_FIELD_NUMBER: _ClassVar[int] + ACCOUNT_KEYS_FIELD_NUMBER: _ClassVar[int] + RECENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int] + INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] + VERSIONED_FIELD_NUMBER: _ClassVar[int] + ADDRESS_TABLE_LOOKUPS_FIELD_NUMBER: _ClassVar[int] + header: MessageHeader + account_keys: _containers.RepeatedScalarFieldContainer[bytes] + recent_blockhash: bytes + instructions: _containers.RepeatedCompositeFieldContainer[CompiledInstruction] + versioned: bool + address_table_lookups: _containers.RepeatedCompositeFieldContainer[MessageAddressTableLookup] + def __init__(self, header: _Optional[_Union[MessageHeader, _Mapping]] = ..., account_keys: _Optional[_Iterable[bytes]] = ..., recent_blockhash: _Optional[bytes] = ..., instructions: _Optional[_Iterable[_Union[CompiledInstruction, _Mapping]]] = ..., versioned: bool = ..., address_table_lookups: _Optional[_Iterable[_Union[MessageAddressTableLookup, _Mapping]]] = ...) -> None: ... + +class MessageHeader(_message.Message): + __slots__ = ("num_required_signatures", "num_readonly_signed_accounts", "num_readonly_unsigned_accounts") + NUM_REQUIRED_SIGNATURES_FIELD_NUMBER: _ClassVar[int] + NUM_READONLY_SIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + NUM_READONLY_UNSIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + num_required_signatures: int + num_readonly_signed_accounts: int + num_readonly_unsigned_accounts: int + def __init__(self, num_required_signatures: _Optional[int] = ..., num_readonly_signed_accounts: _Optional[int] = ..., num_readonly_unsigned_accounts: _Optional[int] = ...) -> None: ... + +class MessageAddressTableLookup(_message.Message): + __slots__ = ("account_key", "writable_indexes", "readonly_indexes") + ACCOUNT_KEY_FIELD_NUMBER: _ClassVar[int] + WRITABLE_INDEXES_FIELD_NUMBER: _ClassVar[int] + READONLY_INDEXES_FIELD_NUMBER: _ClassVar[int] + account_key: bytes + writable_indexes: bytes + readonly_indexes: bytes + def __init__(self, account_key: _Optional[bytes] = ..., writable_indexes: _Optional[bytes] = ..., readonly_indexes: _Optional[bytes] = ...) -> None: ... + +class TransactionStatusMeta(_message.Message): + __slots__ = ("err", "fee", "pre_balances", "post_balances", "inner_instructions", "inner_instructions_none", "log_messages", "log_messages_none", "pre_token_balances", "post_token_balances", "rewards", "loaded_writable_addresses", "loaded_readonly_addresses", "return_data", "return_data_none", "compute_units_consumed") + ERR_FIELD_NUMBER: _ClassVar[int] + FEE_FIELD_NUMBER: _ClassVar[int] + PRE_BALANCES_FIELD_NUMBER: _ClassVar[int] + POST_BALANCES_FIELD_NUMBER: _ClassVar[int] + INNER_INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] + INNER_INSTRUCTIONS_NONE_FIELD_NUMBER: _ClassVar[int] + LOG_MESSAGES_FIELD_NUMBER: _ClassVar[int] + LOG_MESSAGES_NONE_FIELD_NUMBER: _ClassVar[int] + PRE_TOKEN_BALANCES_FIELD_NUMBER: _ClassVar[int] + POST_TOKEN_BALANCES_FIELD_NUMBER: _ClassVar[int] + REWARDS_FIELD_NUMBER: _ClassVar[int] + LOADED_WRITABLE_ADDRESSES_FIELD_NUMBER: _ClassVar[int] + LOADED_READONLY_ADDRESSES_FIELD_NUMBER: _ClassVar[int] + RETURN_DATA_FIELD_NUMBER: _ClassVar[int] + RETURN_DATA_NONE_FIELD_NUMBER: _ClassVar[int] + COMPUTE_UNITS_CONSUMED_FIELD_NUMBER: _ClassVar[int] + err: TransactionError + fee: int + pre_balances: _containers.RepeatedScalarFieldContainer[int] + post_balances: _containers.RepeatedScalarFieldContainer[int] + inner_instructions: _containers.RepeatedCompositeFieldContainer[InnerInstructions] + inner_instructions_none: bool + log_messages: _containers.RepeatedScalarFieldContainer[str] + log_messages_none: bool + pre_token_balances: _containers.RepeatedCompositeFieldContainer[TokenBalance] + post_token_balances: _containers.RepeatedCompositeFieldContainer[TokenBalance] + rewards: _containers.RepeatedCompositeFieldContainer[Reward] + loaded_writable_addresses: _containers.RepeatedScalarFieldContainer[bytes] + loaded_readonly_addresses: _containers.RepeatedScalarFieldContainer[bytes] + return_data: ReturnData + return_data_none: bool + compute_units_consumed: int + def __init__(self, err: _Optional[_Union[TransactionError, _Mapping]] = ..., fee: _Optional[int] = ..., pre_balances: _Optional[_Iterable[int]] = ..., post_balances: _Optional[_Iterable[int]] = ..., inner_instructions: _Optional[_Iterable[_Union[InnerInstructions, _Mapping]]] = ..., inner_instructions_none: bool = ..., log_messages: _Optional[_Iterable[str]] = ..., log_messages_none: bool = ..., pre_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ..., post_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ..., rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., loaded_writable_addresses: _Optional[_Iterable[bytes]] = ..., loaded_readonly_addresses: _Optional[_Iterable[bytes]] = ..., return_data: _Optional[_Union[ReturnData, _Mapping]] = ..., return_data_none: bool = ..., compute_units_consumed: _Optional[int] = ...) -> None: ... + +class TransactionError(_message.Message): + __slots__ = ("err",) + ERR_FIELD_NUMBER: _ClassVar[int] + err: bytes + def __init__(self, err: _Optional[bytes] = ...) -> None: ... + +class InnerInstructions(_message.Message): + __slots__ = ("index", "instructions") + INDEX_FIELD_NUMBER: _ClassVar[int] + INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] + index: int + instructions: _containers.RepeatedCompositeFieldContainer[InnerInstruction] + def __init__(self, index: _Optional[int] = ..., instructions: _Optional[_Iterable[_Union[InnerInstruction, _Mapping]]] = ...) -> None: ... + +class InnerInstruction(_message.Message): + __slots__ = ("program_id_index", "accounts", "data", "stack_height") + PROGRAM_ID_INDEX_FIELD_NUMBER: _ClassVar[int] + ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + STACK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + program_id_index: int + accounts: bytes + data: bytes + stack_height: int + def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ..., stack_height: _Optional[int] = ...) -> None: ... + +class CompiledInstruction(_message.Message): + __slots__ = ("program_id_index", "accounts", "data") + PROGRAM_ID_INDEX_FIELD_NUMBER: _ClassVar[int] + ACCOUNTS_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + program_id_index: int + accounts: bytes + data: bytes + def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ... + +class TokenBalance(_message.Message): + __slots__ = ("account_index", "mint", "ui_token_amount", "owner", "program_id") + ACCOUNT_INDEX_FIELD_NUMBER: _ClassVar[int] + MINT_FIELD_NUMBER: _ClassVar[int] + UI_TOKEN_AMOUNT_FIELD_NUMBER: _ClassVar[int] + OWNER_FIELD_NUMBER: _ClassVar[int] + PROGRAM_ID_FIELD_NUMBER: _ClassVar[int] + account_index: int + mint: str + ui_token_amount: UiTokenAmount + owner: str + program_id: str + def __init__(self, account_index: _Optional[int] = ..., mint: _Optional[str] = ..., ui_token_amount: _Optional[_Union[UiTokenAmount, _Mapping]] = ..., owner: _Optional[str] = ..., program_id: _Optional[str] = ...) -> None: ... + +class UiTokenAmount(_message.Message): + __slots__ = ("ui_amount", "decimals", "amount", "ui_amount_string") + UI_AMOUNT_FIELD_NUMBER: _ClassVar[int] + DECIMALS_FIELD_NUMBER: _ClassVar[int] + AMOUNT_FIELD_NUMBER: _ClassVar[int] + UI_AMOUNT_STRING_FIELD_NUMBER: _ClassVar[int] + ui_amount: float + decimals: int + amount: str + ui_amount_string: str + def __init__(self, ui_amount: _Optional[float] = ..., decimals: _Optional[int] = ..., amount: _Optional[str] = ..., ui_amount_string: _Optional[str] = ...) -> None: ... + +class ReturnData(_message.Message): + __slots__ = ("program_id", "data") + PROGRAM_ID_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + program_id: bytes + data: bytes + def __init__(self, program_id: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ... + +class Reward(_message.Message): + __slots__ = ("pubkey", "lamports", "post_balance", "reward_type", "commission") + PUBKEY_FIELD_NUMBER: _ClassVar[int] + LAMPORTS_FIELD_NUMBER: _ClassVar[int] + POST_BALANCE_FIELD_NUMBER: _ClassVar[int] + REWARD_TYPE_FIELD_NUMBER: _ClassVar[int] + COMMISSION_FIELD_NUMBER: _ClassVar[int] + pubkey: str + lamports: int + post_balance: int + reward_type: RewardType + commission: str + def __init__(self, pubkey: _Optional[str] = ..., lamports: _Optional[int] = ..., post_balance: _Optional[int] = ..., reward_type: _Optional[_Union[RewardType, str]] = ..., commission: _Optional[str] = ...) -> None: ... + +class Rewards(_message.Message): + __slots__ = ("rewards", "num_partitions") + REWARDS_FIELD_NUMBER: _ClassVar[int] + NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int] + rewards: _containers.RepeatedCompositeFieldContainer[Reward] + num_partitions: NumPartitions + def __init__(self, rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...) -> None: ... + +class UnixTimestamp(_message.Message): + __slots__ = ("timestamp",) + TIMESTAMP_FIELD_NUMBER: _ClassVar[int] + timestamp: int + def __init__(self, timestamp: _Optional[int] = ...) -> None: ... + +class BlockHeight(_message.Message): + __slots__ = ("block_height",) + BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int] + block_height: int + def __init__(self, block_height: _Optional[int] = ...) -> None: ... + +class NumPartitions(_message.Message): + __slots__ = ("num_partitions",) + NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int] + num_partitions: int + def __init__(self, num_partitions: _Optional[int] = ...) -> None: ... diff --git a/src/geyser/generated/solana_storage_pb2_grpc.py b/src/geyser/generated/solana_storage_pb2_grpc.py new file mode 100644 index 0000000..1544a78 --- /dev/null +++ b/src/geyser/generated/solana_storage_pb2_grpc.py @@ -0,0 +1,24 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + + +GRPC_GENERATED_VERSION = '1.71.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in solana_storage_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) diff --git a/src/geyser/proto/geyser.proto b/src/geyser/proto/geyser.proto new file mode 100644 index 0000000..a6f0421 --- /dev/null +++ b/src/geyser/proto/geyser.proto @@ -0,0 +1,262 @@ +syntax = "proto3"; + +import public "solana-storage.proto"; + +option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto"; + +package geyser; + +service Geyser { + rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeUpdate) {} + rpc Ping(PingRequest) returns (PongResponse) {} + rpc GetLatestBlockhash(GetLatestBlockhashRequest) returns (GetLatestBlockhashResponse) {} + rpc GetBlockHeight(GetBlockHeightRequest) returns (GetBlockHeightResponse) {} + rpc GetSlot(GetSlotRequest) returns (GetSlotResponse) {} + rpc IsBlockhashValid(IsBlockhashValidRequest) returns (IsBlockhashValidResponse) {} + rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {} +} + +enum CommitmentLevel { + PROCESSED = 0; + CONFIRMED = 1; + FINALIZED = 2; + FIRST_SHRED_RECEIVED = 3; + COMPLETED = 4; + CREATED_BANK = 5; + DEAD = 6; +} + +message SubscribeRequest { + map accounts = 1; + map slots = 2; + map transactions = 3; + map transactions_status = 10; + map blocks = 4; + map blocks_meta = 5; + map entry = 8; + optional CommitmentLevel commitment = 6; + repeated SubscribeRequestAccountsDataSlice accounts_data_slice = 7; + optional SubscribeRequestPing ping = 9; +} + +message SubscribeRequestFilterAccounts { + repeated string account = 2; + repeated string owner = 3; + repeated SubscribeRequestFilterAccountsFilter filters = 4; + optional bool nonempty_txn_signature = 5; +} + +message SubscribeRequestFilterAccountsFilter { + oneof filter { + SubscribeRequestFilterAccountsFilterMemcmp memcmp = 1; + uint64 datasize = 2; + bool token_account_state = 3; + SubscribeRequestFilterAccountsFilterLamports lamports = 4; + } +} + +message SubscribeRequestFilterAccountsFilterMemcmp { + uint64 offset = 1; + oneof data { + bytes bytes = 2; + string base58 = 3; + string base64 = 4; + } +} + +message SubscribeRequestFilterAccountsFilterLamports { + oneof cmp { + uint64 eq = 1; + uint64 ne = 2; + uint64 lt = 3; + uint64 gt = 4; + } +} + +message SubscribeRequestFilterSlots { + optional bool filter_by_commitment = 1; +} + +message SubscribeRequestFilterTransactions { + optional bool vote = 1; + optional bool failed = 2; + optional string signature = 5; + repeated string account_include = 3; + repeated string account_exclude = 4; + repeated string account_required = 6; +} + +message SubscribeRequestFilterBlocks { + repeated string account_include = 1; + optional bool include_transactions = 2; + optional bool include_accounts = 3; + optional bool include_entries = 4; +} + +message SubscribeRequestFilterBlocksMeta {} + +message SubscribeRequestFilterEntry {} + +message SubscribeRequestAccountsDataSlice { + uint64 offset = 1; + uint64 length = 2; +} + +message SubscribeRequestPing { + int32 id = 1; +} + +message SubscribeUpdate { + repeated string filters = 1; + oneof update_oneof { + SubscribeUpdateAccount account = 2; + SubscribeUpdateSlot slot = 3; + SubscribeUpdateTransaction transaction = 4; + SubscribeUpdateTransactionStatus transaction_status = 10; + SubscribeUpdateBlock block = 5; + SubscribeUpdatePing ping = 6; + SubscribeUpdatePong pong = 9; + SubscribeUpdateBlockMeta block_meta = 7; + SubscribeUpdateEntry entry = 8; + } +} + +message SubscribeUpdateAccount { + SubscribeUpdateAccountInfo account = 1; + uint64 slot = 2; + bool is_startup = 3; +} + +message SubscribeUpdateAccountInfo { + bytes pubkey = 1; + uint64 lamports = 2; + bytes owner = 3; + bool executable = 4; + uint64 rent_epoch = 5; + bytes data = 6; + uint64 write_version = 7; + optional bytes txn_signature = 8; +} + +message SubscribeUpdateSlot { + uint64 slot = 1; + optional uint64 parent = 2; + CommitmentLevel status = 3; + optional string dead_error = 4; +} + +message SubscribeUpdateTransaction { + SubscribeUpdateTransactionInfo transaction = 1; + uint64 slot = 2; +} + +message SubscribeUpdateTransactionInfo { + bytes signature = 1; + bool is_vote = 2; + solana.storage.ConfirmedBlock.Transaction transaction = 3; + solana.storage.ConfirmedBlock.TransactionStatusMeta meta = 4; + uint64 index = 5; +} + +message SubscribeUpdateTransactionStatus { + uint64 slot = 1; + bytes signature = 2; + bool is_vote = 3; + uint64 index = 4; + solana.storage.ConfirmedBlock.TransactionError err = 5; +} + +message SubscribeUpdateBlock { + uint64 slot = 1; + string blockhash = 2; + solana.storage.ConfirmedBlock.Rewards rewards = 3; + solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4; + solana.storage.ConfirmedBlock.BlockHeight block_height = 5; + uint64 parent_slot = 7; + string parent_blockhash = 8; + uint64 executed_transaction_count = 9; + repeated SubscribeUpdateTransactionInfo transactions = 6; + uint64 updated_account_count = 10; + repeated SubscribeUpdateAccountInfo accounts = 11; + uint64 entries_count = 12; + repeated SubscribeUpdateEntry entries = 13; +} + +message SubscribeUpdateBlockMeta { + uint64 slot = 1; + string blockhash = 2; + solana.storage.ConfirmedBlock.Rewards rewards = 3; + solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4; + solana.storage.ConfirmedBlock.BlockHeight block_height = 5; + uint64 parent_slot = 6; + string parent_blockhash = 7; + uint64 executed_transaction_count = 8; + uint64 entries_count = 9; +} + +message SubscribeUpdateEntry { + uint64 slot = 1; + uint64 index = 2; + uint64 num_hashes = 3; + bytes hash = 4; + uint64 executed_transaction_count = 5; + uint64 starting_transaction_index = 6; // added in v1.18, for solana 1.17 value is always 0 +} + +message SubscribeUpdatePing {} + +message SubscribeUpdatePong { + int32 id = 1; +} + +// non-streaming methods + +message PingRequest { + int32 count = 1; +} + +message PongResponse { + int32 count = 1; +} + +message GetLatestBlockhashRequest { + optional CommitmentLevel commitment = 1; +} + +message GetLatestBlockhashResponse { + uint64 slot = 1; + string blockhash = 2; + uint64 last_valid_block_height = 3; +} + +message GetBlockHeightRequest { + optional CommitmentLevel commitment = 1; +} + +message GetBlockHeightResponse { + uint64 block_height = 1; +} + +message GetSlotRequest { + optional CommitmentLevel commitment = 1; +} + +message GetSlotResponse { + uint64 slot = 1; +} + +message GetVersionRequest {} + +message GetVersionResponse { + string version = 1; +} + +message IsBlockhashValidRequest { + string blockhash = 1; + optional CommitmentLevel commitment = 2; +} + +message IsBlockhashValidResponse { + uint64 slot = 1; + bool valid = 2; +} \ No newline at end of file diff --git a/src/geyser/proto/solana-storage.proto b/src/geyser/proto/solana-storage.proto new file mode 100644 index 0000000..56e3eb0 --- /dev/null +++ b/src/geyser/proto/solana-storage.proto @@ -0,0 +1,149 @@ +syntax = "proto3"; + +package solana.storage.ConfirmedBlock; + +option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto"; + +message ConfirmedBlock { + string previous_blockhash = 1; + string blockhash = 2; + uint64 parent_slot = 3; + repeated ConfirmedTransaction transactions = 4; + repeated Reward rewards = 5; + UnixTimestamp block_time = 6; + BlockHeight block_height = 7; + NumPartitions num_partitions = 8; +} + +message ConfirmedTransaction { + Transaction transaction = 1; + TransactionStatusMeta meta = 2; +} + +message Transaction { + repeated bytes signatures = 1; + Message message = 2; +} + +message Message { + MessageHeader header = 1; + repeated bytes account_keys = 2; + bytes recent_blockhash = 3; + repeated CompiledInstruction instructions = 4; + bool versioned = 5; + repeated MessageAddressTableLookup address_table_lookups = 6; +} + +message MessageHeader { + uint32 num_required_signatures = 1; + uint32 num_readonly_signed_accounts = 2; + uint32 num_readonly_unsigned_accounts = 3; +} + +message MessageAddressTableLookup { + bytes account_key = 1; + bytes writable_indexes = 2; + bytes readonly_indexes = 3; +} + +message TransactionStatusMeta { + TransactionError err = 1; + uint64 fee = 2; + repeated uint64 pre_balances = 3; + repeated uint64 post_balances = 4; + repeated InnerInstructions inner_instructions = 5; + bool inner_instructions_none = 10; + repeated string log_messages = 6; + bool log_messages_none = 11; + repeated TokenBalance pre_token_balances = 7; + repeated TokenBalance post_token_balances = 8; + repeated Reward rewards = 9; + repeated bytes loaded_writable_addresses = 12; + repeated bytes loaded_readonly_addresses = 13; + ReturnData return_data = 14; + bool return_data_none = 15; + + // Sum of compute units consumed by all instructions. + // Available since Solana v1.10.35 / v1.11.6. + // Set to `None` for txs executed on earlier versions. + optional uint64 compute_units_consumed = 16; +} + +message TransactionError { + bytes err = 1; +} + +message InnerInstructions { + uint32 index = 1; + repeated InnerInstruction instructions = 2; +} + +message InnerInstruction { + uint32 program_id_index = 1; + bytes accounts = 2; + bytes data = 3; + + // Invocation stack height of an inner instruction. + // Available since Solana v1.14.6 + // Set to `None` for txs executed on earlier versions. + optional uint32 stack_height = 4; +} + +message CompiledInstruction { + uint32 program_id_index = 1; + bytes accounts = 2; + bytes data = 3; +} + +message TokenBalance { + uint32 account_index = 1; + string mint = 2; + UiTokenAmount ui_token_amount = 3; + string owner = 4; + string program_id = 5; +} + +message UiTokenAmount { + double ui_amount = 1; + uint32 decimals = 2; + string amount = 3; + string ui_amount_string = 4; +} + +message ReturnData { + bytes program_id = 1; + bytes data = 2; +} + +enum RewardType { + Unspecified = 0; + Fee = 1; + Rent = 2; + Staking = 3; + Voting = 4; +} + +message Reward { + string pubkey = 1; + int64 lamports = 2; + uint64 post_balance = 3; + RewardType reward_type = 4; + string commission = 5; +} + +message Rewards { + repeated Reward rewards = 1; + NumPartitions num_partitions = 2; +} + +message UnixTimestamp { + int64 timestamp = 1; +} + +message BlockHeight { + uint64 block_height = 1; +} + +message NumPartitions { + uint64 num_partitions = 1; +} \ No newline at end of file diff --git a/src/monitoring/geyser_event_processor.py b/src/monitoring/geyser_event_processor.py new file mode 100644 index 0000000..cd05009 --- /dev/null +++ b/src/monitoring/geyser_event_processor.py @@ -0,0 +1,92 @@ +""" +Event processing for pump.fun tokens using Geyser data. +""" + +import struct +from typing import Final + +from solders.pubkey import Pubkey + +from trading.base import TokenInfo +from utils.logger import get_logger + +logger = get_logger(__name__) + + +class GeyserEventProcessor: + """Processes token creation events from Geyser stream.""" + + CREATE_DISCRIMINATOR: Final[bytes] = struct.pack(" TokenInfo | None: + """Process transaction data and extract token creation info. + + Args: + instruction_data: Raw instruction data + accounts: List of account indices + keys: List of account public keys + + Returns: + TokenInfo if token creation found, None otherwise + """ + if not instruction_data.startswith(self.CREATE_DISCRIMINATOR): + return None + + try: + # Skip past the 8-byte discriminator + offset = 8 + + # Helper to read strings (prefixed with length) + def read_string(): + nonlocal offset + # Get string length (4-byte uint) + length = struct.unpack_from("= len(accounts): + return None + account_index = accounts[index] + if account_index >= len(keys): + return None + return Pubkey.from_bytes(keys[account_index]) + + name = read_string() + symbol = read_string() + uri = read_string() + + mint = get_account_key(0) + bonding_curve = get_account_key(2) + associated_bonding_curve = get_account_key(3) + user = get_account_key(7) + + 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, + uri=uri, + mint=mint, + bonding_curve=bonding_curve, + associated_bonding_curve=associated_bonding_curve, + user=user, + ) + + except Exception as e: + logger.error(f"Failed to process transaction data: {e}") + return None diff --git a/src/monitoring/geyser_listener.py b/src/monitoring/geyser_listener.py new file mode 100644 index 0000000..8581fe8 --- /dev/null +++ b/src/monitoring/geyser_listener.py @@ -0,0 +1,159 @@ +""" +Geyser monitoring for pump.fun tokens. +""" + +import asyncio +from collections.abc import Awaitable, Callable + +import grpc +from solders.pubkey import Pubkey + +from geyser.generated import geyser_pb2, geyser_pb2_grpc +from monitoring.base_listener import BaseTokenListener +from monitoring.geyser_event_processor import GeyserEventProcessor +from trading.base import TokenInfo +from utils.logger import get_logger + +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, pump_program: Pubkey): + """Initialize token listener. + + Args: + geyser_endpoint: Geyser gRPC endpoint URL + geyser_api_token: API token for authentication + pump_program: Pump.fun program address + """ + self.geyser_endpoint = geyser_endpoint + self.geyser_api_token = geyser_api_token + 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.""" + auth = grpc.metadata_call_credentials( + lambda context, callback: callback( + (("authorization", f"Basic {self.geyser_api_token}"),), None + ) + ) + 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"].failed = False + request.commitment = geyser_pb2.CommitmentLevel.PROCESSED + return request + + 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 using Geyser subscription. + + 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: + 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}") + + 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() + ): + 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 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: + return None + + for ix in msg.instructions: + # Skip non-Pump.fun program instructions + 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 diff --git a/src/trading/trader.py b/src/trading/trader.py index ea306e1..a690c55 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -22,6 +22,7 @@ from core.priority_fee.manager import PriorityFeeManager from core.pubkeys import PumpAddresses from core.wallet import Wallet from monitoring.block_listener import BlockListener +from monitoring.geyser_listener import GeyserListener from monitoring.logs_listener import LogsListener from trading.base import TokenInfo, TradeResult from trading.buyer import TokenBuyer @@ -43,7 +44,9 @@ class PumpTrader: buy_slippage: float, sell_slippage: float, max_retries: int = 5, - listener_type: str = "block", # Add this parameter + listener_type: str = "logs", + geyser_endpoint: str | None = None, + geyser_api_token: str | None = None, ): """Initialize the pump trader. @@ -55,7 +58,9 @@ class PumpTrader: buy_slippage: Slippage tolerance for buys sell_slippage: Slippage tolerance for sells max_retries: Maximum number of retry attempts - listener_type: Type of listener to use ('block' or 'logs') + listener_type: Type of listener to use ('logs', 'blocks', or 'geyser') + geyser_endpoint: Geyser endpoint URL (required for geyser listener) + geyser_api_token: Geyser API token (required for geyser listener) """ self.solana_client = SolanaClient(rpc_endpoint) self.wallet = Wallet(private_key) @@ -92,7 +97,18 @@ class PumpTrader: ) # Initialize the appropriate listener type - if listener_type.lower() == "logs": + listener_type = listener_type.lower() + if listener_type == "geyser": + if not geyser_endpoint or not geyser_api_token: + raise ValueError("Geyser endpoint and API token are required for geyser listener") + + self.token_listener = GeyserListener( + geyser_endpoint, + geyser_api_token, + PumpAddresses.PROGRAM + ) + logger.info("Using Geyser listener for token monitoring") + elif listener_type == "logs": self.token_listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM) logger.info("Using logsSubscribe listener for token monitoring") else: diff --git a/tests/compare_listeners.py b/tests/compare_listeners.py index 809b7c7..2c6b0bd 100644 --- a/tests/compare_listeners.py +++ b/tests/compare_listeners.py @@ -1,6 +1,6 @@ """ -Test script to compare BlockListener and LogsListener -Runs both listeners simultaneously to compare their performance +Test script to compare BlockListener, LogsListener, and GeyserListener +Runs all listeners simultaneously to compare their performance """ import asyncio @@ -10,13 +10,18 @@ import sys import time from pathlib import Path +from dotenv import load_dotenv + sys.path.append(str(Path(__file__).parent.parent / "src")) from core.pubkeys import PumpAddresses from monitoring.block_listener import BlockListener +from monitoring.geyser_listener import GeyserListener from monitoring.logs_listener import LogsListener from trading.base import TokenInfo +load_dotenv() + logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) @@ -46,9 +51,30 @@ class TimingTokenCallback: print(f"{'=' * 50}\n") +async def listen_with_timeout(listener, callback, timeout): + """Run a listener for a specified duration""" + try: + listen_task = asyncio.create_task( + listener.listen_for_tokens(callback.on_token_created) + ) + + await asyncio.sleep(timeout) + + listen_task.cancel() + try: + await listen_task + except asyncio.CancelledError: + pass + except Exception as e: + logger.error(f"Error in listener {callback.name}: {e}") + + async def run_comparison(test_duration: int = 300): - """Run both listeners and compare their performance""" + """Run all listeners and compare their performance""" wss_endpoint = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") + geyser_endpoint = os.environ.get("GEYSER_ENDPOINT") + geyser_api_token = os.environ.get("GEYSER_API_TOKEN") + if not wss_endpoint: logger.error("SOLANA_NODE_WSS_ENDPOINT environment variable is not set") return @@ -57,70 +83,115 @@ async def run_comparison(test_duration: int = 300): block_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM) logs_listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM) - + block_callback = TimingTokenCallback("BlockListener") logs_callback = TimingTokenCallback("LogsListener") - logger.info("Starting both listeners...") - block_task = asyncio.create_task( - block_listener.listen_for_tokens(block_callback.on_token_created) - ) - logs_task = asyncio.create_task( - logs_listener.listen_for_tokens(logs_callback.on_token_created) - ) + listener_tasks = [ + listen_with_timeout(block_listener, block_callback, test_duration), + listen_with_timeout(logs_listener, logs_callback, test_duration) + ] + + callbacks = [block_callback, logs_callback] + listener_names = ["BlockListener", "LogsListener"] + + # Initialize Geyser listener if credentials are available + if geyser_endpoint and geyser_api_token: + logger.info(f"Connecting to Geyser API: {geyser_endpoint}") + geyser_listener = GeyserListener(geyser_endpoint, geyser_api_token, PumpAddresses.PROGRAM) + geyser_callback = TimingTokenCallback("GeyserListener") + + listener_tasks.append( + listen_with_timeout(geyser_listener, geyser_callback, test_duration) + ) + + callbacks.append(geyser_callback) + listener_names.append("GeyserListener") + else: + logger.warning("Geyser API credentials not found. Running without Geyser listener.") + logger.info("Starting all listeners simultaneously...") logger.info(f"Comparison running for {test_duration} seconds...") + try: - await asyncio.sleep(test_duration) + # Start all listeners at the same time + start_time = time.time() + await asyncio.gather(*listener_tasks) + end_time = time.time() + + logger.info(f"Test completed in {end_time - start_time:.2f} seconds") + except KeyboardInterrupt: logger.info("Test interrupted by user") - finally: - block_task.cancel() - logs_task.cancel() - try: - await asyncio.gather(block_task, logs_task, return_exceptions=True) - except asyncio.CancelledError: - pass + # No need for explicit cancellation as gather() will be interrupted - logger.info(f"BlockListener detected {len(block_callback.detected_tokens)} tokens") - logger.info(f"LogsListener detected {len(logs_callback.detected_tokens)} tokens") + for i, callback in enumerate(callbacks): + logger.info(f"{listener_names[i]} detected {len(callback.detected_tokens)} tokens") - # Find tokens detected by both listeners - block_mints = {str(token.mint) for token in block_callback.detected_tokens} - logs_mints = {str(token.mint) for token in logs_callback.detected_tokens} - common_mints = block_mints.intersection(logs_mints) + # Find tokens detected by multiple listeners + all_mints = {} + for i, callback in enumerate(callbacks): + mints = {str(token.mint) for token in callback.detected_tokens} + all_mints[listener_names[i]] = mints - logger.info(f"Tokens detected by both listeners: {len(common_mints)}") + # Analyze common detections between all listeners + if len(callbacks) > 1: + logger.info("\nAnalyzing token detection across listeners:") + + # Find tokens detected by all listeners + if len(callbacks) > 2: # If we have all 3 listeners + common_to_all = set.intersection(*all_mints.values()) + logger.info(f"Tokens detected by all listeners: {len(common_to_all)}") + + # Compare pairs of listeners + listeners = list(all_mints.keys()) + for i in range(len(listeners)): + for j in range(i+1, len(listeners)): + listener1 = listeners[i] + listener2 = listeners[j] + common = all_mints[listener1].intersection(all_mints[listener2]) + logger.info(f"Tokens detected by both {listener1} and {listener2}: {len(common)}") + + unique1 = all_mints[listener1] - all_mints[listener2] + unique2 = all_mints[listener2] - all_mints[listener1] + logger.info(f"Tokens unique to {listener1}: {len(unique1)}") + logger.info(f"Tokens unique to {listener2}: {len(unique2)}") + + # Find tokens detected by at least one listener + all_detected = set.union(*all_mints.values()) + logger.info(f"Total unique tokens detected by any listener: {len(all_detected)}") - # Compare detection times for common tokens - if common_mints: - logger.info("\nPerformance comparison for tokens detected by both listeners:") - logger.info("Token Mint | BlockListener Time | LogsListener Time | Difference (ms)") + logger.info("\nDetection speed comparison:") + + # Collect all tokens detected by at least two listeners + detection_comparisons = [] + for mint in set.union(*all_mints.values()): + detections = {} + for i, callback in enumerate(callbacks): + if mint in callback.detection_times: + detections[listener_names[i]] = callback.detection_times[mint] + + if len(detections) > 1: # Only consider tokens detected by multiple listeners + detection_comparisons.append((mint, detections)) + + if detection_comparisons: + logger.info("Token | " + " | ".join(listener_names) + " | Fastest") logger.info("-" * 80) - for mint in common_mints: - block_time = block_callback.detection_times.get(mint) - logs_time = logs_callback.detection_times.get(mint) + for mint, detections in detection_comparisons: + # Create row with detection times or "N/A" if not detected + times = [] + for name in listener_names: + time_str = f"{detections.get(name, 0):.6f}" if name in detections else "N/A" + times.append(time_str) - if block_time and logs_time: - diff_ms = abs(block_time - logs_time) * 1000 # Convert to milliseconds - faster = "BlockListener" if block_time < logs_time else "LogsListener" - - logger.info(f"{mint[:10]}... | {block_time:.6f} | {logs_time:.6f} | {diff_ms:.2f}ms ({faster} faster)") - - # Report tokens only detected by one listener - block_only = block_mints - logs_mints - logs_only = logs_mints - block_mints - - if block_only: - logger.info(f"\nTokens only detected by BlockListener: {len(block_only)}") - for mint in block_only: - logger.info(f" - {mint}") - - if logs_only: - logger.info(f"\nTokens only detected by LogsListener: {len(logs_only)}") - for mint in logs_only: - logger.info(f" - {mint}") + # Determine fastest listener + valid_times = {name: time for name, time in detections.items() if time > 0} + fastest = min(valid_times.items(), key=lambda x: x[1])[0] if valid_times else "N/A" + + logger.info(f"{mint[:10]}... | " + " | ".join(times) + f" | {fastest}") + else: + logger.info("No tokens were detected by multiple listeners for timing comparison") if __name__ == "__main__": diff --git a/tests/test_block_listener.py b/tests/test_block_listener.py index 9648f21..1be17f8 100644 --- a/tests/test_block_listener.py +++ b/tests/test_block_listener.py @@ -9,12 +9,16 @@ import os import sys from pathlib import Path +from dotenv import load_dotenv + sys.path.append(str(Path(__file__).parent.parent / "src")) from core.pubkeys import PumpAddresses from monitoring.block_listener import BlockListener from trading.base import TokenInfo +load_dotenv() + logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) @@ -91,7 +95,7 @@ async def test_block_listener( if __name__ == "__main__": match_string = None # Update if you want to filter tokens by name/symbol creator_address = None # Update if you want to filter tokens by creator address - test_duration = 15 + test_duration = 30 logger.info("Starting block listener test (using blockSubscribe)") asyncio.run(test_block_listener(match_string, creator_address, test_duration)) diff --git a/tests/test_geyser_listener.py b/tests/test_geyser_listener.py new file mode 100644 index 0000000..7c9e030 --- /dev/null +++ b/tests/test_geyser_listener.py @@ -0,0 +1,107 @@ +""" +Test script for GeyserListener +Tests gRPC monitoring for new pump.fun tokens using Geyser +""" + +import asyncio +import logging +import os +import sys +from pathlib import Path + +from dotenv import load_dotenv + +sys.path.append(str(Path(__file__).parent.parent / "src")) + +from core.pubkeys import PumpAddresses +from monitoring.geyser_listener import GeyserListener +from trading.base import TokenInfo + +load_dotenv() + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("geyser-listener-test") + + +class TestTokenCallback: + def __init__(self): + self.detected_tokens = [] + + async def on_token_created(self, token_info: TokenInfo) -> None: + """Process detected token""" + logger.info(f"New token detected: {token_info.name} ({token_info.symbol})") + logger.info(f"Mint: {token_info.mint}") + self.detected_tokens.append(token_info) + print(f"\n{'=' * 50}") + print(f"NEW TOKEN: {token_info.name}") + print(f"Symbol: {token_info.symbol}") + print(f"Mint: {token_info.mint}") + print(f"URI: {token_info.uri}") + print(f"Creator: {token_info.user}") + print(f"Bonding Curve: {token_info.bonding_curve}") + print(f"Associated Bonding Curve: {token_info.associated_bonding_curve}") + print(f"{'=' * 50}\n") + + +async def test_geyser_listener( + match_string: str | None = None, + creator_address: str | None = None, + test_duration: int = 60, +): + """Test the Geyser listener functionality""" + geyser_endpoint = os.environ.get("GEYSER_ENDPOINT") + geyser_api_token = os.environ.get("GEYSER_API_TOKEN") + + if not geyser_endpoint: + logger.error("GEYSER_ENDPOINT environment variable is not set") + return [] + + if not geyser_api_token: + logger.error("GEYSER_API_TOKEN environment variable is not set") + return [] + + logger.info(f"Connecting to Geyser API: {geyser_endpoint}") + listener = GeyserListener(geyser_endpoint, geyser_api_token, PumpAddresses.PROGRAM) + callback = TestTokenCallback() + + if match_string: + logger.info(f"Filtering tokens matching: {match_string}") + if creator_address: + logger.info(f"Filtering tokens by creator: {creator_address}") + + listen_task = asyncio.create_task( + listener.listen_for_tokens( + callback.on_token_created, + match_string=match_string, + creator_address=creator_address, + ) + ) + + logger.info(f"Listening for {test_duration} seconds...") + try: + await asyncio.sleep(test_duration) + except KeyboardInterrupt: + logger.info("Test interrupted by user") + finally: + listen_task.cancel() + try: + await listen_task + except asyncio.CancelledError: + pass + + logger.info(f"Detected {len(callback.detected_tokens)} tokens") + for token in callback.detected_tokens: + logger.info(f" - {token.name} ({token.symbol}): {token.mint}") + + return callback.detected_tokens + + +if __name__ == "__main__": + match_string = None # Update if you want to filter tokens by name/symbol + creator_address = None # Update if you want to filter tokens by creator address + test_duration = 30 + + logger.info("Starting Geyser listener test (using Geyser API)") + asyncio.run(test_geyser_listener(match_string, creator_address, test_duration)) diff --git a/tests/test_logs_listener.py b/tests/test_logs_listener.py index 8dcc05f..e27e1b2 100644 --- a/tests/test_logs_listener.py +++ b/tests/test_logs_listener.py @@ -9,12 +9,16 @@ import os import sys from pathlib import Path +from dotenv import load_dotenv + sys.path.append(str(Path(__file__).parent.parent / "src")) from core.pubkeys import PumpAddresses from monitoring.logs_listener import LogsListener from trading.base import TokenInfo +load_dotenv() + logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) @@ -91,7 +95,7 @@ async def test_logs_listener( if __name__ == "__main__": match_string = None # Update if you want to filter tokens by name/symbol creator_address = None # Update if you want to filter tokens by creator address - test_duration = 15 + test_duration = 30 logger.info("Starting logs listener test (using logsSubscribe)") asyncio.run(test_logs_listener(match_string, creator_address, test_duration)) From 46e0862b300f1a95f44bce0e6622f82e9a23315a Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 15 Apr 2025 07:02:37 +0000 Subject: [PATCH 58/65] fix: load env vars in cli.py --- src/cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cli.py b/src/cli.py index 3f35bc0..7d7d5ee 100644 --- a/src/cli.py +++ b/src/cli.py @@ -8,10 +8,14 @@ import asyncio import os import sys +from dotenv import load_dotenv + import config from trading.trader import PumpTrader from utils.logger import get_logger, setup_file_logging +load_dotenv() + logger = get_logger(__name__) From 188f43f4cd84b6bb40383f293d6af45a59f24ab6 Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Tue, 15 Apr 2025 09:05:31 +0200 Subject: [PATCH 59/65] docs: Geyser listener completed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ae9317e..0b3ff6f 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ We assume no responsibility for the code or its usage. This is our public servic | | Copy trading | Enable copy trading functionality | Not started | | | Token analysis script | Script for basic token analysis (market cap, creator investment, liquidity, token age) | Not started | | | Archive node integration | Use Solana archive nodes for historical analysis (accounts that consistently print tokens, average mint to raydium time) | Not started | -| | Geyser implementation | Leverage Solana Geyser for real-time data stream processing | Not started | +| | Geyser implementation | Leverage Solana Geyser for real-time data stream processing | ✅ | | **Stage 4: Minting experience** | Token minting | Ability to mint tokens (based on user request - someone minted 18k tokens) | FAFO | From 81c2af4af1c71bd7cc275d6683477faf4c225861 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Mon, 21 Apr 2025 11:36:09 +0000 Subject: [PATCH 60/65] feat: add example of programSubscribe listener for migrations, group examples into subfolders --- .../get_bonding_curve_status.py} | 90 ++- .../get_graduating_tokens.py | 90 +-- .../poll_bonding_curve_progress.py | 135 ++++ .../compare_migration_listeners.py | 637 ++++++++++++++++++ .../listen_blocksubscribe_old_raydium.py} | 10 +- .../listen_logsubscribe.py} | 35 +- .../listen_programsubscribe.py | 169 +++++ .../generated/__init__.py | 0 .../generated/geyser_pb2.py | 0 .../generated/geyser_pb2.pyi | 0 .../generated/geyser_pb2_grpc.py | 0 .../generated/solana_storage_pb2.py | 0 .../generated/solana_storage_pb2.pyi | 0 .../generated/solana_storage_pb2_grpc.py | 0 .../listen_blocksubscribe.py} | 20 +- .../listen_geyser.py} | 15 +- .../listen_logsubscribe+abc.py} | 47 +- .../listen_logsubscribe.py} | 40 +- .../listen_pumpportal.py} | 4 + .../proto/geyser.proto | 0 .../proto/solana-storage.proto | 0 .../{ => pumpswap}/get_pumpswap_pools.py | 6 + .../{ => pumpswap}/manual_sell_pumpswap.py | 9 + .../track_bonding_curve_progress.py | 96 --- 24 files changed, 1148 insertions(+), 255 deletions(-) rename learning-examples/{check_boding_curve_status.py => bonding-curve-progress/get_bonding_curve_status.py} (52%) rename learning-examples/{ => bonding-curve-progress}/get_graduating_tokens.py (59%) create mode 100644 learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py create mode 100644 learning-examples/listen-migrations/compare_migration_listeners.py rename learning-examples/{listen_to_raydium_migration.py => listen-migrations/listen_blocksubscribe_old_raydium.py} (95%) rename learning-examples/{listen_to_logs_migration.py => listen-migrations/listen_logsubscribe.py} (83%) create mode 100644 learning-examples/listen-migrations/listen_programsubscribe.py rename learning-examples/{ => listen-new-tokens}/generated/__init__.py (100%) rename learning-examples/{ => listen-new-tokens}/generated/geyser_pb2.py (100%) rename learning-examples/{ => listen-new-tokens}/generated/geyser_pb2.pyi (100%) rename learning-examples/{ => listen-new-tokens}/generated/geyser_pb2_grpc.py (100%) rename learning-examples/{ => listen-new-tokens}/generated/solana_storage_pb2.py (100%) rename learning-examples/{ => listen-new-tokens}/generated/solana_storage_pb2.pyi (100%) rename learning-examples/{ => listen-new-tokens}/generated/solana_storage_pb2_grpc.py (100%) rename learning-examples/{listen_create_from_blocksubscribe.py => listen-new-tokens/listen_blocksubscribe.py} (91%) rename learning-examples/{listen_create_from_geyser.py => listen-new-tokens/listen_geyser.py} (86%) rename learning-examples/{listen_new_direct_full_details.py => listen-new-tokens/listen_logsubscribe+abc.py} (86%) rename learning-examples/{listen_new_direct.py => listen-new-tokens/listen_logsubscribe.py} (84%) rename learning-examples/{listen_new_portal.py => listen-new-tokens/listen_pumpportal.py} (97%) rename learning-examples/{ => listen-new-tokens}/proto/geyser.proto (100%) rename learning-examples/{ => listen-new-tokens}/proto/solana-storage.proto (100%) rename learning-examples/{ => pumpswap}/get_pumpswap_pools.py (94%) rename learning-examples/{ => pumpswap}/manual_sell_pumpswap.py (97%) delete mode 100644 learning-examples/track_bonding_curve_progress.py diff --git a/learning-examples/check_boding_curve_status.py b/learning-examples/bonding-curve-progress/get_bonding_curve_status.py similarity index 52% rename from learning-examples/check_boding_curve_status.py rename to learning-examples/bonding-curve-progress/get_bonding_curve_status.py index d66fafa..29974f3 100644 --- a/learning-examples/check_boding_curve_status.py +++ b/learning-examples/bonding-curve-progress/get_bonding_curve_status.py @@ -1,25 +1,42 @@ -import argparse +""" +Module for checking the status of a token's bonding curve on the Solana network using +the Pump.fun program. It allows querying the bonding curve state and completion status. +""" + import asyncio import os import struct -import sys from typing import Final from construct import Flag, Int64ul, Struct +from dotenv import load_dotenv from solana.rpc.async_api import AsyncClient from solders.pubkey import Pubkey -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) - -from core.pubkeys import PumpAddresses - -# Constants -EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" tuple[Pubkey, int]: """ - Derives the associated bonding curve address for a given mint + Derives the associated bonding curve address for a given mint. + + Args: + mint: The token mint address + program_id: The program ID for the bonding curve + + Returns: + Tuple of (bonding curve address, bump seed) """ return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], program_id) @@ -46,6 +70,19 @@ def get_associated_bonding_curve_address( async def get_bonding_curve_state( conn: AsyncClient, curve_address: Pubkey ) -> BondingCurveState: + """ + Fetches and validates the state of a bonding curve account. + + Args: + conn: AsyncClient connection to Solana RPC + curve_address: Address of the bonding curve account + + Returns: + BondingCurveState object containing parsed account data + + Raises: + ValueError: If account data is invalid or missing + """ 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") @@ -58,19 +95,25 @@ async def get_bonding_curve_state( async def check_token_status(mint_address: str) -> None: + """ + Checks and prints the status of a token and its bonding curve. + + Args: + mint_address: The token mint address as a string + """ try: mint = Pubkey.from_string(mint_address) # Get the associated bonding curve address bonding_curve_address, bump = get_associated_bonding_curve_address( - mint, PumpAddresses.PROGRAM + mint, PUMP_PROGRAM_ID ) - print("\nToken Status:") + print("\nToken status:") print("-" * 50) - print(f"Token Mint: {mint}") - print(f"Associated Bonding Curve: {bonding_curve_address}") - print(f"Bump Seed: {bump}") + print(f"Token mint: {mint}") + print(f"Associated bonding curve: {bonding_curve_address}") + print(f"Bump seed: {bump}") print("-" * 50) # Check completion status @@ -80,14 +123,14 @@ async def check_token_status(mint_address: str) -> None: client, bonding_curve_address ) - print("\nBonding Curve Status:") + print("\nBonding curve status:") print("-" * 50) print( - f"Completion Status: {'Completed' if curve_state.complete else 'Not Completed'}" + f"Completion status: {'Completed' if curve_state.complete else 'Not completed'}" ) if curve_state.complete: print( - "\nNote: This bonding curve has completed and liquidity has been migrated to Raydium." + "\nNote: This bonding curve has completed and liquidity has been migrated to PumpSwap." ) print("-" * 50) @@ -100,12 +143,13 @@ async def check_token_status(mint_address: str) -> None: print(f"\nUnexpected error: {e}") -def main(): - parser = argparse.ArgumentParser(description="Check token bonding curve status") - parser.add_argument("mint_address", help="The token mint address") - - args = parser.parse_args() - asyncio.run(check_token_status(args.mint_address)) +def main() -> None: + """Main entry point for the token status checker.""" + #parser = argparse.ArgumentParser(description="Check token bonding curve status") + #parser.add_argument("mint_address", help="The token mint address" + #args = parser.parse_args() + + asyncio.run(check_token_status(TOKEN_MINT)) if __name__ == "__main__": diff --git a/learning-examples/get_graduating_tokens.py b/learning-examples/bonding-curve-progress/get_graduating_tokens.py similarity index 59% rename from learning-examples/get_graduating_tokens.py rename to learning-examples/bonding-curve-progress/get_graduating_tokens.py index df72be3..e6d2540 100644 --- a/learning-examples/get_graduating_tokens.py +++ b/learning-examples/bonding-curve-progress/get_graduating_tokens.py @@ -1,6 +1,15 @@ +""" +Module for querying and analyzing soon-to-gradute tokens in the Pump.fun program. +It includes functionality to fetch bonding curves based on token reserves and +find associated SPL token accounts. + +Note: getProgramAccounts may be slow as it is a pretty heavy method for RPC. +""" + import asyncio import os import struct +from typing import Final from dotenv import load_dotenv from solana.rpc.async_api import AsyncClient @@ -9,19 +18,18 @@ from solders.pubkey import Pubkey load_dotenv() -RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT1") -PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") -TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") +# Constants +RPC_ENDPOINT: Final[str] = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") +PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") +TOKEN_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") # The 8-byte discriminator for bonding curve accounts in Pump.fun -# Used to identify account types in getProgramAccounts requests -BONDING_CURVE_DISCRIMINATOR_BYTES = bytes.fromhex("17b7f83760d8ac60") +BONDING_CURVE_DISCRIMINATOR_BYTES: Final[bytes] = bytes.fromhex("17b7f83760d8ac60") async def get_bonding_curves_by_reserves(client: AsyncClient | None = None) -> list: """ - Fetch bonding curve accounts from the Pump.fun program that have real_token_reserves - below a defined threshold and are not marked as complete (i.e., not migrated). + Fetch bonding curve accounts with real token reserves below a threshold. Args: client: Optional AsyncClient instance. If None, a new one will be created. @@ -29,31 +37,22 @@ async def get_bonding_curves_by_reserves(client: AsyncClient | None = None) -> l Returns: List of bonding curve accounts matching the criteria """ - # Define the reserve threshold we're interested in - # 100 trillion (in token base units) - threshold = 100_000_000_000_000 - - # Convert the threshold into 8-byte little-endian format (as stored on-chain) - threshold_bytes = threshold.to_bytes(8, 'little') - - # Extract the 2 most significant bytes to pre-filter values less than 2^48 (~281T) - # This optimization reduces the number of accounts returned by the RPC - msb_prefix = threshold_bytes[6:] + # Define the reserve threshold (100 trillion in token base units) + threshold: int = 100_000_000_000_000 + threshold_bytes: bytes = threshold.to_bytes(8, "little") + msb_prefix: bytes = threshold_bytes[6:] # Most significant bytes for pre-filtering - should_close_client = client is None + should_close_client: bool = client is None try: if should_close_client: - client = AsyncClient(RPC_ENDPOINT, commitment="processed", timeout=120) + client = AsyncClient(RPC_ENDPOINT, commitment="processed", timeout=180) await client.is_connected() # Define on-chain filters for getProgramAccounts filters = [ - # Match only bonding curve accounts - MemcmpOpts(offset=0, bytes=BONDING_CURVE_DISCRIMINATOR_BYTES), - # Real token reserves MSB bytes (pre-filter) - MemcmpOpts(offset=30, bytes=msb_prefix), - # Complete flag is False (not migrated) - MemcmpOpts(offset=48, bytes=b'\x00'), + MemcmpOpts(offset=0, bytes=BONDING_CURVE_DISCRIMINATOR_BYTES), # Match bonding curve accounts + MemcmpOpts(offset=30, bytes=msb_prefix), # Pre-filter by real token reserves MSB + MemcmpOpts(offset=48, bytes=b"\x00"), # Ensure complete flag is False ] # Query accounts matching filters @@ -67,26 +66,15 @@ async def get_bonding_curves_by_reserves(client: AsyncClient | None = None) -> l for acc in response.value: raw = acc.account.data - # Parse account data according to the Pump.fun bonding curve layout: - # [8] discriminator - # [8] virtual_token_reserves (u64) - # [8] virtual_sol_reserves (u64) - # [8] real_token_reserves (u64) - # [8] real_sol_reserves (u64) - # [8] token_total_supply (u64) - # [1] complete (bool) - - # Skip to real_token_reserves field (8 + 8 + 8 = 24 bytes offset) - offset = 24 - # Extract real_token_reserves (u64 = 8 bytes, little-endian) - real_token_reserves = struct.unpack(" l await client.close() -async def find_associated_bonding_curve(bonding_curve_address: str, client: AsyncClient | None = None): +async def find_associated_bonding_curve( + bonding_curve_address: str, client: AsyncClient | None = None +) -> dict | None: """ Find the SPL token account owned by a bonding curve. - A bonding curve typically owns exactly one SPL token account that represents - the token being traded through the curve. - Args: bonding_curve_address: The bonding curve public key (as a string) client: Optional AsyncClient instance. If None, a new one will be created. @@ -109,7 +96,7 @@ async def find_associated_bonding_curve(bonding_curve_address: str, client: Asyn Returns: The associated SPL token account data or None if not found """ - should_close_client = client is None + should_close_client: bool = client is None try: if should_close_client: client = AsyncClient(RPC_ENDPOINT) @@ -137,26 +124,23 @@ def get_mint_address(data: bytes) -> str: """ Extract the mint address from SPL token account data. - In SPL token account data, the mint address is stored in the first 32 bytes. - Args: data: The token account data as bytes Returns: The mint address as a base58-encoded string """ - # The mint address is stored in the first 32 bytes - mint_bytes = data[:32] - return str(Pubkey(mint_bytes)) + return str(Pubkey(data[:32])) -async def main(): +async def main() -> None: + """Main entry point for querying and processing bonding curves.""" async with AsyncClient(RPC_ENDPOINT, commitment="processed", timeout=120) as client: await client.is_connected() bonding_curves = await get_bonding_curves_by_reserves(client) print(f"Total matches: {len(bonding_curves)}") - print("="*50) + print("=" * 50) for bonding_curve in bonding_curves: # Find the SPL token account owned by the bonding curve @@ -168,7 +152,7 @@ async def main(): mint_address = get_mint_address(associated_token_account.data) print(f"Bonding curve: {bonding_curve.pubkey}") print(f"Mint address: {mint_address}") - print("="*50) + print("=" * 50) # For demonstration, only process the first curve break diff --git a/learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py b/learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py new file mode 100644 index 0000000..1ab52c3 --- /dev/null +++ b/learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py @@ -0,0 +1,135 @@ +""" +Module for tracking the progress of a bonding curve for a Pump.fun token. +It continuously polls the bonding curve state and prints updates at regular intervals. +""" + +import asyncio +import os +import struct +from typing import Final + +from dotenv import load_dotenv +from solana.rpc.async_api import AsyncClient +from solders.pubkey import Pubkey + +load_dotenv() + +# Constants +RPC_URL: Final[str] = os.getenv("SOLANA_NODE_RPC_ENDPOINT") +TOKEN_MINT: Final[str] = "xWrzYY4c1LnbSkLrd2LDUg9vw7YtVyJhGmw7MABpump" +PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") +LAMPORTS_PER_SOL: Final[int] = 1_000_000_000 +TOKEN_DECIMALS: Final[int] = 6 +EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" Pubkey: + """ + Derive the bonding curve PDA address from a mint address. + + Args: + mint: The token mint address + program_id: The program ID for the bonding curve + + Returns: + The bonding curve address + """ + return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], program_id)[0] + + +async def get_account_data(client: AsyncClient, pubkey: Pubkey) -> bytes: + """ + Fetch raw account data for a given public key. + + Args: + client: AsyncClient connection to Solana RPC + pubkey: The public key of the account to fetch + + Returns: + The raw account data as bytes + + Raises: + ValueError: If the account is not found or has no data + """ + resp = await client.get_account_info(pubkey, encoding="base64") + if not resp.value or not resp.value.data: + raise ValueError(f"Account {pubkey} not found or has no data") + + return resp.value.data + + +def parse_curve_state(data: bytes) -> dict: + """ + Decode bonding curve account data into a readable format. + + Args: + data: The raw bonding curve account data + + Returns: + A dictionary containing parsed bonding curve fields + + Raises: + ValueError: If the account discriminator is invalid + """ + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid discriminator for bonding curve") + + fields = struct.unpack_from(" None: + """ + Print the current status of the bonding curve in a readable format. + + Args: + state: The parsed bonding curve state dictionary + """ + progress = 0 + if state["token_total_supply"]: + progress = 100 - (100 * state["real_token_reserves"] / state["token_total_supply"]) + + print("=" * 30) + print(f"Complete: {'✅' if state['complete'] else '❌'}") + print(f"Progress: {progress:.2f}%") + print(f"Token reserves: {state['real_token_reserves']:.4f}") + print(f"SOL reserves: {state['real_sol_reserves']:.4f}") + print("=" * 30, "\n") + + +async def track_curve() -> None: + """ + Continuously track and display the state of a bonding curve. + """ + if not RPC_URL or not TOKEN_MINT: + print("❌ Set SOLANA_NODE_RPC_ENDPOINT and TOKEN_MINT in .env") + return + + mint_pubkey: Pubkey = Pubkey.from_string(TOKEN_MINT) + curve_pubkey: Pubkey = get_associated_bonding_curve_address(mint_pubkey, PUMP_PROGRAM_ID) + + print("Tracking bonding curve for:", mint_pubkey) + print("Curve address:", curve_pubkey, "\n") + + async with AsyncClient(RPC_URL) as client: + while True: + try: + data = await get_account_data(client, curve_pubkey) + state = parse_curve_state(data) + print_curve_status(state) + except Exception as e: + print(f"⚠️ Error: {e}") + + await asyncio.sleep(POLL_INTERVAL) + + +if __name__ == "__main__": + asyncio.run(track_curve()) diff --git a/learning-examples/listen-migrations/compare_migration_listeners.py b/learning-examples/listen-migrations/compare_migration_listeners.py new file mode 100644 index 0000000..afe3a4a --- /dev/null +++ b/learning-examples/listen-migrations/compare_migration_listeners.py @@ -0,0 +1,637 @@ +""" +This script compares two methods of detecting migrations: +1. Migration program listener (listens Migration program) - detects markets via successful migration transactions +2. Direct market account listener (listens Pump Fun AMM program aka PumpSwap) - detects markets via program account subscription + +The script tracks which method detects new markets 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 aiohttp +import base58 +import websockets +from dotenv import load_dotenv +from solders.pubkey import Pubkey + +load_dotenv() + +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") +MIGRATION_PROGRAM_ID = Pubkey.from_string("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg") +PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") +QUOTE_MINT_SOL = base58.b58encode(bytes(Pubkey.from_string("So11111111111111111111111111111111111111112"))).decode() + +MARKET_DISCRIMINATOR = base58.b58encode(b'\xf1\x9am\x04\x11\xb1m\xbc').decode() +MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure + + +class DetectionTracker: + """Tracks and analyzes detection times for both methods across providers""" + + def __init__(self): + self.migrations = {} # {base_mint: {provider: timestamp}} + self.markets = {} # {base_mint: {provider: timestamp}} + self.migration_messages = {} # {provider: count} + self.market_messages = {} # {provider: count} + self.start_time = time.time() + + def add_migration(self, base_mint, provider, timestamp): + """Record a migration detection event""" + if base_mint not in self.migrations: + self.migrations[base_mint] = {} + self.migrations[base_mint][provider] = timestamp + print(f"[MIGRATION] base_mint={base_mint} provider={provider} time={timestamp:.3f}") + + def add_market(self, base_mint, provider, timestamp): + """Record a market detection event""" + if base_mint not in self.markets: + self.markets[base_mint] = {} + self.markets[base_mint][provider] = timestamp + print(f"[MARKET] base_mint={base_mint} provider={provider} time={timestamp:.3f}") + + def increment_migration_messages(self, provider): + """Count WebSocket messages received by migration listener""" + if provider not in self.migration_messages: + self.migration_messages[provider] = 0 + self.migration_messages[provider] += 1 + + def increment_market_messages(self, provider): + """Count WebSocket messages received by market listener""" + if provider not in self.market_messages: + self.market_messages[provider] = 0 + self.market_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_migration_messages = sum(self.migration_messages.values()) + total_market_messages = sum(self.market_messages.values()) + + print("\n=== Test Summary ===") + print(f"Test duration: {test_duration:.2f} seconds") + print(f"WebSocket messages received: {total_migration_messages + total_market_messages}") + print(f" - Migration events: {total_migration_messages}") + print(f" - Market events: {total_market_messages}") + + # Count unique tokens detected by each method + unique_migrations = set(self.migrations.keys()) + unique_markets = set(self.markets.keys()) + common_tokens = unique_migrations & unique_markets + + print(f"Tokens detected: {len(unique_migrations | unique_markets)}") + print(f" - Migration events: {len(unique_migrations)}") + print(f" - Market events: {len(unique_markets)}") + print(f" - Detected in both: {len(common_tokens)}\n") + + print("=== Provider Message Counts ===") + print("Provider | Migration Messages | Market Messages | Total Messages") + print("-" * 80) + all_providers = set(self.migration_messages.keys()) | set(self.market_messages.keys()) + for provider in sorted(all_providers): + migration_count = self.migration_messages.get(provider, 0) + market_count = self.market_messages.get(provider, 0) + total = migration_count + market_count + print(f"{provider:<22} | {migration_count:<18} | {market_count:<14} | {total}") + print() + + print("=== Migration Event Provider Performance ===") + self._print_provider_performance(self.migrations) + + print("\n=== Market Event Provider Performance ===") + self._print_provider_performance(self.markets) + + # Compare detection methods for tokens detected by both + if common_tokens: + print("\n=== Detection Timing Comparison: Migration vs Market ===") + print("Base Mint | First Detection Method | First Provider | Time Delta (ms)") + print("-" * 100) + + migration_first = 0 + market_first = 0 + total_delta_ms = 0 + + for base_mint in sorted(common_tokens): + # Find earliest time for each method + migration_time = min(self.migrations[base_mint].values()) if base_mint in self.migrations else float('inf') + market_time = min(self.markets[base_mint].values()) if base_mint in self.markets else float('inf') + + # Find provider with earliest time for each method + migration_provider = None + if base_mint in self.migrations: + migration_provider = min(self.migrations[base_mint].items(), key=lambda x: x[1])[0] + + market_provider = None + if base_mint in self.markets: + market_provider = min(self.markets[base_mint].items(), key=lambda x: x[1])[0] + + delta_ms = abs(migration_time - market_time) * 1000 + total_delta_ms += delta_ms + + if migration_time < market_time: + first_method = "Migration" + first_provider = migration_provider + migration_first += 1 + else: + first_method = "Market" + first_provider = market_provider + market_first += 1 + + print(f"{base_mint} | {first_method:<21} | {first_provider:<14} | {delta_ms:8.1f}") + + # Print statistics summary + if common_tokens: + avg_delta_ms = total_delta_ms / len(common_tokens) + print("\nSummary statistics:") + print(f" - Migration detected first: {migration_first}/{len(common_tokens)} ({migration_first/len(common_tokens)*100:.1f}%)") + print(f" - Market detected first: {market_first}/{len(common_tokens)} ({market_first/len(common_tokens)*100:.1f}%)") + print(f" - Average timing difference: {avg_delta_ms:.1f} ms") + + def _print_provider_performance(self, events_dict): + """Print performance metrics for providers using a specific detection method""" + # Count how many times each provider was first + first_count = {} + total_events = 0 + + for base_mint, providers in events_dict.items(): + total_events += 1 + if not providers: + continue + + # Find the fastest provider for this event + fastest_provider = min(providers.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 events 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_events) * 100 if total_events > 0 else 0 + print(f"{provider:<22} | {count:<16} | {percentage:.1f}%") + + # Calculate average latency between providers + self._print_provider_latency_matrix(events_dict) + + def _print_provider_latency_matrix(self, events_dict): + """Print a matrix of average latency between providers""" + # Get unique providers + all_providers = set() + for providers_data in events_dict.values(): + all_providers.update(providers_data.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 base_mint, providers_data in events_dict.items(): + if provider1 in providers_data and provider2 in providers_data: + latency_ms = (providers_data[provider2] - providers_data[provider1]) * 1000 + latencies.append(latency_ms) + + if latencies: + avg_latency = sum(latencies) / len(latencies) + row += f" {avg_latency:>+7.1f} |" + else: + row += " ? |" + print(row) + + +# ============ MARKET DETECTION METHODS ============ + +async def fetch_existing_market_pubkeys(): + """ + Fetch existing AMM market accounts from the blockchain + + Used to filter out already existing markets when detecting new ones + """ + headers = {"Content-Type": "application/json"} + body = { + "jsonrpc": "2.0", + "id": 1, + "method": "getProgramAccounts", + "params": [ + str(PUMP_AMM_PROGRAM_ID), + { + "encoding": "base64", + "commitment": "processed", + "filters": [ + {"dataSize": MARKET_ACCOUNT_LENGTH}, + {"memcmp": {"offset": 0, "bytes": MARKET_DISCRIMINATOR}}, + {"memcmp": {"offset": 75, "bytes": QUOTE_MINT_SOL}} + ] + } + ] + } + + async with aiohttp.ClientSession() as session: + async with session.post(RPC_ENDPOINT, headers=headers, json=body) as resp: + res = await resp.json() + return {account["pubkey"] for account in res.get("result", [])} + + +def parse_market_account_data(data): + """ + Parse binary market account data into a structured format + + This function matches the parser from the market listener script + """ + parsed_data = {} + offset = 8 # Skip discriminator + + fields = [ + ("pool_bump", "u8"), + ("index", "u16"), + ("creator", "pubkey"), + ("base_mint", "pubkey"), + ("quote_mint", "pubkey"), + ("lp_mint", "pubkey"), + ("pool_base_token_account", "pubkey"), + ("pool_quote_token_account", "pubkey"), + ("lp_supply", "u64"), + ] + + try: + for field_name, field_type in fields: + if field_type == "pubkey": + value = data[offset:offset + 32] + parsed_data[field_name] = base58.b58encode(value).decode("utf-8") + offset += 32 + elif field_type in {"u64", "i64"}: + value = struct.unpack(" no parsed data). +To cover those cases, please use an additional RPC call (get transaction data) or additional listener not based on logs. +""" + import asyncio import base64 import json @@ -6,8 +14,11 @@ import struct import base58 import websockets +from dotenv import load_dotenv from solders.pubkey import Pubkey +load_dotenv() + WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") MIGRATION_PROGRAM_ID = Pubkey.from_string("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg") @@ -77,9 +88,8 @@ def is_transaction_successful(logs): def print_transaction_details(log_data): - signature = log_data.get('signature', 'N/A') - print(f"\n[INFO] Transaction Signature: {signature}") logs = log_data.get("logs", []) + parsed_data = {} for log in logs: if log.startswith("Program data:"): @@ -87,16 +97,14 @@ def print_transaction_details(log_data): data = base64.b64decode(log.split(": ")[1]) parsed_data = parse_migrate_instruction(data) if parsed_data: - print("\n[INFO] Parsed Migration Instruction Data:") + print("[INFO] Parsed from Program data:") for key, value in parsed_data.items(): print(f" {key}: {value}") except Exception as e: - print(f"[ERROR] Base64 decode failed: {log}, Error: {e}") + print(f"[ERROR] Failed to decode Program data: {e}") -def print_log_details(logs): - print("\n[INFO] Processing logs:") - for log in logs: - print(f" Log: {log}") + if not parsed_data: + print("[ERROR] Failed to parse migration data: parsed data is empty") async def listen_for_migrations(): @@ -130,16 +138,19 @@ async def listen_for_migrations(): log_data = data["params"]["result"]["value"] logs = log_data.get("logs", []) + signature = log_data.get('signature', 'N/A') + print(f"\n[INFO] Transaction signature: {signature}") + if is_transaction_successful(logs): if not any("Program log: Instruction: Migrate" in log for log in logs): - print("[INFO] Skipping: No Migrate instruction") + print("[INFO] Skipping: no migrate instruction") continue if any("Program log: Bonding curve already migrated" in log for log in logs): - print("[INFO] Skipping: Bonding curve already migrated") + print("[INFO] Skipping: bonding curve already migrated") continue - print("[INFO] Processing migration instruction!") + print("[INFO] Processing migration instruction...") print_transaction_details(log_data) else: print("[INFO] Skipping failed transaction.") @@ -155,4 +166,4 @@ async def listen_for_migrations(): await asyncio.sleep(5) if __name__ == "__main__": - asyncio.run(listen_for_migrations()) + asyncio.run(listen_for_migrations()) \ No newline at end of file diff --git a/learning-examples/listen-migrations/listen_programsubscribe.py b/learning-examples/listen-migrations/listen_programsubscribe.py new file mode 100644 index 0000000..4045703 --- /dev/null +++ b/learning-examples/listen-migrations/listen_programsubscribe.py @@ -0,0 +1,169 @@ +""" +Monitors Solana for new Pump AMM markets via WebSocket. +Fetches existing markets to filter out already existing ones, parses market account data (e.g., mints, token accounts, creator), +and excludes user-created markets. May also detect non-migration-based markets (if they created by a program). + +Note: this method consumes HUGE AMOUNT OF MESSAGES from a WebSocket. +""" + +import asyncio +import base64 +import json +import os +import struct + +import aiohttp +import base58 +import websockets +from dotenv import load_dotenv +from solders.pubkey import Pubkey + +load_dotenv() + +WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") +PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") + +MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure +MARKET_DISCRIMINATOR = base58.b58encode(b'\xf1\x9am\x04\x11\xb1m\xbc').decode() +QUOTE_MINT_SOL = base58.b58encode(bytes(Pubkey.from_string("So11111111111111111111111111111111111111112"))).decode() + + +async def fetch_existing_market_pubkeys(): + headers = {"Content-Type": "application/json"} + body = { + "jsonrpc": "2.0", + "id": 1, + "method": "getProgramAccounts", + "params": [ + str(PUMP_AMM_PROGRAM_ID), + { + "encoding": "base64", + "commitment": "processed", + "filters": [ + {"dataSize": MARKET_ACCOUNT_LENGTH}, + {"memcmp": {"offset": 0, "bytes": MARKET_DISCRIMINATOR}}, + {"memcmp": {"offset": 75, "bytes": QUOTE_MINT_SOL}} + ] + } + ] + } + + async with aiohttp.ClientSession() as session: + async with session.post(RPC_ENDPOINT, headers=headers, json=body) as resp: + res = await resp.json() + return {account["pubkey"] for account in res.get("result", [])} + + +def parse_market_account_data(data): + parsed_data = {} + offset = 8 # Discriminator + + fields = [ + ("pool_bump", "u8"), + ("index", "u16"), + ("creator", "pubkey"), + ("base_mint", "pubkey"), + ("quote_mint", "pubkey"), + ("lp_mint", "pubkey"), + ("pool_base_token_account", "pubkey"), + ("pool_quote_token_account", "pubkey"), + ("lp_supply", "u64"), + ] + + try: + for field_name, field_type in fields: + if field_type == "pubkey": + value = data[offset:offset + 32] + parsed_data[field_name] = base58.b58encode(value).decode("utf-8") + offset += 32 + elif field_type in {"u64", "i64"}: + value = struct.unpack(" Pubkey: """ @@ -23,24 +32,14 @@ def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey derived_address, _ = Pubkey.find_program_address( [ bytes(bonding_curve), - bytes(SystemAddresses.TOKEN_PROGRAM), + bytes(TOKEN_PROGRAM_ID), bytes(mint), ], - SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, + ASSOCIATED_TOKEN_PROGRAM_ID, ) return derived_address -# Load the IDL JSON file -with open("idl/pump_fun_idl.json") as f: - idl = json.load(f) - -# Extract the "create" instruction definition -create_instruction = next( - instr for instr in idl["instructions"] if instr["name"] == "create" -) - - def parse_create_instruction(data): if len(data) < 8: return None @@ -75,18 +74,6 @@ def parse_create_instruction(data): return None -def print_transaction_details(log_data): - print(f"Signature: {log_data.get('signature')}") - - for log in log_data.get("logs", []): - if log.startswith("Program data:"): - try: - data = base58.b58decode(log.split(": ")[1]).decode("utf-8") - print(f"Data: {data}") - except: - pass - - async def listen_for_new_tokens(): while True: try: @@ -97,14 +84,14 @@ async def listen_for_new_tokens(): "id": 1, "method": "logsSubscribe", "params": [ - {"mentions": [str(PumpAddresses.PROGRAM)]}, + {"mentions": [str(PUMP_PROGRAM_ID)]}, {"commitment": "processed"}, ], } ) await websocket.send(subscription_message) print( - f"Listening for new token creations from program: {PumpAddresses.PROGRAM}" + f"Listening for new token creations from program: {PUMP_PROGRAM_ID}" ) # Wait for subscription confirmation diff --git a/learning-examples/listen_new_direct.py b/learning-examples/listen-new-tokens/listen_logsubscribe.py similarity index 84% rename from learning-examples/listen_new_direct.py rename to learning-examples/listen-new-tokens/listen_logsubscribe.py index 00962de..6d9506a 100644 --- a/learning-examples/listen_new_direct.py +++ b/learning-examples/listen-new-tokens/listen_logsubscribe.py @@ -1,27 +1,25 @@ +""" +Listens for new Pump.fun token creations via Solana WebSocket. +Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.). + +It is usually faster than blockSubscribe, but slower than Geyser. +""" + import asyncio import base64 import json import os import struct -import sys import base58 import websockets +from dotenv import load_dotenv +from solders.pubkey import Pubkey -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) -from core.pubkeys import PumpAddresses +load_dotenv() WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") - - -# Load the IDL JSON file -with open("idl/pump_fun_idl.json") as f: - idl = json.load(f) - -# Extract the "create" instruction definition -create_instruction = next( - instr for instr in idl["instructions"] if instr["name"] == "create" -) +PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") def parse_create_instruction(data): @@ -58,18 +56,6 @@ def parse_create_instruction(data): return None -def print_transaction_details(log_data): - print(f"Signature: {log_data.get('signature')}") - - for log in log_data.get("logs", []): - if log.startswith("Program data:"): - try: - data = base58.b58decode(log.split(": ")[1]).decode("utf-8") - print(f"Data: {data}") - except: - pass - - async def listen_for_new_tokens(): while True: try: @@ -80,14 +66,14 @@ async def listen_for_new_tokens(): "id": 1, "method": "logsSubscribe", "params": [ - {"mentions": [str(PumpAddresses.PROGRAM)]}, + {"mentions": [str(PUMP_PROGRAM_ID)]}, {"commitment": "processed"}, ], } ) await websocket.send(subscription_message) print( - f"Listening for new token creations from program: {PumpAddresses.PROGRAM}" + f"Listening for new token creations from program: {PUMP_PROGRAM_ID}" ) # Wait for subscription confirmation diff --git a/learning-examples/listen_new_portal.py b/learning-examples/listen-new-tokens/listen_pumpportal.py similarity index 97% rename from learning-examples/listen_new_portal.py rename to learning-examples/listen-new-tokens/listen_pumpportal.py index 3817a8d..0bb5c72 100644 --- a/learning-examples/listen_new_portal.py +++ b/learning-examples/listen-new-tokens/listen_pumpportal.py @@ -1,3 +1,7 @@ +""" +Listens for new Pump.fun token creations via PumpPortal WebSocket. +""" + import asyncio import json from datetime import datetime diff --git a/learning-examples/proto/geyser.proto b/learning-examples/listen-new-tokens/proto/geyser.proto similarity index 100% rename from learning-examples/proto/geyser.proto rename to learning-examples/listen-new-tokens/proto/geyser.proto diff --git a/learning-examples/proto/solana-storage.proto b/learning-examples/listen-new-tokens/proto/solana-storage.proto similarity index 100% rename from learning-examples/proto/solana-storage.proto rename to learning-examples/listen-new-tokens/proto/solana-storage.proto diff --git a/learning-examples/get_pumpswap_pools.py b/learning-examples/pumpswap/get_pumpswap_pools.py similarity index 94% rename from learning-examples/get_pumpswap_pools.py rename to learning-examples/pumpswap/get_pumpswap_pools.py index 8b511d1..6578367 100644 --- a/learning-examples/get_pumpswap_pools.py +++ b/learning-examples/pumpswap/get_pumpswap_pools.py @@ -1,3 +1,9 @@ +""" +This module provides functionality to: +1. Find market addresses by base mint +2. Fetch and parse market data (including pool addresses) from Pump AMM program accounts +""" + import asyncio import os import struct diff --git a/learning-examples/manual_sell_pumpswap.py b/learning-examples/pumpswap/manual_sell_pumpswap.py similarity index 97% rename from learning-examples/manual_sell_pumpswap.py rename to learning-examples/pumpswap/manual_sell_pumpswap.py index 566cc56..0340264 100644 --- a/learning-examples/manual_sell_pumpswap.py +++ b/learning-examples/pumpswap/manual_sell_pumpswap.py @@ -1,3 +1,12 @@ +""" +This module provides functionality to: +- Find market addresses by token mint. +- Fetch and parse market data from PUMP AMM pools. +- Calculate token prices in AMM pools. +- Create associated token accounts (ATAs) idempotently. +- Sell tokens on the PUMP AMM with slippage protection. +""" + import asyncio import os import struct diff --git a/learning-examples/track_bonding_curve_progress.py b/learning-examples/track_bonding_curve_progress.py deleted file mode 100644 index 11933c1..0000000 --- a/learning-examples/track_bonding_curve_progress.py +++ /dev/null @@ -1,96 +0,0 @@ -""" -Track bonding curve progress for a pump.fun token by mint address. -""" - -import asyncio -import os -import struct - -from dotenv import load_dotenv -from solana.rpc.async_api import AsyncClient -from solders.pubkey import Pubkey - -# Import pump.fun program address -from core.pubkeys import PumpAddresses - -load_dotenv() - -RPC_URL = os.getenv("SOLANA_NODE_RPC_ENDPOINT") -TOKEN_MINT = "xWrzYY4c1LnbSkLrd2LDUg9vw7YtVyJhGmw7MABpump" - -LAMPORTS_PER_SOL = 1_000_000_000 -TOKEN_DECIMALS = 6 -EXPECTED_DISCRIMINATOR = struct.pack(" Pubkey: - """Derive the bonding curve PDA address from mint.""" - return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], program_id)[0] - - -async def get_account_data(client: AsyncClient, pubkey: Pubkey) -> bytes: - """Fetch raw account data for a given public key.""" - resp = await client.get_account_info(pubkey, encoding="base64") - if not resp.value or not resp.value.data: - raise ValueError(f"Account {pubkey} not found or has no data") - - return resp.value.data - - -def parse_curve_state(data: bytes) -> dict: - """Decode bonding curve account data.""" - if data[:8] != EXPECTED_DISCRIMINATOR: - raise ValueError("Invalid discriminator for bonding curve") - - fields = struct.unpack_from(" Date: Thu, 24 Apr 2025 20:32:47 +0000 Subject: [PATCH 61/65] feat: add support for multiple bots --- bots/bot-sniper-1-geyser.yaml | 71 +++ bots/bot-sniper-2-logs.yaml | 71 +++ pyproject.toml | 3 +- src/bot_runner.py | 152 ++++++ src/cleanup/manager.py | 10 +- src/cleanup/modes.py | 37 +- src/cli.py | 142 ----- src/config.py | 112 ---- src/config_loader.py | 188 +++++++ src/core/client.py | 5 +- src/trading/trader.py | 505 ++++++++++++------ src/utils/logger.py | 16 +- ...DeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump.txt | 9 - trades/trades.log | 14 +- 14 files changed, 875 insertions(+), 460 deletions(-) create mode 100644 bots/bot-sniper-1-geyser.yaml create mode 100644 bots/bot-sniper-2-logs.yaml create mode 100644 src/bot_runner.py delete mode 100644 src/cli.py delete mode 100644 src/config.py create mode 100644 src/config_loader.py delete mode 100644 trades/LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump.txt diff --git a/bots/bot-sniper-1-geyser.yaml b/bots/bot-sniper-1-geyser.yaml new file mode 100644 index 0000000..432a69e --- /dev/null +++ b/bots/bot-sniper-1-geyser.yaml @@ -0,0 +1,71 @@ +# This file defines comprehensive parameters and settings for the trading bot. +# Carefully review and adjust values to match your trading strategy and risk tolerance. + +# Bot identification and connection settings +name: "bot-sniper-1" +env_file: ".env" +rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" +wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" +private_key: "${SOLANA_PRIVATE_KEY}" + +enabled: true # You can turn off the bot w/o removing its config +separate_process: true + +# Geyser configuration (fastest method for getting updates) +geyser: + endpoint: "${GEYSER_ENDPOINT}" + api_token: "${GEYSER_API_TOKEN}" + +# Trading parameters +# Control trade execution: amount of SOL per trade and acceptable price deviation +trade: + buy_amount: 0.0001 # Amount of SOL to spend when buying (in SOL) + buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%) + sell_slippage: 0.3 + + # EXTREME FAST mode configuration + # When enabled, skips waiting for the bonding curve to stabilize and RPC price check. + # The bot buys the specified number of tokens directly, making the process faster but less precise. + extreme_fast_mode: true + extreme_fast_token_amount: 20 # Amount of tokens to buy + +# Priority fee configuration +# Manage transaction speed and cost on the Solana network. +# Note: dynamic mode requires an additional RPC call, which slows down the buying process. +priority_fees: + enable_dynamic: false # Use latest transactions to estimate required fee (getRecentPrioritizationFees) + enable_fixed: true # Use fixed amount below + fixed_amount: 1_000_000 # Base fee in microlamports + extra_percentage: 0.0 # Percentage increase on riority fee regardless of the calculation method (0.1 = 10%) + hard_cap: 1_000_000 # Maximum allowable fee in microlamports to prevent excessive spending + +# Filters for token selection +filters: + match_string: null # Only process tokens with this string in name/symbol + bro_address: null # Only trade tokens created by this user address + listener_type: "geyser" # Method for detecting new tokens: "logs", "blocks", or "geyser" + max_token_age: 0.001 # Maximum token age in seconds for processing + marry_mode: false # Only buy tokens, skip selling + yolo_mode: false # Continuously trade tokens + +# Retry and timeout settings +retries: + max_attempts: 1 # Number of attempts for transaction submission + wait_after_creation: 15 # Seconds to wait after token creation (only if EXTREME FAST is disabled) + wait_after_buy: 15 # Holding period after buy transaction + wait_before_new_token: 15 # Pause between token trades + +# Token and account management +cleanup: + # Cleanup mode determines when to manage token accounts. Options: + # "disabled": no cleanup will occur. + # "on_fail": only clean up if a buy transaction fails. + # "after_sell": clean up after selling. + # "post_session": clean up all empty accounts after a trading session ends. + mode: "post_session" + force_close_with_burn: false # Force burning remaining tokens before closing account + with_priority_fee: false # Use priority fees for cleanup transactions + +# Node provider configuration (not implemented) +node: + max_rps: 25 # Maximum requests per second diff --git a/bots/bot-sniper-2-logs.yaml b/bots/bot-sniper-2-logs.yaml new file mode 100644 index 0000000..a078df7 --- /dev/null +++ b/bots/bot-sniper-2-logs.yaml @@ -0,0 +1,71 @@ +# This file defines comprehensive parameters and settings for the trading bot. +# Carefully review and adjust values to match your trading strategy and risk tolerance. + +# Bot identification and connection settings +name: "bot-sniper-2" +env_file: ".env" +rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" +wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" +private_key: "${SOLANA_PRIVATE_KEY}" + +enabled: false # You can turn off the bot w/o removing its config +separate_process: true + +# Geyser configuration (fastest method for getting updates) +geyser: + endpoint: "${GEYSER_ENDPOINT}" + api_token: "${GEYSER_API_TOKEN}" + +# Trading parameters +# Control trade execution: amount of SOL per trade and acceptable price deviation +trade: + buy_amount: 0.0001 # Amount of SOL to spend when buying (in SOL) + buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%) + sell_slippage: 0.3 + + # EXTREME FAST mode configuration + # When enabled, skips waiting for the bonding curve to stabilize and RPC price check. + # The bot buys the specified number of tokens directly, making the process faster but less precise. + extreme_fast_mode: true + extreme_fast_token_amount: 20 # Amount of tokens to buy + +# Priority fee configuration +# Manage transaction speed and cost on the Solana network. +# Note: dynamic mode requires an additional RPC call, which slows down the buying process. +priority_fees: + enable_dynamic: false # Use latest transactions to estimate required fee (getRecentPrioritizationFees) + enable_fixed: true # Use fixed amount below + fixed_amount: 200_000 # Base fee in microlamports + extra_percentage: 0.0 # Percentage increase on riority fee regardless of the calculation method (0.1 = 10%) + hard_cap: 200_000 # Maximum allowable fee in microlamports to prevent excessive spending + +# Filters for token selection +filters: + match_string: null # Only process tokens with this string in name/symbol + bro_address: null # Only trade tokens created by this user address + listener_type: "logs" # Method for detecting new tokens: "logs", "blocks", or "geyser" + max_token_age: 0.001 # Maximum token age in seconds for processing + marry_mode: false # Only buy tokens, skip selling + yolo_mode: false # Continuously trade tokens + +# Retry and timeout settings +retries: + max_attempts: 1 # Number of attempts for transaction submission + wait_after_creation: 15 # Seconds to wait after token creation (only if EXTREME FAST is disabled) + wait_after_buy: 15 # Holding period after buy transaction + wait_before_new_token: 15 # Pause between token trades + +# Token and account management +cleanup: + # Cleanup mode determines when to manage token accounts. Options: + # "disabled": no cleanup will occur. + # "on_fail": only clean up if a buy transaction fails. + # "after_sell": clean up after selling. + # "post_session": clean up all empty accounts after a trading session ends. + mode: "post_session" + force_close_with_burn: false # Force burning remaining tokens before closing account + with_priority_fee: false # Use priority fees for cleanup transactions + +# Node provider configuration (not implemented) +node: + max_rps: 25 # Maximum requests per second diff --git a/pyproject.toml b/pyproject.toml index 02a80b8..8750aa5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "grpcio>=1.71.0", "grpcio-tools>=1.71.0", "protobuf>=5.29.4", + "pyyaml>=6.0.2", ] [project.optional-dependencies] @@ -26,7 +27,7 @@ dev = [ ] [project.scripts] -pump_bot = "cli:sync_main" +pump_bot = "bot_runner:main" [tool.ruff] exclude = [ diff --git a/src/bot_runner.py b/src/bot_runner.py new file mode 100644 index 0000000..48a7f4a --- /dev/null +++ b/src/bot_runner.py @@ -0,0 +1,152 @@ +import asyncio +import logging +import multiprocessing +from datetime import datetime +from pathlib import Path + +from config_loader import load_bot_config, print_config_summary +from trading.trader import PumpTrader +from utils.logger import setup_file_logging + + +def setup_logging(bot_name: str): + """ + Set up logging to file for a specific bot instance. + + Args: + bot_name: Name of the bot for the log file + """ + log_dir = Path("logs") + log_dir.mkdir(exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + log_filename = log_dir / f"{bot_name}_{timestamp}.log" + + setup_file_logging(str(log_filename)) + +async def start_bot(config_path: str): + """ + Start a trading bot with the configuration from the specified path. + + Args: + config_path: Path to the YAML configuration file + """ + cfg = load_bot_config(config_path) + setup_logging(cfg["name"]) + print_config_summary(cfg) + + trader = PumpTrader( + # Connection settings + rpc_endpoint=cfg["rpc_endpoint"], + wss_endpoint=cfg["wss_endpoint"], + private_key=cfg["private_key"], + + # Trade parameters + buy_amount=cfg["trade"]["buy_amount"], + buy_slippage=cfg["trade"]["buy_slippage"], + sell_slippage=cfg["trade"]["sell_slippage"], + + # Extreme fast mode settings + extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False), + extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30), + + # Listener configuration + listener_type=cfg["filters"]["listener_type"], + + # Geyser configuration (if applicable) + geyser_endpoint=cfg.get("geyser", {}).get("endpoint"), + geyser_api_token=cfg.get("geyser", {}).get("api_token"), + + # Priority fee configuration + enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get("enable_dynamic", False), + enable_fixed_priority_fee=cfg.get("priority_fees", {}).get("enable_fixed", True), + fixed_priority_fee=cfg.get("priority_fees", {}).get("fixed_amount", 500000), + extra_priority_fee=cfg.get("priority_fees", {}).get("extra_percentage", 0.0), + hard_cap_prior_fee=cfg.get("priority_fees", {}).get("hard_cap", 500000), + + # Retry and timeout settings + max_retries=cfg.get("retries", {}).get("max_attempts", 10), + wait_time_after_creation=cfg.get("retries", {}).get("wait_after_creation", 15), + wait_time_after_buy=cfg.get("retries", {}).get("wait_after_buy", 15), + wait_time_before_new_token=cfg.get("retries", {}).get("wait_before_new_token", 15), + max_token_age=cfg.get("timing", {}).get("max_token_age", 0.001), + token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 30), + + # Cleanup settings + cleanup_mode=cfg.get("cleanup", {}).get("mode", "disabled"), + cleanup_force_close_with_burn=cfg.get("cleanup", {}).get("force_close_with_burn", False), + cleanup_with_priority_fee=cfg.get("cleanup", {}).get("with_priority_fee", False), + + # Trading filters + match_string=cfg["filters"].get("match_string"), + bro_address=cfg["filters"].get("bro_address"), + marry_mode=cfg["filters"].get("marry_mode", False), + yolo_mode=cfg["filters"].get("yolo_mode", False), + ) + + await trader.start() + +def run_all_bots(): + """ + Run all bots defined in YAML files in the 'bots' directory. + Only runs bots that have enabled=True (or where enabled is not specified). + Bots can be run in separate processes based on their configuration. + """ + bot_dir = Path("bots") + if not bot_dir.exists(): + logging.error(f"Bot directory '{bot_dir}' not found") + return + + bot_files = list(bot_dir.glob("*.yaml")) + if not bot_files: + logging.error(f"No bot configuration files found in '{bot_dir}'") + return + + logging.info(f"Found {len(bot_files)} bot configuration files") + + processes = [] + skipped_bots = 0 + + for file in bot_files: + try: + cfg = load_bot_config(str(file)) + bot_name = cfg.get("name", file.stem) + + # Skip bots with enabled=False + if not cfg.get("enabled", True): + logging.info(f"Skipping disabled bot '{bot_name}'") + skipped_bots += 1 + continue + + if cfg.get("separate_process", False): + logging.info(f"Starting bot '{bot_name}' in separate process") + p = multiprocessing.Process( + target=lambda path=str(file): asyncio.run(start_bot(path)), + name=f"bot-{bot_name}" + ) + p.start() + processes.append(p) + else: + logging.info(f"Starting bot '{bot_name}' in main process") + asyncio.run(start_bot(str(file))) + except Exception as e: + logging.exception(f"Failed to start bot from {file}: {e}") + + logging.info(f"Started {len(bot_files) - skipped_bots} bots, skipped {skipped_bots} disabled bots") + + for p in processes: + p.join() + logging.info(f"Process {p.name} completed") + + +def main() -> None: + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + run_all_bots() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/cleanup/manager.py b/src/cleanup/manager.py index 9bcb8dc..2d4dca8 100644 --- a/src/cleanup/manager.py +++ b/src/cleanup/manager.py @@ -1,7 +1,8 @@ +import asyncio + from solders.pubkey import Pubkey from spl.token.instructions import BurnParams, CloseAccountParams, burn, close_account -from config import CLEANUP_FORCE_CLOSE_WITH_BURN from core.client import SolanaClient from core.priority_fee.manager import PriorityFeeManager from core.pubkeys import SystemAddresses @@ -19,6 +20,7 @@ class AccountCleanupManager: wallet: Wallet, priority_fee_manager: PriorityFeeManager, use_priority_fee: bool = False, + force_burn: bool = False, ): """ Args: @@ -29,6 +31,7 @@ class AccountCleanupManager: self.wallet = wallet self.priority_fee_manager = priority_fee_manager self.use_priority_fee = use_priority_fee + self.close_with_force_burn = force_burn async def cleanup_ata(self, mint: Pubkey) -> None: """ @@ -44,6 +47,9 @@ class AccountCleanupManager: else None ) + logger.info("Waiting for 15 seconds for RPC node to synchronize...") + await asyncio.sleep(15) + try: info = await solana_client.get_account_info(ata, encoding="base64") if not info.value: @@ -53,7 +59,7 @@ class AccountCleanupManager: balance = await self.client.get_token_account_balance(ata) instructions = [] - if balance > 0 and CLEANUP_FORCE_CLOSE_WITH_BURN: + if balance > 0 and self.close_with_force_burn: logger.info(f"Burning {balance} tokens from ATA {ata} (mint: {mint})...") burn_ix = burn( BurnParams( diff --git a/src/cleanup/modes.py b/src/cleanup/modes.py index 8c11794..428f3b4 100644 --- a/src/cleanup/modes.py +++ b/src/cleanup/modes.py @@ -1,37 +1,42 @@ from cleanup.manager import AccountCleanupManager -from config import CLEANUP_MODE, CLEANUP_WITH_PRIORITY_FEE from utils.logger import get_logger logger = get_logger(__name__) -def should_cleanup_after_failure() -> bool: - return CLEANUP_MODE == "on_fail" +def should_cleanup_after_failure(cleanup_mode) -> bool: + return cleanup_mode == "on_fail" -def should_cleanup_after_sell() -> bool: - return CLEANUP_MODE == "after_sell" +def should_cleanup_after_sell(cleanup_mode) -> bool: + return cleanup_mode == "after_sell" -def should_cleanup_post_session() -> bool: - return CLEANUP_MODE == "post_session" +def should_cleanup_post_session(cleanup_mode) -> bool: + return cleanup_mode == "post_session" -async def handle_cleanup_after_failure(client, wallet, mint, priority_fee_manager): - if should_cleanup_after_failure(): +async def handle_cleanup_after_failure( + client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn + ): + if should_cleanup_after_failure(cleanup_mode): logger.info("[Cleanup] Triggered by failed buy transaction.") - manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE) + manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn) await manager.cleanup_ata(mint) -async def handle_cleanup_after_sell(client, wallet, mint, priority_fee_manager): - if should_cleanup_after_sell(): +async def handle_cleanup_after_sell( + client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn + ): + if should_cleanup_after_sell(cleanup_mode): logger.info("[Cleanup] Triggered after token sell.") - manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE) + manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn) await manager.cleanup_ata(mint) -async def handle_cleanup_post_session(client, wallet, mints, priority_fee_manager): - if should_cleanup_post_session(): +async def handle_cleanup_post_session( + client, wallet, mints, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn + ): + if should_cleanup_post_session(cleanup_mode): logger.info("[Cleanup] Triggered post trading session.") - manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE) + manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn) for mint in mints: await manager.cleanup_ata(mint) diff --git a/src/cli.py b/src/cli.py deleted file mode 100644 index 7d7d5ee..0000000 --- a/src/cli.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 -""" -Command-line interface for the pump.fun trading bot. -""" - -import argparse -import asyncio -import os -import sys - -from dotenv import load_dotenv - -import config -from trading.trader import PumpTrader -from utils.logger import get_logger, setup_file_logging - -load_dotenv() - -logger = get_logger(__name__) - - -def parse_args() -> argparse.Namespace: - """Parse command line arguments. - - Returns: - Parsed arguments - """ - parser = argparse.ArgumentParser(description="Trade tokens on pump.fun.") - parser.add_argument( - "--yolo", action="store_true", help="Run in YOLO mode (continuous trading)" - ) - parser.add_argument( - "--match", - type=str, - help="Only trade tokens with names or symbols matching this string", - ) - parser.add_argument( - "--bro", type=str, help="Only trade tokens created by this user address" - ) - parser.add_argument( - "--marry", action="store_true", help="Only buy tokens, skip selling" - ) - parser.add_argument( - "--amount", - type=float, - help=f"Amount of SOL to spend on each buy (default: {config.BUY_AMOUNT})", - ) - parser.add_argument( - "--buy-slippage", - type=float, - help=f"Buy slippage tolerance (default: {config.BUY_SLIPPAGE})", - ) - parser.add_argument( - "--sell-slippage", - type=float, - help=f"Sell slippage tolerance (default: {config.SELL_SLIPPAGE})", - ) - - return parser.parse_args() - - -async def main() -> None: - """Main entry point for the CLI.""" - setup_file_logging("pump_trading.log") - - args = parse_args() - - # Get configuration values, preferring command line args over config.py - rpc_endpoint: str | None = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") - wss_endpoint: str | None = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") - private_key: str | None = os.environ.get("SOLANA_PRIVATE_KEY") - geyser_endpoint: str | None = os.environ.get("GEYSER_ENDPOINT") - geyser_api_token: str | None = os.environ.get("GEYSER_API_TOKEN") - - - # Validate configuration values - if not rpc_endpoint or not rpc_endpoint.startswith(("http://", "https://")): - logger.error("Invalid RPC endpoint. Must start with http:// or https://") - sys.exit(1) - - if not wss_endpoint or not wss_endpoint.startswith(("ws://", "wss://")): - logger.error("Invalid WebSocket endpoint. Must start with ws:// or wss://") - sys.exit(1) - - if not private_key or len(private_key) < 80: - logger.error("Invalid private key. Key appears to be missing or too short") - sys.exit(1) - - if config.LISTENER_TYPE.lower() == "geyser": - if not geyser_endpoint: - logger.error("GEYSER_ENDPOINT environment variable is not set") - sys.exit(1) - if not geyser_api_token: - logger.error("GEYSER_API_TOKEN environment variable is not set") - sys.exit(1) - - # Get trading parameters - buy_amount: float = args.amount if args.amount is not None else config.BUY_AMOUNT - buy_slippage: float = ( - args.buy_slippage if args.buy_slippage is not None else config.BUY_SLIPPAGE - ) - sell_slippage: float = ( - args.sell_slippage if args.sell_slippage is not None else config.SELL_SLIPPAGE - ) - - trader: PumpTrader = PumpTrader( - rpc_endpoint=rpc_endpoint, # type: ignore - wss_endpoint=wss_endpoint, # type: ignore - private_key=private_key, - buy_amount=buy_amount, - buy_slippage=buy_slippage, - sell_slippage=sell_slippage, - max_retries=config.MAX_RETRIES, - listener_type=config.LISTENER_TYPE, - geyser_endpoint=geyser_endpoint, - geyser_api_token=geyser_api_token, - ) - - try: - await trader.start( - match_string=args.match, - bro_address=args.bro, - marry_mode=args.marry, - yolo_mode=args.yolo, - ) - except KeyboardInterrupt: - logger.info("Trading stopped by user") - except Exception as e: - logger.error(f"Trading stopped due to error: {e!s}") - finally: - try: - await trader.solana_client.close() - except Exception: - pass - - -def sync_main() -> None: - asyncio.run(main()) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/src/config.py b/src/config.py deleted file mode 100644 index 70497bd..0000000 --- a/src/config.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -Configuration for the pump.fun trading bot. - -This file defines comprehensive parameters and settings for the trading bot. -Carefully review and adjust values to match your trading strategy and risk tolerance. -""" - -# Trading parameters -# Control trade execution: amount of SOL per trade and acceptable price deviation -BUY_AMOUNT: int | float = 0.000_001 # Amount of SOL to spend when buying -BUY_SLIPPAGE: float = 0.4 # Maximum acceptable price deviation (0.4 = 40%) -SELL_SLIPPAGE: float = 0.4 # Consistent slippage tolerance to maintain trading strategy - - -# EXTREME FAST Mode configuration -# When enabled, skips waiting for the bonding curve to stabilize and RPC price check. -# The bot buys the specified number of tokens directly, making the process faster but less precise. -EXTREME_FAST_MODE: bool = True -# Amount of tokens to buy in EXTREME FAST mode. No price calculation is done; the bot buys exactly this amount. -EXTREME_FAST_TOKEN_AMOUNT: int = 30 - - -# Priority fee configuration -# Manage transaction speed and cost on the Solana network -ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Adaptive fee calculation -ENABLE_FIXED_PRIORITY_FEE: bool = True # Use consistent, predictable fee -FIXED_PRIORITY_FEE: int = 500_000 # Base fee in microlamports -EXTRA_PRIORITY_FEE: float = 0.0 # Percentage increase on priority fee (0.1 = 10%) -HARD_CAP_PRIOR_FEE: int = 500_000 # Maximum allowable fee to prevent excessive spending in microlamports - - -# Listener configuration -# Choose method for detecting new tokens on the network -# "logs": Recommended for more stable token detection -# "blocks": Unstable method, potentially less reliable -# "geyser": The fastest way for getting updates, requires Geyser endpoint -LISTENER_TYPE = "logs" - - -# Retry and timeout settings -# Control bot resilience and transaction handling -MAX_RETRIES: int = 10 # Number of attempts for transaction submission - -# Waiting periods in seconds between actions (TODO: to be replaced with retry mechanism) -WAIT_TIME_AFTER_CREATION: int | float = 15 # Seconds to wait after token creation -WAIT_TIME_AFTER_BUY: int | float = 15 # Holding period after buy transaction -WAIT_TIME_BEFORE_NEW_TOKEN: int | float = 15 # Pause between token trades - - -# Token and account management -# Control token processing and account cleanup strategies -MAX_TOKEN_AGE: int | float = 0.001 # Maximum token age in seconds for processing - -# Cleanup mode determines when to manage token accounts. Options: -# "disabled": No cleanup will occur. -# "on_fail": Only clean up if a buy transaction fails. -# "after_sell": Clean up after selling, but only if the balance is zero. -# "post_session": Clean up all empty accounts after a trading session ends. -CLEANUP_MODE: str = "disabled" -CLEANUP_FORCE_CLOSE_WITH_BURN: bool = False # Burn remaining tokens before closing account, else skip ATA with non-zero balances -CLEANUP_WITH_PRIORITY_FEE: bool = False # Use priority fees for cleanup transactions - - -# Node provider configuration (TODO: to be implemented) -# Manage RPC node interaction to prevent rate limiting -MAX_RPS: int = 25 # Maximum requests per second - - -def validate_configuration() -> None: - """ - Comprehensive validation of bot configuration. - - Checks: - - Type correctness - - Value ranges - - Logical consistency of settings - """ - # Configuration validation checks - config_checks = [ - # (value, type, min_value, max_value, error_message) - (BUY_AMOUNT, (int, float), 0, float('inf'), "BUY_AMOUNT must be a positive number"), - (BUY_SLIPPAGE, float, 0, 1, "BUY_SLIPPAGE must be between 0 and 1"), - (SELL_SLIPPAGE, float, 0, 1, "SELL_SLIPPAGE must be between 0 and 1"), - (FIXED_PRIORITY_FEE, int, 0, float('inf'), "FIXED_PRIORITY_FEE must be a non-negative integer"), - (EXTRA_PRIORITY_FEE, float, 0, 1, "EXTRA_PRIORITY_FEE must be between 0 and 1"), - (HARD_CAP_PRIOR_FEE, int, 0, float('inf'), "HARD_CAP_PRIOR_FEE must be a non-negative integer"), - (MAX_RETRIES, int, 0, 100, "MAX_RETRIES must be between 0 and 100") - ] - - for value, expected_type, min_val, max_val, error_msg in config_checks: - if not isinstance(value, expected_type): - raise ValueError(f"Type error: {error_msg}") - - if isinstance(value, (int, float)) and not (min_val <= value <= max_val): - raise ValueError(f"Range error: {error_msg}") - - # Logical consistency checks - if ENABLE_DYNAMIC_PRIORITY_FEE and ENABLE_FIXED_PRIORITY_FEE: - raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously") - - # Validate listener type - if LISTENER_TYPE not in ["logs", "blocks", "geyser"]: - raise ValueError("LISTENER_TYPE must be either 'logs' or 'blocks'") - - # Validate cleanup mode - valid_cleanup_modes = ["disabled", "on_fail", "after_sell", "post_session"] - if CLEANUP_MODE not in valid_cleanup_modes: - raise ValueError(f"CLEANUP_MODE must be one of {valid_cleanup_modes}") - - -# Validate configuration on import -validate_configuration() \ No newline at end of file diff --git a/src/config_loader.py b/src/config_loader.py new file mode 100644 index 0000000..65b80f3 --- /dev/null +++ b/src/config_loader.py @@ -0,0 +1,188 @@ +import os +from typing import Any + +import yaml +from dotenv import load_dotenv + +REQUIRED_FIELDS = [ + "name", "rpc_endpoint", "wss_endpoint", "private_key", + "trade.buy_amount", "trade.buy_slippage", "trade.sell_slippage", + "filters.listener_type", "filters.max_token_age" +] + +CONFIG_VALIDATION_RULES = [ + # (path, type, min_value, max_value, error_message) + ("trade.buy_amount", (int, float), 0, float('inf'), "trade.buy_amount must be a positive number"), + ("trade.buy_slippage", float, 0, 1, "trade.buy_slippage must be between 0 and 1"), + ("trade.sell_slippage", float, 0, 1, "trade.sell_slippage must be between 0 and 1"), + ("priority_fees.fixed_amount", int, 0, float('inf'), "priority_fees.fixed_amount must be a non-negative integer"), + ("priority_fees.extra_percentage", float, 0, 1, "priority_fees.extra_percentage must be between 0 and 1"), + ("priority_fees.hard_cap", int, 0, float('inf'), "priority_fees.hard_cap must be a non-negative integer"), + ("retries.max_attempts", int, 0, 100, "retries.max_attempts must be between 0 and 100"), + ("filters.max_token_age", (int, float), 0, float('inf'), "filters.max_token_age must be a non-negative number") +] + +# Valid values for enum-like fields +VALID_VALUES = { + "filters.listener_type": ["logs", "blocks", "geyser"], + "cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"] +} + + +def load_bot_config(path: str) -> dict: + """ + Load and validate a bot configuration from a YAML file. + + Args: + path: Path to the YAML configuration file (relative or absolute) + + Returns: + Validated configuration dictionary + + Raises: + FileNotFoundError: If the configuration file doesn't exist + ValueError: If the configuration is invalid + """ + with open(path) as f: + config = yaml.safe_load(f) + + env_file = config.get("env_file") + if env_file: + env_path = os.path.join(os.path.dirname(path), env_file) + if os.path.exists(env_path): + load_dotenv(env_path) + else: + # If not found relative to config, try relative to current working directory + load_dotenv(env_file) + + resolve_env_vars(config) + validate_config(config) + + return config + +def resolve_env_vars(config: dict) -> None: + """ + Recursively resolve environment variables in the configuration. + + Args: + config: Configuration dictionary to process + """ + def resolve_env(value): + if isinstance(value, str) and value.startswith("${") and value.endswith("}"): + env_var = value[2:-1] + env_value = os.getenv(env_var) + if env_value is None: + raise ValueError(f"Environment variable '{env_var}' not found") + return env_value + return value + + def resolve_all(d): + for k, v in d.items(): + if isinstance(v, dict): + resolve_all(v) + else: + d[k] = resolve_env(v) + + resolve_all(config) + +def get_nested_value(config: dict, path: str) -> Any: + """ + Get a nested value from the configuration using dot notation. + + Args: + config: Configuration dictionary + path: Path to the value using dot notation (e.g., "trade.buy_amount") + + Returns: + The value at the specified path + + Raises: + ValueError: If the path doesn't exist in the configuration + """ + keys = path.split(".") + value = config + for key in keys: + if not isinstance(value, dict) or key not in value: + raise ValueError(f"Missing required config key: {path}") + value = value[key] + return value + +def validate_config(config: dict) -> None: + """ + Validate the configuration against defined rules. + + Args: + config: Configuration dictionary to validate + + Raises: + ValueError: If the configuration is invalid + """ + for field in REQUIRED_FIELDS: + get_nested_value(config, field) + + for path, expected_type, min_val, max_val, error_msg in CONFIG_VALIDATION_RULES: + try: + value = get_nested_value(config, path) + + if not isinstance(value, expected_type): + raise ValueError(f"Type error: {error_msg}") + + if isinstance(value, (int, float)) and not (min_val <= value <= max_val): + raise ValueError(f"Range error: {error_msg}") + + except ValueError as e: + # Re-raise if it's our own error + if str(e).startswith(("Type error:", "Range error:")): + raise + # Otherwise, the field might be missing + continue + + # Validate enum-like fields + for path, valid_values in VALID_VALUES.items(): + try: + value = get_nested_value(config, path) + if value not in valid_values: + raise ValueError(f"{path} must be one of {valid_values}") + except ValueError: + # Skip if the field is missing + continue + + # Cannot enable both dynamic and fixed priority fees + try: + dynamic = get_nested_value(config, "priority_fees.enable_dynamic") + fixed = get_nested_value(config, "priority_fees.enable_fixed") + if dynamic and fixed: + raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously") + except ValueError: + # Skip if one of the fields is missing + pass + +def print_config_summary(config: dict) -> None: + """ + Print a summary of the loaded configuration. + + Args: + config: Configuration dictionary + """ + print(f"Bot name: {config.get('name', 'unnamed')}") + print(f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}") + + trade = config.get('trade', {}) + print("Trade settings:") + print(f" - Buy amount: {trade.get('buy_amount', 'not configured')} SOL") + print(f" - Buy slippage: {trade.get('buy_slippage', 'not configured') * 100}%") + print(f" - Extreme fast mode: {'enabled' if trade.get('extreme_fast_mode') else 'disabled'}") + + fees = config.get('priority_fees', {}) + print("Priority fees:") + if fees.get('enable_dynamic'): + print(" - Dynamic fees enabled") + elif fees.get('enable_fixed'): + print(f" - Fixed fee: {fees.get('fixed_amount', 'not configured')} microlamports") + + print("Configuration loaded successfully!") + + +if __name__ == "__main__": + config = load_bot_config("bots/bot-sniper.yaml") + print_config_summary(config) \ No newline at end of file diff --git a/src/core/client.py b/src/core/client.py index d0caa16..4193064 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -47,7 +47,8 @@ class SolanaClient: self._cached_blockhash = blockhash except Exception as e: logger.warning(f"Blockhash fetch failed: {e!s}") - await asyncio.sleep(interval) + finally: + await asyncio.sleep(interval) async def get_cached_blockhash(self) -> Hash: """Return the most recently cached blockhash.""" @@ -195,7 +196,7 @@ class SolanaClient: """ client = await self.get_client() try: - await client.confirm_transaction(signature, commitment=commitment) + await client.confirm_transaction(signature, commitment=commitment, sleep_seconds=1) return True except Exception as e: logger.error(f"Failed to confirm transaction {signature}: {e!s}") diff --git a/src/trading/trader.py b/src/trading/trader.py index a690c55..b69aa5f 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -10,7 +10,6 @@ from datetime import datetime from solders.pubkey import Pubkey -import config from cleanup.modes import ( handle_cleanup_after_failure, handle_cleanup_after_sell, @@ -34,7 +33,6 @@ logger = get_logger(__name__) class PumpTrader: """Coordinates trading operations for pump.fun tokens with focus on freshness.""" - def __init__( self, rpc_endpoint: str, @@ -43,13 +41,40 @@ class PumpTrader: buy_amount: float, buy_slippage: float, sell_slippage: float, - max_retries: int = 5, listener_type: str = "logs", geyser_endpoint: str | None = None, geyser_api_token: str | None = None, + + extreme_fast_mode: bool = False, + extreme_fast_token_amount: int = 30, + + # Priority fee configuration + enable_dynamic_priority_fee: bool = False, + enable_fixed_priority_fee: bool = True, + fixed_priority_fee: int = 200_000, + extra_priority_fee: float = 0.0, + hard_cap_prior_fee: int = 200_000, + + # Retry and timeout settings + max_retries: int = 3, + wait_time_after_creation: int = 15, # here and further - seconds + wait_time_after_buy: int = 15, + wait_time_before_new_token: int = 15, + max_token_age: int | float = 0.001, + token_wait_timeout: int = 30, + + # Cleanup settings + cleanup_mode: str = "disabled", + cleanup_force_close_with_burn: bool = False, + cleanup_with_priority_fee: bool = False, + + # Trading filters + match_string: str | None = None, + bro_address: str | None = None, + marry_mode: bool = False, + yolo_mode: bool = False, ): """Initialize the pump trader. - Args: rpc_endpoint: RPC endpoint URL wss_endpoint: WebSocket endpoint URL @@ -57,24 +82,47 @@ class PumpTrader: buy_amount: Amount of SOL to spend on buys buy_slippage: Slippage tolerance for buys sell_slippage: Slippage tolerance for sells - max_retries: Maximum number of retry attempts + listener_type: Type of listener to use ('logs', 'blocks', or 'geyser') geyser_endpoint: Geyser endpoint URL (required for geyser listener) geyser_api_token: Geyser API token (required for geyser listener) + + extreme_fast_mode: Whether to enable extreme fast mode + extreme_fast_token_amount: Maximum token amount for extreme fast mode + + enable_dynamic_priority_fee: Whether to enable dynamic priority fees + enable_fixed_priority_fee: Whether to enable fixed priority fees + fixed_priority_fee: Fixed priority fee amount + extra_priority_fee: Extra percentage for priority fees + hard_cap_prior_fee: Hard cap for priority fees + + max_retries: Maximum number of retry attempts + wait_time_after_creation: Time to wait after token creation (seconds) + wait_time_after_buy: Time to wait after buying a token (seconds) + wait_time_before_new_token: Time to wait before processing a new token (seconds) + max_token_age: Maximum age of token to process (seconds) + token_wait_timeout: Timeout for waiting for a token in single-token mode (seconds) + + cleanup_mode: Cleanup mode ("disabled", "auto", or "manual") + cleanup_force_close_with_burn: Whether to force close with burn during cleanup + cleanup_with_priority_fee: Whether to use priority fees during cleanup + + match_string: Optional string to match in token name/symbol + bro_address: Optional creator address to filter by + marry_mode: If True, only buy tokens and skip selling + yolo_mode: If True, trade continuously """ self.solana_client = SolanaClient(rpc_endpoint) self.wallet = Wallet(private_key) self.curve_manager = BondingCurveManager(self.solana_client) - self.priority_fee_manager = PriorityFeeManager( client=self.solana_client, - enable_dynamic_fee=config.ENABLE_DYNAMIC_PRIORITY_FEE, - enable_fixed_fee=config.ENABLE_FIXED_PRIORITY_FEE, - fixed_fee=config.FIXED_PRIORITY_FEE, - extra_fee=config.EXTRA_PRIORITY_FEE, - hard_cap=config.HARD_CAP_PRIOR_FEE, + enable_dynamic_fee=enable_dynamic_priority_fee, + enable_fixed_fee=enable_fixed_priority_fee, + fixed_fee=fixed_priority_fee, + extra_fee=extra_priority_fee, + hard_cap=hard_cap_prior_fee, ) - self.buyer = TokenBuyer( self.solana_client, self.wallet, @@ -83,10 +131,9 @@ class PumpTrader: buy_amount, buy_slippage, max_retries, - config.EXTREME_FAST_TOKEN_AMOUNT, - config.EXTREME_FAST_MODE + extreme_fast_token_amount, + extreme_fast_mode ) - self.seller = TokenSeller( self.solana_client, self.wallet, @@ -95,7 +142,7 @@ class PumpTrader: sell_slippage, max_retries, ) - + # Initialize the appropriate listener type listener_type = listener_type.lower() if listener_type == "geyser": @@ -114,69 +161,166 @@ class PumpTrader: else: self.token_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM) logger.info("Using blockSubscribe listener for token monitoring") - + + # Trading parameters self.buy_amount = buy_amount self.buy_slippage = buy_slippage self.sell_slippage = sell_slippage self.max_retries = max_retries - self.max_token_age = config.MAX_TOKEN_AGE + self.extreme_fast_mode = extreme_fast_mode + self.extreme_fast_token_amount = extreme_fast_token_amount + + # Timing parameters + self.wait_time_after_creation = wait_time_after_creation + self.wait_time_after_buy = wait_time_after_buy + self.wait_time_before_new_token = wait_time_before_new_token + self.max_token_age = max_token_age + self.token_wait_timeout = token_wait_timeout + + # Cleanup parameters + self.cleanup_mode = cleanup_mode + self.cleanup_force_close_with_burn = cleanup_force_close_with_burn + self.cleanup_with_priority_fee = cleanup_with_priority_fee + # Trading filters/modes + self.match_string = match_string + self.bro_address = bro_address + self.marry_mode = marry_mode + self.yolo_mode = yolo_mode + + # State tracking self.traded_mints: set[Pubkey] = set() - - # Token processing state - self.token_queue = asyncio.Queue() - self.processing = False + self.token_queue: asyncio.Queue = asyncio.Queue() + self.processing: bool = False self.processed_tokens: set[str] = set() self.token_timestamps: dict[str, float] = {} - async def start( - self, - match_string: str | None = None, - bro_address: str | None = None, - marry_mode: bool = False, - yolo_mode: bool = False, - ) -> None: - """Start the trading bot. - - Args: - match_string: Optional string to match in token name/symbol - bro_address: Optional creator address to filter by - marry_mode: If True, only buy tokens and skip selling - yolo_mode: If True, trade continuously - """ + async def start(self) -> None: + """Start the trading bot and listen for new tokens.""" logger.info("Starting pump.fun trader") - logger.info(f"Match filter: {match_string if match_string else 'None'}") - logger.info(f"Creator filter: {bro_address if bro_address else 'None'}") - logger.info(f"Marry mode: {marry_mode}") - logger.info(f"YOLO mode: {yolo_mode}") + logger.info(f"Match filter: {self.match_string if self.match_string else 'None'}") + logger.info(f"Creator filter: {self.bro_address if self.bro_address else 'None'}") + logger.info(f"Marry mode: {self.marry_mode}") + logger.info(f"YOLO mode: {self.yolo_mode}") logger.info(f"Max token age: {self.max_token_age} seconds") - # Start processor task - processor_task = asyncio.create_task( - self._process_token_queue(marry_mode, yolo_mode) - ) - try: - await self.token_listener.listen_for_tokens( - lambda token: self._queue_token(token), - match_string, - bro_address, - ) + # Choose operating mode based on yolo_mode + if not self.yolo_mode: + # Single token mode: process one token and exit + logger.info("Running in single token mode - will process one token and exit") + token_info = await self._wait_for_token() + if token_info: + await self._handle_token(token_info) + logger.info("Finished processing single token. Exiting...") + else: + logger.info(f"No suitable token found within timeout period ({self.token_wait_timeout}s). Exiting...") + else: + # Continuous mode: process tokens until interrupted + logger.info("Running in continuous mode - will process tokens until interrupted") + processor_task = asyncio.create_task( + self._process_token_queue() + ) + try: + await self.token_listener.listen_for_tokens( + lambda token: self._queue_token(token), + self.match_string, + self.bro_address, + ) + except Exception as e: + logger.error(f"Token listening stopped due to error: {e!s}") + finally: + processor_task.cancel() + try: + await processor_task + except asyncio.CancelledError: + pass + except Exception as e: logger.error(f"Trading stopped due to error: {e!s}") - processor_task.cancel() - await self.solana_client.close() - + finally: - processor_task.cancel() - if self.traded_mints: - # Close ATA if enabled - await handle_cleanup_post_session(self.solana_client, self.wallet, list(self.traded_mints), self.priority_fee_manager) - await self.solana_client.close() + await self._cleanup_resources() + logger.info("Pump trader has shut down") - async def _queue_token(self, token_info: TokenInfo) -> None: - """Queue a token for processing if not already processed.""" + async def _wait_for_token(self) -> TokenInfo | None: + """Wait for a single token to be detected. + + Returns: + TokenInfo or None if timeout occurs + """ + # Create a one-time event to signal when a token is found + token_found = asyncio.Event() + found_token = None + + async def token_callback(token: TokenInfo) -> None: + nonlocal found_token + token_key = str(token.mint) + + # Only process if not already processed and fresh + if token_key not in self.processed_tokens: + # Record when the token was discovered + self.token_timestamps[token_key] = asyncio.get_event_loop().time() + found_token = token + self.processed_tokens.add(token_key) + token_found.set() + + listener_task = asyncio.create_task( + self.token_listener.listen_for_tokens( + token_callback, + self.match_string, + self.bro_address, + ) + ) + + # Wait for a token with a timeout + try: + logger.info(f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)...") + await asyncio.wait_for(token_found.wait(), timeout=self.token_wait_timeout) + logger.info(f"Found token: {found_token.symbol} ({found_token.mint})") + return found_token + except TimeoutError: + logger.info(f"Timed out after waiting {self.token_wait_timeout}s for a token") + return None + finally: + listener_task.cancel() + try: + await listener_task + except asyncio.CancelledError: + pass + + async def _cleanup_resources(self) -> None: + """Perform cleanup operations before shutting down.""" + if self.traded_mints: + try: + logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...") + await handle_cleanup_post_session( + self.solana_client, + self.wallet, + list(self.traded_mints), + self.priority_fee_manager, + self.cleanup_mode, + self.cleanup_with_priority_fee, + self.cleanup_force_close_with_burn + ) + except Exception as e: + logger.error(f"Error during cleanup: {e!s}") + + old_keys = {k for k in self.token_timestamps if k not in self.processed_tokens} + for key in old_keys: + self.token_timestamps.pop(key, None) + + await self.solana_client.close() + + async def _queue_token( + self, token_info: TokenInfo + ) -> None: + """Queue a token for processing if not already processed. + + Args: + token_info: Token information to queue + """ token_key = str(token_info.mint) if token_key in self.processed_tokens: @@ -189,126 +333,178 @@ class PumpTrader: await self.token_queue.put(token_info) logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})") - async def _process_token_queue(self, marry_mode: bool, yolo_mode: bool) -> None: + async def _process_token_queue(self) -> None: """Continuously process tokens from the queue, only if they're fresh.""" while True: - token_info = await self.token_queue.get() - token_key = str(token_info.mint) + try: + token_info = await self.token_queue.get() + token_key = str(token_info.mint) - # Check if token is still "fresh" - current_time = asyncio.get_event_loop().time() - token_age = current_time - self.token_timestamps.get( - token_key, current_time - ) - - if token_age > self.max_token_age: - logger.info( - f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)" + # Check if token is still "fresh" + current_time = asyncio.get_event_loop().time() + token_age = current_time - self.token_timestamps.get( + token_key, current_time ) + + if token_age > self.max_token_age: + logger.info( + f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)" + ) + #self.token_queue.task_done() + continue + + self.processed_tokens.add(token_key) + + logger.info( + f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)" + ) + await self._handle_token(token_info) + + except asyncio.CancelledError: + # Handle cancellation gracefully + logger.info("Token queue processor was cancelled") + break + except Exception as e: + logger.error(f"Error in token queue processor: {e!s}") + finally: self.token_queue.task_done() - continue - - self.processed_tokens.add(token_key) - - logger.info( - f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)" - ) - await self._handle_token(token_info, marry_mode, yolo_mode) - - self.token_queue.task_done() async def _handle_token( - self, token_info: TokenInfo, marry_mode: bool, yolo_mode: bool + self, token_info: TokenInfo ) -> None: """Handle a new token creation event. Args: token_info: Token information - marry_mode: If True, only buy tokens and skip selling - yolo_mode: If True, continue trading after this token """ try: + # Save token info to file await self._save_token_info(token_info) - if not config.EXTREME_FAST_MODE: + # Wait for bonding curve to stabilize (unless in extreme fast mode) + if not self.extreme_fast_mode: logger.info( - f"Waiting for {config.WAIT_TIME_AFTER_CREATION} seconds for the bonding curve to stabilize..." + f"Waiting for {self.wait_time_after_creation} seconds for the bonding curve to stabilize..." ) - await asyncio.sleep(config.WAIT_TIME_AFTER_CREATION) + await asyncio.sleep(self.wait_time_after_creation) + # Buy token logger.info( f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol}..." ) buy_result: TradeResult = await self.buyer.execute(token_info) if buy_result.success: - logger.info(f"Successfully bought {token_info.symbol}") - self._log_trade( - "buy", - token_info, - buy_result.price, # type: ignore - buy_result.amount, # type: ignore - buy_result.tx_signature, - ) - self.traded_mints.add(token_info.mint) + await self._handle_successful_buy(token_info, buy_result) else: - logger.error( - f"Failed to buy {token_info.symbol}: {buy_result.error_message}" - ) - # Close ATA if enabled - await handle_cleanup_after_failure(self.solana_client, self.wallet, token_info.mint, self.priority_fee_manager) + await self._handle_failed_buy(token_info, buy_result) - # Sell token if not in marry mode - if not marry_mode and buy_result.success: + # Only wait for next token in yolo mode + if self.yolo_mode: logger.info( - f"Waiting for {config.WAIT_TIME_AFTER_BUY} seconds before selling..." + f"YOLO mode enabled. Waiting {self.wait_time_before_new_token} seconds before looking for next token..." ) - await asyncio.sleep(config.WAIT_TIME_AFTER_BUY) - - logger.info(f"Selling {token_info.symbol}...") - sell_result: TradeResult = await self.seller.execute(token_info) - - if sell_result.success: - logger.info(f"Successfully sold {token_info.symbol}") - self._log_trade( - "sell", - token_info, - sell_result.price, # type: ignore - sell_result.amount, # type: ignore - sell_result.tx_signature, - ) - # Close ATA if enabled - await handle_cleanup_after_sell(self.solana_client, self.wallet, token_info.mint, self.priority_fee_manager) - else: - logger.error( - f"Failed to sell {token_info.symbol}: {sell_result.error_message}" - ) - elif marry_mode: - logger.info("Marry mode enabled. Skipping sell operation.") - - # Wait before looking for the next token - if yolo_mode: - logger.info( - f"YOLO mode enabled. Waiting {config.WAIT_TIME_BEFORE_NEW_TOKEN} seconds before looking for next token..." - ) - await asyncio.sleep(config.WAIT_TIME_BEFORE_NEW_TOKEN) + await asyncio.sleep(self.wait_time_before_new_token) except Exception as e: logger.error(f"Error handling token {token_info.symbol}: {e!s}") - async def _save_token_info(self, token_info: TokenInfo) -> None: + async def _handle_successful_buy( + self, token_info: TokenInfo, buy_result: TradeResult + ) -> None: + """Handle successful token purchase. + + Args: + token_info: Token information + buy_result: The result of the buy operation + """ + logger.info(f"Successfully bought {token_info.symbol}") + self._log_trade( + "buy", + token_info, + buy_result.price, # type: ignore + buy_result.amount, # type: ignore + buy_result.tx_signature, + ) + self.traded_mints.add(token_info.mint) + + # Sell token if not in marry mode + if not self.marry_mode: + logger.info( + f"Waiting for {self.wait_time_after_buy} seconds before selling..." + ) + await asyncio.sleep(self.wait_time_after_buy) + + logger.info(f"Selling {token_info.symbol}...") + sell_result: TradeResult = await self.seller.execute(token_info) + + if sell_result.success: + logger.info(f"Successfully sold {token_info.symbol}") + self._log_trade( + "sell", + token_info, + sell_result.price, # type: ignore + sell_result.amount, # type: ignore + sell_result.tx_signature, + ) + # Close ATA if enabled + await handle_cleanup_after_sell( + self.solana_client, + self.wallet, + token_info.mint, + self.priority_fee_manager, + self.cleanup_mode, + self.cleanup_with_priority_fee, + self.cleanup_force_close_with_burn + ) + else: + logger.error( + f"Failed to sell {token_info.symbol}: {sell_result.error_message}" + ) + else: + logger.info("Marry mode enabled. Skipping sell operation.") + + async def _handle_failed_buy( + self, token_info: TokenInfo, buy_result: TradeResult + ) -> None: + """Handle failed token purchase. + + Args: + token_info: Token information + buy_result: The result of the buy operation + """ + logger.error( + f"Failed to buy {token_info.symbol}: {buy_result.error_message}" + ) + # Close ATA if enabled + await handle_cleanup_after_failure( + self.solana_client, + self.wallet, + token_info.mint, + self.priority_fee_manager, + self.cleanup_mode, + self.cleanup_with_priority_fee, + self.cleanup_force_close_with_burn + ) + + async def _save_token_info( + self, token_info: TokenInfo + ) -> None: """Save token information to a file. Args: token_info: Token information """ - os.makedirs("trades", exist_ok=True) - file_name = os.path.join("trades", f"{token_info.mint}.txt") + try: + os.makedirs("trades", exist_ok=True) + file_name = os.path.join("trades", f"{token_info.mint}.txt") - with open(file_name, "w") as file: - file.write(json.dumps(token_info.to_dict(), indent=2)) + with open(file_name, "w") as file: + file.write(json.dumps(token_info.to_dict(), indent=2)) - logger.info(f"Token information saved to {file_name}") + logger.info(f"Token information saved to {file_name}") + except Exception as e: + logger.error(f"Failed to save token information: {e!s}") def _log_trade( self, @@ -327,17 +523,20 @@ class PumpTrader: amount: Trade amount in SOL tx_hash: Transaction hash """ - os.makedirs("trades", exist_ok=True) + try: + os.makedirs("trades", exist_ok=True) - log_entry = { - "timestamp": datetime.utcnow().isoformat(), - "action": action, - "token_address": str(token_info.mint), - "symbol": token_info.symbol, - "price": price, - "amount": amount, - "tx_hash": str(tx_hash), - } + log_entry = { + "timestamp": datetime.utcnow().isoformat(), + "action": action, + "token_address": str(token_info.mint), + "symbol": token_info.symbol, + "price": price, + "amount": amount, + "tx_hash": str(tx_hash) if tx_hash else None, + } - with open("trades/trades.log", "a") as log_file: - log_file.write(json.dumps(log_entry) + "\n") + with open("trades/trades.log", "a") as log_file: + log_file.write(json.dumps(log_entry) + "\n") + except Exception as e: + logger.error(f"Failed to log trade information: {e!s}") \ No newline at end of file diff --git a/src/utils/logger.py b/src/utils/logger.py index 9fa07f5..0c0cf90 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -3,7 +3,6 @@ Logging utilities for the pump.fun trading bot. """ import logging -import sys # Global dict to store loggers _loggers: dict[str, logging.Logger] = {} @@ -27,16 +26,6 @@ def get_logger(name: str, level: int = logging.INFO) -> logging.Logger: logger = logging.getLogger(name) logger.setLevel(level) - if not logger.handlers: - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - - console_handler = logging.StreamHandler(sys.stdout) - console_handler.setFormatter(formatter) - logger.addHandler(console_handler) - _loggers[name] = logger return logger @@ -52,6 +41,11 @@ def setup_file_logging( """ root_logger = logging.getLogger() + # Check if file handler with same filename already exists + for handler in root_logger.handlers: + if isinstance(handler, logging.FileHandler) and handler.baseFilename == filename: + return # File handler already added + formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", diff --git a/trades/LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump.txt b/trades/LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump.txt deleted file mode 100644 index 987a833..0000000 --- a/trades/LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump.txt +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "RAKAMAKAFO", - "symbol": "MAKAFO", - "uri": "https://cf-ipfs.com/ipfs/QmYyNfffJL8aLzsQQBxSDEmbQ2STSPS35rfFwE7isjJiVu", - "mint": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", - "bondingCurve": "9UznPeXvYtPqq8saSyM65cC8LCtyAKPnLCGG7Mq1pUTq", - "associatedBondingCurve": "CTe6pwwgzEUvRmPGRE2EZXrESpiWawK5TwyJhgvFgJ6T", - "user": "Ev5WEEJ2Y1qKGE8KCNvBNVEgy9fJUuttA93pMo2fcGFd" -} \ No newline at end of file diff --git a/trades/trades.log b/trades/trades.log index d44d1ed..d226794 100644 --- a/trades/trades.log +++ b/trades/trades.log @@ -1,12 +1,2 @@ -{"timestamp": "2024-08-22T10:51:30.306580", "action": "buy", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "XZDRuEnaBNEeuFAiGyJsqBmVF3uZvFqAQUKsZVKTxsThHxN43LiRqy3T4cSH4dRXGB7c5A4855iYQDLY4BNJJhP"} -{"timestamp": "2024-08-22T10:51:56.116951", "action": "sell", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "WQHnEoyztfDf5FJyQRXDGxxtqf2EcMGs8SET43KuYU8Dpdunruku22SHnfiH2xsUi1hTBjC1msvTcnU78fFKrc2"} -{"timestamp": "2025-03-06T21:53:16.417950", "action": "buy", "token_address": "7ztobvXwBd8UBn2mUuatjpmEmn7EZpifXv98xjCrpump", "symbol": "$Auti", "price": 3.3860819583417394e-08, "amount": 29.532657871333047, "tx_hash": "4fUuh16912cK7xiZsvY1scu5P5NtWGwowcAYXQvvr1FWAvPD57mPKfrq5PMMzMULhMiVn86fiDCUfwWt6B7NPNxV"} -{"timestamp": "2025-03-06T21:53:33.704455", "action": "sell", "token_address": "7ztobvXwBd8UBn2mUuatjpmEmn7EZpifXv98xjCrpump", "symbol": "$Auti", "price": 3.38403327639918e-08, "amount": 29.532657, "tx_hash": "4TRFmKYkibrgn2fX4UT7mM8i7Lpu3FgXKvY1d6AsEUaxLj11JxWo61xxqYGW7B6wzdaY9LWVWZnTSDTZUUNs5pFU"} -{"timestamp": "2025-03-06T22:04:01.067407", "action": "buy", "token_address": "5jch2xH14RBWZgKwmxEUJwC4howFNBCBqbnZ5uguGujj", "symbol": "$MOONWEN", "price": 2.8543914418196173e-08, "amount": 35.03373732659877, "tx_hash": "VCWbh79TmNWtt9kjex5LLtWKGHjc3nPBQVJdynC7B85nbnXx4DBgwguqAe6StYVAUS7jzP17pN5FWWGvbiUBRfk"} -{"timestamp": "2025-03-06T22:04:48.843998", "action": "sell", "token_address": "5jch2xH14RBWZgKwmxEUJwC4howFNBCBqbnZ5uguGujj", "symbol": "$MOONWEN", "price": 2.8056120698010214e-08, "amount": 35.033737, "tx_hash": "RLx8hFVd5DVZdUg5vN41Fs8Yz66yTEUEvegasuHwYQ7DTJhPcCdggoNBtipb93VPVdtQLZydxc9ZKRZ1uzK6KzH"} -{"timestamp": "2025-03-06T22:05:11.444317", "action": "buy", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.383038210726932e-08, "amount": 29.559228649242023, "tx_hash": "3E4RT5kvRezWJmQKJ9wmcAiDcYZefohxFVNdEZPweE9vhq215rSmqBn4Dv4ypH5KUUuNJzKdhjyrGU2b47woWr9e"} -{"timestamp": "2025-03-06T22:05:25.789876", "action": "sell", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.3830384158620706e-08, "amount": 29.559228, "tx_hash": "3oyJVzsRvnQV5qBsEp4hSk97eFWzUjzfWWa16GMUXtMd7JBmbQkTkmvs7N8nagYcJvqfFhzhCKZEG6onxjYYQbwX"} -{"timestamp": "2025-04-03T21:55:46.849123", "action": "buy", "token_address": "4rHSLfYLkFmHTcyCF8thnUmyuX4MBrHdjdftZXM7pump", "symbol": "GCT", "price": 3.3333333333333334e-08, "amount": 30, "tx_hash": "35UncvxypdvuKC4gD3jXnmmtwfBfEhFc2Y2JLfszzvHqsZumcjp2CMkXH4HD7iexgkPEpG1je73bwnFWbNQjxNUS"} -{"timestamp": "2025-04-03T21:56:05.816803", "action": "sell", "token_address": "4rHSLfYLkFmHTcyCF8thnUmyuX4MBrHdjdftZXM7pump", "symbol": "GCT", "price": 3.1716731316027684e-08, "amount": 30.0, "tx_hash": "2NoMsScE5tJu3gzvrSn1uarnVZ4DcgXVLsc7BEwJ1ndU7T69xPbgyk53YW4MVCEfrSvdoaCup6VyM9pnf6YR6pGc"} -{"timestamp": "2025-04-03T21:56:49.686823", "action": "buy", "token_address": "Hx4wR3Z7tEjfVip8jHFFsF3d4LwNmXgjBRkNR9qSUarF", "symbol": "Pablo", "price": 5.067234074416521e-08, "amount": 19.734632055953472, "tx_hash": "52hQYXPjtP1Rr945UW5peef6qbxThUubyBbEbcnGCCJ146nxx6bZXj3mCpWtpqpmMe82oNkEFwMms26JqhAaJMXL"} -{"timestamp": "2025-04-03T21:57:09.656288", "action": "sell", "token_address": "Hx4wR3Z7tEjfVip8jHFFsF3d4LwNmXgjBRkNR9qSUarF", "symbol": "Pablo", "price": 2.8113188162567704e-08, "amount": 19.734632, "tx_hash": "xzpCFN8PgtPvXyRfQjkhHkPQcdzTNb6JXtCsQixtsMQabiBt9yNMSb7JLxRCT295rbVZuuocRNxYe6AqAiRnE91"} +{"timestamp": "2025-04-24T20:30:13.087092", "action": "buy", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 5e-06, "amount": 20, "tx_hash": "3JvdfCep45PUB6rCcH4dB2NuwvFP8n67SCUxqJMt4MuN5ekHYc6J27aCUfwNUK3hh5rSyKNYAWXya5vQAT2qQivB"} +{"timestamp": "2025-04-24T20:30:32.759177", "action": "sell", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 3.805530050663904e-08, "amount": 20.0, "tx_hash": "5cveLfU7XhPNCPMCZfTXyugJpmAQNmi7zr81PSqs8DsP1T2swYFjJwaB5hNSf3kFPfRzgzd7QZBVaZLd5MqsJevB"} From 2ef1a09b8d8caa108fd0106170f9cde5b3c674ef Mon Sep 17 00:00:00 2001 From: smypmsa Date: Fri, 25 Apr 2025 07:33:44 +0000 Subject: [PATCH 62/65] fix: decrease tx CUs, add explicit commitment level for getLatestBlockhash --- src/core/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/client.py b/src/core/client.py index 4193064..6d5fd16 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -120,7 +120,7 @@ class SolanaClient: Recent blockhash as string """ client = await self.get_client() - response = await client.get_latest_blockhash() + response = await client.get_latest_blockhash(commitment="processed") return response.value.blockhash async def build_and_send_transaction( @@ -152,7 +152,7 @@ class SolanaClient: # Add priority fee instructions if applicable if priority_fee is not None: fee_instructions = [ - set_compute_unit_limit(100_000), # Default compute unit limit + set_compute_unit_limit(75_000), # Default compute unit limit set_compute_unit_price(priority_fee), ] instructions = fee_instructions + instructions From 465e804d393303903415c9d1790d3747c4e3cc21 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Fri, 25 Apr 2025 09:38:19 +0000 Subject: [PATCH 63/65] feat: add warming up endpoint, minor optimizations --- pyproject.toml | 1 + src/bot_runner.py | 4 ++++ src/core/client.py | 13 ++++++++++++- src/trading/buyer.py | 14 +++++++------- src/trading/trader.py | 18 ++++++++++++------ 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8750aa5..30cbbed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "grpcio-tools>=1.71.0", "protobuf>=5.29.4", "pyyaml>=6.0.2", + "uvloop>=0.21.0", ] [project.optional-dependencies] diff --git a/src/bot_runner.py b/src/bot_runner.py index 48a7f4a..35a53ae 100644 --- a/src/bot_runner.py +++ b/src/bot_runner.py @@ -4,6 +4,10 @@ import multiprocessing from datetime import datetime from pathlib import Path +import uvloop + +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + from config_loader import load_bot_config, print_config_summary from trading.trader import PumpTrader from utils.logger import setup_file_logging diff --git a/src/core/client.py b/src/core/client.py index 6d5fd16..786d4b6 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -80,6 +80,17 @@ class SolanaClient: await self._client.close() self._client = None + async def get_health(self) -> str | None: + body = { + "jsonrpc": "2.0", + "id": 1, + "method": "getHealth", + } + result = await self.post_rpc(body) + if result and "result" in result: + return result["result"] + return None + async def get_account_info(self, pubkey: Pubkey) -> dict[str, Any]: """Get account info from the blockchain. @@ -152,7 +163,7 @@ class SolanaClient: # Add priority fee instructions if applicable if priority_fee is not None: fee_instructions = [ - set_compute_unit_limit(75_000), # Default compute unit limit + set_compute_unit_limit(70_000), # Default compute unit limit set_compute_unit_price(priority_fee), ] instructions = fee_instructions + instructions diff --git a/src/trading/buyer.py b/src/trading/buyer.py index 9c565f8..7047d3f 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -92,13 +92,6 @@ class TokenBuyer(Trader): # Calculate maximum SOL to spend with slippage max_amount_lamports = int(amount_lamports * (1 + self.slippage)) - logger.info( - f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token" - ) - logger.info( - f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)" - ) - associated_token_account = self.wallet.get_associated_token_address( token_info.mint ) @@ -110,6 +103,13 @@ class TokenBuyer(Trader): max_amount_lamports, ) + logger.info( + f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token" + ) + logger.info( + f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)" + ) + success = await self.client.confirm_transaction(tx_signature) if success: diff --git a/src/trading/trader.py b/src/trading/trader.py index b69aa5f..8dcdd50 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -7,6 +7,7 @@ import asyncio import json import os from datetime import datetime +from time import monotonic from solders.pubkey import Pubkey @@ -204,6 +205,12 @@ class PumpTrader: logger.info(f"YOLO mode: {self.yolo_mode}") logger.info(f"Max token age: {self.max_token_age} seconds") + try: + health_resp = await self.solana_client.get_health() + logger.info(f"RPC warm-up successful (getHealth passed: {health_resp})") + except Exception as e: + logger.warning(f"RPC warm-up failed: {e!s}") + try: # Choose operating mode based on yolo_mode if not self.yolo_mode: @@ -261,7 +268,7 @@ class PumpTrader: # Only process if not already processed and fresh if token_key not in self.processed_tokens: # Record when the token was discovered - self.token_timestamps[token_key] = asyncio.get_event_loop().time() + self.token_timestamps[token_key] = monotonic() found_token = token self.processed_tokens.add(token_key) token_found.set() @@ -328,7 +335,7 @@ class PumpTrader: return # Record timestamp when token was discovered - self.token_timestamps[token_key] = asyncio.get_event_loop().time() + self.token_timestamps[token_key] = monotonic() await self.token_queue.put(token_info) logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})") @@ -341,7 +348,7 @@ class PumpTrader: token_key = str(token_info.mint) # Check if token is still "fresh" - current_time = asyncio.get_event_loop().time() + current_time = monotonic() token_age = current_time - self.token_timestamps.get( token_key, current_time ) @@ -378,11 +385,10 @@ class PumpTrader: token_info: Token information """ try: - # Save token info to file - await self._save_token_info(token_info) - # Wait for bonding curve to stabilize (unless in extreme fast mode) if not self.extreme_fast_mode: + # Save token info to file + # await self._save_token_info(token_info) logger.info( f"Waiting for {self.wait_time_after_creation} seconds for the bonding curve to stabilize..." ) From a36fab735277c1eff1510f5f358ec919e8c72349 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 29 Apr 2025 11:56:55 +0000 Subject: [PATCH 64/65] fix: comments, minor fixes --- bots/bot-sniper-1-geyser.yaml | 2 +- bots/bot-sniper-2-logs.yaml | 2 +- src/config_loader.py | 4 ++-- src/core/client.py | 2 +- src/trading/buyer.py | 2 +- src/trading/trader.py | 4 +++- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/bots/bot-sniper-1-geyser.yaml b/bots/bot-sniper-1-geyser.yaml index 432a69e..65b38c2 100644 --- a/bots/bot-sniper-1-geyser.yaml +++ b/bots/bot-sniper-1-geyser.yaml @@ -8,7 +8,7 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" private_key: "${SOLANA_PRIVATE_KEY}" -enabled: true # You can turn off the bot w/o removing its config +enabled: false # You can turn off the bot w/o removing its config separate_process: true # Geyser configuration (fastest method for getting updates) diff --git a/bots/bot-sniper-2-logs.yaml b/bots/bot-sniper-2-logs.yaml index a078df7..08c24b2 100644 --- a/bots/bot-sniper-2-logs.yaml +++ b/bots/bot-sniper-2-logs.yaml @@ -8,7 +8,7 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" private_key: "${SOLANA_PRIVATE_KEY}" -enabled: false # You can turn off the bot w/o removing its config +enabled: true # You can turn off the bot w/o removing its config separate_process: true # Geyser configuration (fastest method for getting updates) diff --git a/src/config_loader.py b/src/config_loader.py index 65b80f3..15be4a5 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -50,10 +50,10 @@ def load_bot_config(path: str) -> dict: if env_file: env_path = os.path.join(os.path.dirname(path), env_file) if os.path.exists(env_path): - load_dotenv(env_path) + load_dotenv(env_path, override=True) else: # If not found relative to config, try relative to current working directory - load_dotenv(env_file) + load_dotenv(env_file, override=True) resolve_env_vars(config) validate_config(config) diff --git a/src/core/client.py b/src/core/client.py index 786d4b6..5868d4d 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -163,7 +163,7 @@ class SolanaClient: # Add priority fee instructions if applicable if priority_fee is not None: fee_instructions = [ - set_compute_unit_limit(70_000), # Default compute unit limit + set_compute_unit_limit(72_000), # Default compute unit limit set_compute_unit_price(priority_fee), ] instructions = fee_instructions + instructions diff --git a/src/trading/buyer.py b/src/trading/buyer.py index 7047d3f..c0b2a2b 100644 --- a/src/trading/buyer.py +++ b/src/trading/buyer.py @@ -82,7 +82,7 @@ class TokenBuyer(Trader): # Skip the wait and directly calculate the amount token_amount = self.extreme_fast_token_amount token_price_sol = self.amount / token_amount - logger.info(f"EXTREME FAST Mode: Buying {token_amount} tokens.") + #logger.info(f"EXTREME FAST Mode: Buying {token_amount} tokens.") else: # Regular behavior with RPC call curve_state = await self.curve_manager.get_curve_state(token_info.bonding_curve) diff --git a/src/trading/trader.py b/src/trading/trader.py index 8dcdd50..0b28334 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -9,6 +9,7 @@ import os from datetime import datetime from time import monotonic +import uvloop from solders.pubkey import Pubkey from cleanup.modes import ( @@ -29,6 +30,8 @@ from trading.buyer import TokenBuyer from trading.seller import TokenSeller from utils.logger import get_logger +asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + logger = get_logger(__name__) @@ -357,7 +360,6 @@ class PumpTrader: logger.info( f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)" ) - #self.token_queue.task_done() continue self.processed_tokens.add(token_key) From 9a26aa7532a63f84a3d27c589c61405b6f04f8f4 Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:38:32 +0200 Subject: [PATCH 65/65] docs: add license --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.